Structured outputs force the shape of the model’s
text reply; function calling is different — the model asks you to run
code and waits for the result. On the Responses API a tool is a flat object with no nested
function wrapper, and you drive a short loop until the model stops asking.
1. Define a tool with a JSON-schema parameters block
pip install openai
from openai import OpenAI
client = OpenAI() # reads OPENAI_API_KEY
tools = [{
"type": "function",
"name": "get_weather",
"description": "Get the current temperature for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
}]
Each tool carries type, name, description, a JSON-schema parameters, and strict right on the object, with no nested function key in the Responses API.
2. Send it and read the function_call item
def get_weather(city):
return {"city": city, "temp_c": 18}
input_list = [{"role": "user", "content": "What's the weather in Paris?"}]
response = client.responses.create(model="gpt-6-astra", input=input_list, tools=tools)
The model answers with one or more function_call items in response.output, each holding a name, a call_id, and an arguments JSON string.
3. Run your function and return a function_call_output
import json
input_list += response.output
for item in response.output:
if item.type == "function_call":
result = get_weather(**json.loads(item.arguments))
input_list.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result),
})
Parse arguments, call your function, then append a function_call_output whose call_id matches the call and whose output is a string.
4. Loop until the model stops calling tools
while True:
response = client.responses.create(model="gpt-6-astra", input=input_list, tools=tools)
calls = [i for i in response.output if i.type == "function_call"]
if not calls:
break
input_list += response.output
for item in calls:
result = get_weather(**json.loads(item.arguments))
input_list.append({"type": "function_call_output",
"call_id": item.call_id, "output": json.dumps(result)})
print(response.output_text)
Resend the growing input after every batch of outputs until a turn returns no function_call items, then read the final text from output_text.
5. Handle parallel tool calls
response = client.responses.create(
model="gpt-6-astra",
input=input_list,
tools=tools,
tool_choice="auto",
parallel_tool_calls=False,
)
A single turn may emit several function_call items at once; tool_choice accepts "auto", "required", "none" or {"type": "function", "name": "get_weather"}, and parallel_tool_calls=False caps it at one call per turn.
Verify it worked
A correct run prints a sentence containing the number your function returned, proving the model consumed the tool result rather than guessing.
If it fails:
No tool output found for function call call_...— everyfunction_callneeds a matchingfunction_call_outputwith the samecall_idbefore the next request.TypeError: argument of type ...or aJSONDecodeError—argumentsis a JSON string, sojson.loadsit instead of treating it as a dict.- The model answers without calling the tool — sharpen the
description, or force it withtool_choice={"type": "function", "name": "get_weather"}.
Start here if this is new: your first OpenAI request. The Claude equivalent, with its own loop, is tool use with the Claude API.
Source: OpenAI function calling guide.