With the launch of GPT-5, OpenAI expanded tool/function calling in the API to include free-form tool calls (raw text, no JSON), grammar constraints via Lark/CFG, tool allowlists, and improved tool-use reasoning. Together, these make GPT-5 a truly agentic model; you can connect APIs, databases, and custom tools to the Responses API and generate grounded answers or even automate workflows.
In this tutorial, I will cover function tools, custom tools, grammar constraints, tool allowlists, and preambles, with code examples and clear explanations of how they work and when to use them.
Function Calling in GPT-5: What & Why
Function Calling in GPT-5 allows you to enhance the model with your applicationโs data and actions. The model can determine when to call a tool, while you handle the logic and return results for a final, grounded response.ย
Types of Function Calling:
Function Tools (JSON Schema): These offer structured inputs and outputs that the model can call precisely.
Custom Tools (Free-Form): These provide flexible text inputs and outputs for unstructured integrations.
What is function calling?
GPT-5 supports both structured function tools (using JSON Schema) and custom tools that accept free-form text payloads (such as SQL, scripts, or configurations) for seamless integration with external runtimes.ย
Function tools are defined by JSON Schema, ensuring the model understands exactly what arguments to pass and allowing for strict input validation. You can read more in our OpenAI Function Calling tutorial.
Why use function calling?
Flexibility: Free-form tools enable the model to produce the exact text your system requires, without the constraints of rigid JSON, making them ideal for code, queries, or configurations.
Reliability and Control: Function tools enforce structured arguments, which improves predictability and reduces parsing errors.
Agentic Workflows: GPT-5 is designed for complex, multi-step tool usage and coding tasks, making tool calling a primary feature.
How does function calling work?
Send a prompt that includes all the available tools.
The model issues a tool call (either with arguments or free-form text).
Your application executes the tool (run Python function).
Return the output from the tool to the model.
The model then responds or may call additional tools.
Use function tools when you want strict validation and predictable, typed arguments through JSON Schema.
Use custom tools (free-form) when your runtime requires raw text (like scripts, SQL, or configurations) or when you need fast iterations without schemas.
Function Tools (JSON Schema): Send Structured Data
Using Function Tools with JSON Schema enables you to obtain predictable and structured results from the model. The model evaluates the tools you have declared, determines when to use one, proposes JSON-validated arguments, and then executes the corresponding function in your code.ย
After that, you can ask the model to generate a response based solely on the data returned by the tool, ensuring that the output is grounded, machine-readable, and easy to integrate.
This example illustrates several key concepts:
Tool discovery and argument generation: The model selects the appropriate tool and automatically fills in its JSON-typed arguments.
Tool execution loop: Your application executes the requested tool(s) and sends the results back to the model.
Schema-shaped final answer: The final response is based strictly on the outputs from the tools, which minimizes inaccuracies and ensures reliability.
1. Setting up
Create an OpenAI account and generate an API key on the platform dashboard. On the dashboard, open the API section, then โView API keys,โ and click โCreate new secret keyโ.
Add a payment method or credits to your account so API calls can run; usage deducts from your prepaid balance as you make requests.
Save your API key as an environment variable named OPENAI_API_KEY before starting your app.ย
Install the official OpenAI Python SDK with pip install openai.
2. Define tools
You provide a list of tools with a name, description, and JSON Schema for parameters. The schema helps the model supply well-formed arguments.
make_coffee expects one string parameter: coffee_type.
random_coffee_fact takes no parameters (empty object). These definitions are passed via tools argument in the API call so the model knows what is available.
import os
from openai import OpenAI
import json
client = OpenAI(api_key = os.environ["OPENAI_API_KEY"])
tools =[{"type":"function","name":"make_coffee","description":"Gives a simple recipe for making a coffee drink.","parameters":{"type":"object","properties":{"coffee_type":{"type":"string","description":"The coffee drink, e.g. espresso, cappuccino, latte"}},"required":["coffee_type"],},},{"type":"function","name":"random_coffee_fact","description":"Returns a fun fact about coffee.","parameters":{"type":"object","properties":{}}}]
You implement the Python functions that actually do the work:
make_coffee(coffee_type) returns a concise recipe string keyed by the requested drink.
random_coffee_fact() returns a small fact payload.ย
Both return JSON-serializable dicts, ideal for feeding back to the model.
defmake_coffee(coffee_type):
recipes ={"espresso":"Grind fine, 18g coffee โ 36g espresso in ~28s.","cappuccino":"Brew 1 espresso shot, steam 150ml milk, pour and top with foam.","latte":"Brew 1 espresso shot, steam 250ml milk, pour for silky texture.",}return{"coffee_type": coffee_type,"recipe": recipes.get(coffee_type.lower(),"Unknown coffee type!")}defrandom_coffee_fact():return{"fact":"Coffee is the second most traded commodity in the world, after oil."}์ ๊ณต
4. Start a conversation
Initiate the conversation with the user's question: {"role": "user", "content": "How do I make a latte?"}
The model might return messages that include a function_call item, such as make_coffee with {"coffee_type":"latte"}. You should then append the model's messages back into input_list: input_list += response.output. This helps maintain the conversation state for the next turn.
# Track used tools
used_tools =[]
input_list =[{"role":"user","content":"How do I make a latte?"}]
response = client.responses.create(
model="gpt-5",
tools=tools,input=input_list,)
input_list += response.output
You should iterate over response.output and identify items where item.type is equal to "function_call". For these items, parse item.arguments and dispatch the requests to your Python functions.ย
For the make_coffee function, call it using make_coffee(args["coffee_type"]). For the random_coffee_fact function, simply call random_coffee_fact().ย
After executing the functions, append a function_call_output message that includes the following:ย
call_id (which links this output to the modelโs request)ย
output (a JSON string of your function result).
for item in response.output:ifgetattr(item,"type","")=="function_call":
used_tools.append(item.name)
args = json.loads(item.arguments or"{}")if item.name =="make_coffee":
result = make_coffee(args["coffee_type"])elif item.name =="random_coffee_fact":
result = random_coffee_fact()else:
result ={"error":f"Unknown tool {item.name}"}
input_list.append({"type":"function_call_output","call_id": item.call_id,"output": json.dumps(result)})
You make a second client.responses.create call, passing the updated input_list that now includes the tool outputs.
The used_tools variable captures the names of tools that appeared in the model's function calls. We will use it to display which tools were used.ย
final = client.responses.create(
model="gpt-5",
tools=tools,input=input_list,
instructions="Answer using only the tool results.")print("Final output:\n", final.output_text)print("\n--- Tool Usage ---")for t in tools:
status ="USED โ "if t["name"]in used_tools else"NOT USED โ"print(f"{t['name']}: {status}")
Because the user asked about a latte, the model selected make_coffee with coffee_type = "latte", your code executed it, and the final response was built only from that tool result.
Final output:{"coffee_type":"latte","recipe":"Brew 1 espresso shot, steam 250ml milk, pour for silky texture."}--- Tool Usage ---
make_coffee: USED โ
random_coffee_fact: NOT USED โ
Use GPT-5's free-form custom tools to allow the model to send raw text directly to your tool, such as code, SQL, shell commands, or simple CSV, without requiring JSON Schema.ย
This capability goes beyond the older JSON-only function calls, providing you with greater flexibility for integration with executors, query engines, or domain-specific language (DSL) interpreters.ย
In free-form mode, the model generates a custom tool call with an unstructured text payload that you can route to your runtime. You then return the result for the model to use in finalizing the user-facing answer.
We will now write the code that enables the model to select the appropriate tools and return unstructured raw text as the output. This output will then be passed to a Python function to generate a result. This way, the user can provide the ingredients they have, and the model will generate recipes that are tailored to their needs.
In this example code, we have:
Initialized the OpenAI client and defined a single custom tool called meal_planner that accepts raw text input.
Created the Python plan_meal that takes a comma-separated string of ingredients and returns a meal idea.
Started the conversation with a user message listing the available ingredients.
Made an initial model call instructing GPT-5 to invoke the tool using only a clean list of ingredients, and appended the modelโs output to the conversation history.
Located the emitted custom tool call and extracted the raw comma-separated input string.
Executed the tool by passing the list to plan_meal, then sent the result back to the model as a function_call_output linked via call_id.
Performed a final model call to convert the toolโs raw idea into a concise, step-by-step recipe, and printed both the tool call details and the final output.
from openai import OpenAI
import json
import random
client = OpenAI()# --- Custom tool ---
tools =[{"type":"custom","name":"meal_planner","description":"Takes ONLY a comma-separated list of ingredients and suggests a meal idea."}]# --- Fake meal planner ---defplan_meal(ingredients:str)->str:
ideas =[f"Stir-fry: {ingredients} with garlic & soy sauce.",f"One-pot rice: cook {ingredients} together in broth until fluffy.",f"Soup: simmer {ingredients} in stock with herbs.",f"Sheet-pan bake: roast {ingredients} at 200ยฐC for ~20 min."]return random.choice(ideas)# --- Start conversation ---
messages =[{"role":"user","content":"I only have chicken, rice, and broccoli. Any dinner ideas?"}]# 1) Ask model with clear instruction
resp = client.responses.create(
model="gpt-5",
tools=tools,input=messages,
instructions="If the user mentions ingredients, call the meal_planner tool with ONLY a comma-separated list like 'chicken, rice, broccoli'.")
messages += resp.output
# 2) Find the tool call
tool_call =next((x for x in resp.output ifgetattr(x,"type","")=="custom_tool_call"),None)assert tool_call,"No tool call found!"# Get clean CSV input
ingredients_csv = tool_call.input.strip()# 3) Run the tool
meal_result = plan_meal(ingredients_csv)# 4) Send tool output back
messages.append({"type":"function_call_output","call_id": tool_call.call_id,"output": meal_result
})# 5) Final model response
final = client.responses.create(
model="gpt-5",
tools=tools,input=messages,
instructions="Turn the meal idea into a short recipe with 3-4 steps.")print("\n--- Tool Call ---")print("Name:", tool_call.name)print("Input:", ingredients_csv)print("Output:", meal_result)print("\n--- Final Output ---\n", final.output_text)์ ๊ณต
As a result, we received stats on the tools used, including installation, input, and output. Additionally, the final output provides a complete recipe on how to create the meal.
--- Tool Call ---
Name: meal_planner
Input: chicken, rice, broccoli
Output: One-pot rice: cook chicken, rice, broccoli together in broth until fluffy.
--- Final Output ---
One-Pot Chicken, Rice & Broccoli
- Season bite-size chicken pieces with salt and pepper; sear in a little oil in a pot until lightly browned.
- Add 1 cup rinsed rice and 2 cups broth (or water + salt). Bring to a boil, then cover and simmer on low for12 minutes.
- Scatter 2 cups small broccoli florets on top, cover, and cook 5-7 more minutes until rice is fluffy and broccoli is tender.
- Rest 5 minutes off heat, fluff, and adjust seasoning. Optional: stir in a knob of butter or a splash of soy.
GPT-5 now supports context-free grammar (CFG) to ensure that output formats are strictly controlled.ย
By applying a grammar, such as SQL or a domain-specific language (DSL), to its responses, GPT-5 guarantees that the outputs consistently follow the required structure. CFG is especially important for automated processes and high-stakes workflows.ย
GPT-5 can use grammars defined in formats like Lark to restrict the tool's outputs during generation. This approach to constrained decoding enhances reliability by preventing format drift and ensuring structural consistency.
We will now write the code where the model selects a tool and outputs ONLY a grammar-valid arithmetic expression; this expression is then evaluated in Python, the result is returned to the conversation, and the final response is presented clearly in natural language.
In this example code, we have:
Initialized the OpenAI client and defined a custom tool math_solver that must output ONLY a grammar-valid arithmetic expression (Lark CFG).
Started the conversation with a natural-language math question from the user.
Made a first model call instructing GPT-5 to call math_solver with an expression that conforms to the grammar.
Located the emitted tool call and extracted the raw expression string from the tool payload.
Evaluated the expression locally in a restricted environment to produce a concrete result.
Returned the tool execution result back to the conversation, linked via the original call_id.
Performed a final model call to present the evaluated result clearly in natural language.
from openai import OpenAI
import json
client = OpenAI()# --- Local fake evaluator ---defeval_expression(expr:str)->str:try:# Evaluate safely using Python's eval on restricted globals
result =eval(expr,{"__builtins__":{}},{})returnf"{expr} = {result}"except Exception as e:returnf"Error evaluating expression: {e}"# --- Custom tool with grammar constraint ---
tools =[{"type":"custom","name":"math_solver","description":"Solve a math problem by outputting ONLY a valid arithmetic expression.","format":{"type":"grammar","syntax":"lark","definition":r"""
start: expr
?expr: term
| expr "+" term -> add
| expr "-" term -> sub
?term: factor
| term "*" factor -> mul
| term "/" factor -> div
?factor: NUMBER
| "(" expr ")"
%import common.NUMBER
%ignore " "
"""},}]# --- User asks ---
msgs =[{"role":"user","content":"What is (12 + 8) * 3 minus 5 divided by 5?"}]# 1) First model call: GPT-5 must emit a grammar-valid expression
resp = client.responses.create(
model="gpt-5",
tools=tools,input=msgs,
instructions="Always call the math_solver tool with a grammar-valid arithmetic expression like '(12 + 8) * 3 - 5 / 5'.")
msgs += resp.output
tool_call =next(
x for x in resp.output
ifgetattr(x,"type","")in("custom_tool_call","tool_call","function_call"))
expr =getattr(tool_call,"input","")orgetattr(tool_call,"arguments","")print("\n=== Grammar-Constrained Expression ===")print(expr)# 2) Run the local evaluator
tool_result = eval_expression(expr)print("\n=== Tool Execution Result ===")print(tool_result)# 3) Return tool output
msgs.append({"type":"function_call_output","call_id": tool_call.call_id,"output": tool_result
})# 4) Final pass: GPT-5 presents answer nicely
final = client.responses.create(
model="gpt-5",input=msgs,
tools=tools,
instructions="Present the evaluated result clearly in natural language.")print("\n=== Final Output ===")print(final.output_text)
As you can see, the model has selected the tool and then generated a grammar-constrained expression. After that, it is passed through the Python function to generate the answer. In the end, we have a proper answer in natural language.
Tool Allowlists: Safely Limit What the Model Can Use
The allowed_tools setting allows you to restrict the model to a safe selection of tools from your full toolkit. This enhances predictability and prevents unintended tool calls, while still providing the model with flexibility within the allowed set.ย
You can let the model choose among allowed tools or require it to use one, mirroring โforce toolโ behaviors seen in other stacks, and aligning with general best practices for tool calling orchestration.
We will now write the code where we allow a single tool, have the model call only that tool, run it, feed the results back, and then produce a concise user-facing summary.
Note: Please create a free Firecrawl account, generate an API key, save it as an environment variable FIRECRAWL_API_KEY, and install the Firecrawl Python SDK using pip install firecrawl-py.
In this example code, we have:
Defined a full toolset with both dummy_web_search and firecrawl_search.
Restrained the model to firecrawl_search only via tool_choice = { type: "allowed_tools", mode: "required", tools: [...] }.
Issued the first model call, captured the tool call name and arguments, and printed them for visibility.
When firecrawl_search is invoked, deserialized arguments, executed Firecrawl, converted its response into a JSON-serializable dict, and extracted the safe data payload.
Fed the tool results back to the conversation using function_call_output linked by call_id, then made a final model call to summarize into 3โ4 bullets with Markdown links.
Included a fallback branch that would run the dummy tool (though the allowlist prevents it in this run).
Printed the final natural-language answer.
from openai import OpenAI
from firecrawl import Firecrawl
import json
import os
client = OpenAI()
firecrawl = Firecrawl(api_key=os.environ["FIRECRAWL_API_KEY"])# --- Full toolset ---
tools =[{"type":"function","name":"dummy_web_search","description":"A fake web search that always returns static dummy results.","parameters":{"type":"object","properties":{"query":{"type":"string"},"limit":{"type":"integer"}},"required":["query"]},},{"type":"function","name":"firecrawl_search","description":"Perform a real web search using Firecrawl.","parameters":{"type":"object","properties":{"query":{"type":"string"},"limit":{"type":"integer"}},"required":["query"]},},]# --- Dummy implementation ---defdummy_web_search(query, limit=3):return{"query": query,"results":[f"[Dummy] Result {i+1} for '{query}'"for i inrange(limit)]}# --- User asks ---
messages =[{"role":"user","content":"Find me the latest info about the stock market."}]# --- Restrict to Firecrawl only ---
resp = client.responses.create(
model="gpt-5",
tools=tools,input=messages,
instructions="Only the firecrawl_search tool is allowed. Do not use dummy_web_search.",
tool_choice={"type":"allowed_tools","mode":"required",# force Firecrawl"tools":[{"type":"function","name":"firecrawl_search"}],},)for item in resp.output:ifgetattr(item,"type","")in("function_call","tool_call"):print("\n--- Tool Call ---")print("Tool name:", item.name)print("Arguments:", item.arguments)if item.name =="firecrawl_search":
args = json.loads(item.arguments)# Firecrawl returns a SearchData object โ convert it
results_obj = firecrawl.search(
query=args["query"],
limit=args.get("limit",3),)# ๐ฅ Convert to JSON-serializable dictifhasattr(results_obj,"to_dict"):
results = results_obj.to_dict()elifhasattr(results_obj,"dict"):
results = results_obj.dict()else:
results = json.loads(results_obj.json())ifhasattr(results_obj,"json")else results_obj
print("\n--- Firecrawl Raw Results ---")print(json.dumps(results, indent=2)[:500])# โ Extract only the data portion for summarization
safe_results = results.get("data", results)# Feed back to GPT
messages += resp.output
messages.append({"type":"function_call_output","call_id": item.call_id,"output": json.dumps(safe_results)# now safe})# Final pass
final = client.responses.create(
model="gpt-5",
tools=tools,input=messages,
instructions="Summarize the Firecrawl search results into 3-4 bullet points with clickable [Title](URL) links.")print("\n--- Final Natural Language Answer ---")print(final.output_text)elif item.name =="dummy_web_search":
args = json.loads(item.arguments)
results = dummy_web_search(args["query"], args.get("limit",3))print("\n--- Dummy Web Search Results ---")print(json.dumps(results, indent=2))์ ๊ณต
It has selected the Firecrawl search, and using that, it has generated the final response in markdown format.
Preambles: Tell the Model How to Call Tools
Preambles are brief explanations that the model generates before invoking a tool, clarifying the reason for the call. They enhance transparency, build user trust, and improve debugging clarity in complex workflows. You can enable them by using a simple instruction like โexplain before calling a tool,โ typically without adding noticeable latency.ย
We will now write the code where the model first generates a brief โPreamble:โ line explaining why itโs calling a tool, then calls the tool, you execute it locally, return the result, and the model produces a clear final answer.
In this example code, we have:
Initialized the OpenAI client and defined a strict-typed function tool (medical_advice) with JSON Schema.
Added a system instruction telling the model to output a one-line โPreamble:โ before any tool call.
Initialize the conversation with a userโs symptom question.
Made the first model call to obtain both the preamble and the tool call with its arguments.
Extracted and executed the tool locally to generate advice.
Returned the toolโs output back to the model, linked via call_id.
Performed a final model call so GPTโ5 presents the result as a concise, user-friendly answer.
from openai import OpenAI
import json
import random
client = OpenAI()# --- Fake tool implementation ---defmedical_advice(symptom:str):
remedies ={"headache":"You can take acetaminophen (paracetamol) or ibuprofen, rest in a quiet room, and stay hydrated.","cough":"Drink warm fluids, use honey in tea, and consider over-the-counter cough syrup.","fever":"Use acetaminophen to reduce fever, stay hydrated, and rest. See a doctor if >39ยฐC.",}return{"advice": remedies.get(symptom.lower(),"Please consult a healthcare provider for guidance.")}# --- Tool definition ---
tools =[{"type":"function","name":"medical_advice","description":"Provide safe, general over-the-counter advice for common symptoms.","parameters":{"type":"object","properties":{"symptom":{"type":"string"}},"required":["symptom"],"additionalProperties":False},"strict":True,}]# --- Messages (system preamble instruction) ---
messages =[{"role":"system","content":"Before you call a tool, explain why you are calling it in ONE short sentence prefixed with 'Preamble:'."},{"role":"user","content":"What should I take for a headache?"}]# 1) First call: expect preamble + tool call
resp = client.responses.create(model="gpt-5",input=messages, tools=tools)print("=== First Response ===")for item in resp.output:
t =getattr(item,"type",None)if t =="message":# Extract just the model's text
content =getattr(item,"content",None)
text =Noneifisinstance(content,list):
text ="".join([c.text for c in content ifhasattr(c,"text")])elif content:
text =str(content)if text:# โ Only print onceprint(text)if t in("function_call","tool_call","custom_tool_call"):print("Tool:",getattr(item,"name",None))print("Args:",getattr(item,"arguments",None))# Extract tool call
tool_call =next(x for x in resp.output ifgetattr(x,"type",None)in("function_call","tool_call","custom_tool_call"))
messages += resp.output
# 2) Execute tool locally
args = json.loads(getattr(tool_call,"arguments","{}"))
symptom = args.get("symptom","")
tool_result = medical_advice(symptom)# 3) Return tool result
messages.append({"type":"function_call_output","call_id": tool_call.call_id,"output": json.dumps(tool_result)})# 4) Final model call โ natural answer
final = client.responses.create(model="gpt-5",input=messages, tools=tools)print("\n=== Final Answer ===")print(final.output_text)์ ๊ณต
You can see that before calling the tool, it provides an explanation of why it is calling the tool. This is good for observability and debugging.
Summary
GPT-5 is OpenAIโs most advanced model to date, particularly excelling in coding and agent-based workflows. Its latest API features allow for the straightforward development of production-grade systems from start to finish.
In this tutorial, you learned how to:
Return structured outputs using Function Tools and JSON Schema for reliable downstream automation.
Enable free-form execution with Custom Tools (raw-text), allowing the model to generate code, SQL, shell scripts, and CSV files beyond just JSON calls.
Restricted formats using Lark grammar constraints when precision is crucial, such as in mathematics, SQL, or domain-specific languages (DSLs).
Constrain tool access with allowlists (or by enforcing a single tool) to enhance safety, control, and determinism.
Improve transparency with brief preambles, so the model explains why it is calling a specific tool, aiding observability and debugging.
As a certified data scientist, I am passionate about leveraging cutting-edge technology to create innovative machine learning applications. With a strong background in speech recognition, data analysis and reporting, MLOps, conversational AI, and NLP, I have honed my skills in developing intelligent systems that can make a real impact. In addition to my technical expertise, I am also a skilled communicator with a talent for distilling complex concepts into clear and concise language. As a result, I have become a sought-after blogger on data science, sharing my insights and experiences with a growing community of fellow data professionals. Currently, I am focusing on content creation and editing, working with large language models to develop powerful and engaging content that can help businesses and individuals alike make the most of their data.
Discover how GPT-5.2 improves knowledge work with major upgrades in long-context reasoning, tool calling, coding, vision, and end-to-end workflow execution.
Learn how OpenAI's new Function Calling capability enables GPT models to generate structured JSON output, resolving common dev issues caused by irregular outputs.
Explore the latest OpenAI GPT-5 API features with code examples, including reasoning effort, verbosity control, chain-of-thought handoff, freeform input, output constraints, allowed tools, preambles, prompt optimization, and more.
This tutorial will introduce you to everything you need to know about GPT-4 Vision, from accessing it to, going hands-on into real-world examples, and the limitations of it.
With the launch of GPT-5, OpenAI expanded tool/function calling in the API to include free-form tool calls (raw text, no JSON), grammar constraints via Lark/CFG, tool allowlists, and improved tool-use reasoning. Together, these make GPT-5 a truly agentic model; you can connect APIs, databases, and custom tools to the Responses API and generate grounded answers or even automate workflows.
In this tutorial, I will cover function tools, custom tools, grammar constraints, tool allowlists, and preambles, with code examples and clear explanations of how they work and when to use them.
Function Calling in GPT-5: What & Why
Function Calling in GPT-5 allows you to enhance the model with your applicationโs data and actions. The model can determine when to call a tool, while you handle the logic and return results for a final, grounded response.ย
Types of Function Calling:
What is function calling?
GPT-5 supports both structured function tools (using JSON Schema) and custom tools that accept free-form text payloads (such as SQL, scripts, or configurations) for seamless integration with external runtimes.ย
Function tools are defined by JSON Schema, ensuring the model understands exactly what arguments to pass and allowing for strict input validation. You can read more in our OpenAI Function Calling tutorial.
Why use function calling?
How does function calling work?
Source: Function calling - OpenAI API
When to use Function vs. Custom Tools
Function Tools (JSON Schema): Send Structured Data
Using Function Tools with JSON Schema enables you to obtain predictable and structured results from the model. The model evaluates the tools you have declared, determines when to use one, proposes JSON-validated arguments, and then executes the corresponding function in your code.ย
After that, you can ask the model to generate a response based solely on the data returned by the tool, ensuring that the output is grounded, machine-readable, and easy to integrate.
This example illustrates several key concepts:
1. Setting up
pip install openai.2. Define tools
You provide a list of tools with a name, description, and JSON Schema for parameters. The schema helps the model supply well-formed arguments.
make_coffeeexpects one string parameter:coffee_type.random_coffee_facttakes no parameters (empty object). These definitions are passed viatoolsargument in the API call so the model knows what is available.3. Implement tools
You implement the Python functions that actually do the work:
make_coffee(coffee_type)returns a concise recipe string keyed by the requested drink.random_coffee_fact()returns a small fact payload.ยBoth return JSON-serializable dicts, ideal for feeding back to the model.
4. Start a conversation
Initiate the conversation with the user's question: {
"role": "user", "content": "How do I make a latte?"}The model might return messages that include a
function_callitem, such asmake_coffeewith {"coffee_type":"latte"}. You should then append the model's messages back intoinput_list: input_list += response.output. This helps maintain the conversation state for the next turn.5. Execute tool calls and attach outputs
You should iterate over
response.outputand identify items whereitem.typeis equal to"function_call". For these items, parseitem.argumentsand dispatch the requests to your Python functions.ยFor the
make_coffeefunction, call it usingmake_coffee(args["coffee_type"]). For therandom_coffee_factfunction, simply callrandom_coffee_fact().ยAfter executing the functions, append a
function_call_outputmessage that includes the following:ยcall_id(which links this output to the modelโs request)ยoutput(a JSON string of your function result).6. Generate the final answer
You make a second
client.responses.createcall, passing the updatedinput_listthat now includes the tool outputs.The
used_toolsvariable captures the names of tools that appeared in the model's function calls. We will use it to display which tools were used.ยBecause the user asked about a latte, the model selected
make_coffeewithcoffee_type = "latte", your code executed it, and the final response was built only from that tool result.Custom Tools (Free-Form): Send Raw Text
Use GPT-5's free-form custom tools to allow the model to send raw text directly to your tool, such as code, SQL, shell commands, or simple CSV, without requiring JSON Schema.ย
This capability goes beyond the older JSON-only function calls, providing you with greater flexibility for integration with executors, query engines, or domain-specific language (DSL) interpreters.ย
In free-form mode, the model generates a custom tool call with an unstructured text payload that you can route to your runtime. You then return the result for the model to use in finalizing the user-facing answer.
We will now write the code that enables the model to select the appropriate tools and return unstructured raw text as the output. This output will then be passed to a Python function to generate a result. This way, the user can provide the ingredients they have, and the model will generate recipes that are tailored to their needs.
In this example code, we have:
meal_plannerthat accepts raw text input.plan_mealthat takes a comma-separated string of ingredients and returns a meal idea.plan_meal, then sent the result back to the model as afunction_call_outputlinked viacall_id.As a result, we received stats on the tools used, including installation, input, and output. Additionally, the final output provides a complete recipe on how to create the meal.
Grammar Constraints (Lark): Constrain Tool Outputs
GPT-5 now supports context-free grammar (CFG) to ensure that output formats are strictly controlled.ย
By applying a grammar, such as SQL or a domain-specific language (DSL), to its responses, GPT-5 guarantees that the outputs consistently follow the required structure. CFG is especially important for automated processes and high-stakes workflows.ย
GPT-5 can use grammars defined in formats like Lark to restrict the tool's outputs during generation. This approach to constrained decoding enhances reliability by preventing format drift and ensuring structural consistency.
We will now write the code where the model selects a tool and outputs ONLY a grammar-valid arithmetic expression; this expression is then evaluated in Python, the result is returned to the conversation, and the final response is presented clearly in natural language.
In this example code, we have:
math_solverthat must output ONLY a grammar-valid arithmetic expression (Lark CFG).math_solverwith an expression that conforms to the grammar.call_id.As you can see, the model has selected the tool and then generated a grammar-constrained expression. After that, it is passed through the Python function to generate the answer. In the end, we have a proper answer in natural language.
Tool Allowlists: Safely Limit What the Model Can Use
The
allowed_toolssetting allows you to restrict the model to a safe selection of tools from your full toolkit. This enhances predictability and prevents unintended tool calls, while still providing the model with flexibility within the allowed set.ยYou can let the model choose among allowed tools or require it to use one, mirroring โforce toolโ behaviors seen in other stacks, and aligning with general best practices for tool calling orchestration.
We will now write the code where we allow a single tool, have the model call only that tool, run it, feed the results back, and then produce a concise user-facing summary.
Note: Please create a free Firecrawl account, generate an API key, save it as an environment variable
FIRECRAWL_API_KEY, and install the Firecrawl Python SDK usingpip install firecrawl-py.In this example code, we have:
dummy_web_searchandfirecrawl_search.firecrawl_searchonly viatool_choice = { type: "allowed_tools", mode: "required", tools: [...] }.firecrawl_searchis invoked, deserialized arguments, executed Firecrawl, converted its response into a JSON-serializable dict, and extracted the safe data payload.function_call_outputlinked bycall_id, then made a final model call to summarize into 3โ4 bullets with Markdown links.It has selected the Firecrawl search, and using that, it has generated the final response in markdown format.
Preambles: Tell the Model How to Call Tools
Preambles are brief explanations that the model generates before invoking a tool, clarifying the reason for the call. They enhance transparency, build user trust, and improve debugging clarity in complex workflows. You can enable them by using a simple instruction like โexplain before calling a tool,โ typically without adding noticeable latency.ย
We will now write the code where the model first generates a brief โPreamble:โ line explaining why itโs calling a tool, then calls the tool, you execute it locally, return the result, and the model produces a clear final answer.
In this example code, we have:
medical_advice) with JSON Schema.โPreamble:โbefore any tool call.call_id.You can see that before calling the tool, it provides an explanation of why it is calling the tool. This is good for observability and debugging.
Summary
GPT-5 is OpenAIโs most advanced model to date, particularly excelling in coding and agent-based workflows. Its latest API features allow for the straightforward development of production-grade systems from start to finish.
In this tutorial, you learned how to:
To learn more about what you can do with OpenAIโs various tools, I recommend taking our OpenAI Fundamentals skill track and Working with the OpenAI API course.