Tool use and function calling (OpenAI)
OpenAI’s tool surface in 2026 has two layers: custom functions (you declare a JSON-Schema spec, the model emits a tool_calls[] array, you execute and return results) and hosted tools on the Responses API (web_search_preview, file_search, computer_use_preview, code_interpreter, plus a few others) — Google-style server-managed tools that run on OpenAI’s infrastructure. The biggest 2024-2025 change was strict mode (strict: true on a function) — when set with additionalProperties: false and all fields in required, OpenAI uses constrained decoding to guarantee the model’s arguments match your schema (not just probable, mathematically forced). parallel_tool_calls=True (the default) lets the model emit multiple tool calls in one turn; tool_choice controls routing semantics ("auto", "none", "required", or a specific {type: "function", function: {name}}).
See also
- structured-outputs-openai — sibling feature (JSON schema constraint without invoking a tool)
- openai-api-and-sdks — the
toolsparameter in context - agents-sdk-openai — higher-level abstraction (Agents SDK)
- hidden-tricks-and-gotchas-openai — parallel-tool-call quirks
- tool-use-and-function-calling — sister doc for Claude
- tool-use-and-function-calling-gemini — sister doc for Gemini
1. Function declaration shape
Chat Completions (legacy nested shape)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True, # constrained decoding
},
},
]Responses API (flatter shape)
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
},
"required": ["city"],
"additionalProperties": False,
},
"strict": True,
},
]The schema is JSON Schema, lowercase ("type": "object" not "OBJECT" — unlike Gemini). Supported features:
- Basic types:
string,number,integer,boolean,object,array,null enumdescription,titleproperties,required,additionalPropertiesitems(for arrays),minItems,maxItemsminimum,maximum,patternanyOf(in strict mode, has restrictions)
In strict mode:
additionalProperties: falseis required on every object- All fields must be in
required(no truly-optional fields — make them nullable via["type", "null"]instead) - No recursive
$ref oneOf/allOfnot allowed- Tuple-typed arrays (heterogeneous) not allowed
2. Strict mode mechanics
Per platform.openai.com/docs/guides/function-calling:
When strict: true, the model uses constrained decoding — token sampling at each step is restricted to tokens that keep the output on a valid schema-completion path. The result: emitted arguments are guaranteed to parse against the schema (syntactically), with the format/type constraints satisfied.
Tradeoffs:
- ✅ Eliminates schema validation errors from malformed args
- ✅ Reduces retries
- ⚠️ Slightly higher latency (decoding constraint adds work)
- ⚠️ Strict mode rules limit schema expressiveness
- ⚠️ First request with a new strict schema includes a one-time schema-validation pass
For most production use, strict: true is the right default. Non-strict mode (strict: false or omitted) is for legacy schemas you can’t refactor.
3. The call → execute → respond loop
Chat Completions
messages = [
{"role": "system", "content": "You are a weather assistant."},
{"role": "user", "content": "What's the weather in Paris?"},
]
# Pass 1: model decides to call function
resp = client.chat.completions.create(
model="gpt-5",
messages=messages,
tools=tools,
)
msg = resp.choices[0].message
if msg.tool_calls:
messages.append(msg) # append assistant turn
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = execute_locally(tc.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result),
})
# Pass 2: feed results back
resp = client.chat.completions.create(
model="gpt-5",
messages=messages,
tools=tools,
)
print(resp.choices[0].message.content)Responses API
resp = client.responses.create(
model="gpt-5",
input="What's the weather in Paris?",
tools=tools,
store=True,
)
# Find function calls in the output array
for item in resp.output:
if item.type == "function_call":
args = json.loads(item.arguments)
result = execute_locally(item.name, args)
# Submit tool result via continuation
resp2 = client.responses.create(
model="gpt-5",
previous_response_id=resp.id,
input=[
{
"type": "function_call_output",
"call_id": item.call_id,
"output": json.dumps(result),
}
],
tools=tools,
)
print(resp2.output_text)The Responses API uses function_call_output items rather than role: "tool" messages, and the call_id field rather than tool_call_id. Migrate carefully.
4. tool_choice semantics
| Value | Behavior |
|---|---|
"auto" (default) | Model decides whether to call any tool or respond with text |
"none" | Disable tool calling for this request |
"required" | Model must call at least one tool (cannot reply with text) |
{type: "function", function: {name: "..."}} (Chat Completions) | Force specific function |
{type: "function", name: "..."} (Responses) | Force specific function (flatter shape) |
# Chat Completions
tool_choice={"type": "function", "function": {"name": "classify_intent"}}
# Responses
tool_choice={"type": "function", "name": "classify_intent"}
# For hosted tools (Responses)
tool_choice={"type": "file_search"}"required" is useful for routing / classification — guarantees an action vs a “Sorry, I can’t help” reply.
5. Parallel tool calls
Default parallel_tool_calls=True. The model can return multiple tool calls in one turn when independent:
# Chat Completions response
resp.choices[0].message.tool_calls
# [
# ToolCall(id="call_1", function=Function(name="get_weather", arguments='{"city":"Paris"}')),
# ToolCall(id="call_2", function=Function(name="get_weather", arguments='{"city":"Tokyo"}')),
# ]Execute all in parallel:
import asyncio
results = await asyncio.gather(*[execute(tc) for tc in msg.tool_calls])Then submit results in one combined message (Chat Completions) or one input batch (Responses):
# Chat Completions
for tc, result in zip(msg.tool_calls, results):
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result),
})Disabling parallel
client.chat.completions.create(model="gpt-5", messages=[...], tools=[...],
parallel_tool_calls=False)Forces serial execution. Useful when tool calls have side effects that must be ordered (e.g., create → tag → publish).
6. Hosted tools (Responses API)
Per platform.openai.com/docs/guides/responses. Run on OpenAI infrastructure; you don’t implement them.
web_search_preview
client.responses.create(
model="gpt-5",
input="What's the latest news on SpaceX Starship?",
tools=[{"type": "web_search_preview"}],
)Returns text with inline citations + a web_search_call item in output listing the queries. Per-search billing fee.
file_search
client.responses.create(
model="gpt-5",
input="Find info about Q3 revenue.",
tools=[{"type": "file_search", "vector_store_ids": ["vs_abc123"]}],
)Pre-built RAG using your vector store (uploaded via client.vector_stores.*). Returns file_search_call items with retrieved chunks.
code_interpreter
client.responses.create(
model="gpt-5",
input="Run this CSV through a pandas summary.",
tools=[{"type": "code_interpreter", "container": {"type": "auto"}}],
)Python sandbox (similar to Gemini’s code_execution). Pre-installed scientific Python stack. Per-container-hour billing.
computer_use_preview
client.responses.create(
model="computer-use-preview-2025-XX-XX",
input="Book a flight on this airline website.",
tools=[{
"type": "computer_use_preview",
"display_width": 1024,
"display_height": 768,
"environment": "browser", # or "mac", "windows"
}],
)Returns computer_call items with actions (click, type, scroll, screenshot). Your code executes them on a real environment and feeds back computer_call_output with the resulting screenshot. Premium per-token rate.
Other hosted tools
image_generation— generate images mid-response (via DALL-E-3 / gpt-image-1)local_shell— execute shell commands (Codex / agent use)mcp— connect to a Model Context Protocol server
7. Custom + hosted tools together
tools = [
{"type": "function", "name": "my_db_query", "parameters": {...}, "strict": True},
{"type": "web_search_preview"},
{"type": "file_search", "vector_store_ids": ["vs_abc"]},
]
client.responses.create(model="gpt-5", input="...", tools=tools)Model picks among them. Common pattern: custom function for internal data, file_search for owned docs, web_search_preview for fresh facts.
8. Schema design tips
Make descriptions count
The model uses description to decide when to call. Be specific:
"description": "Search the internal customer database by email or customer_id. "
"Returns name, subscription_plan, and last_login. "
"Use this for any question about specific customers."vs
"description": "Search customers." # too vague, model may not callUse enums for constrained inputs
"status": {"type": "string", "enum": ["active", "paused", "archived"]}Strict mode enforces enums via constrained decoding — the model literally cannot generate an out-of-enum value.
Avoid deeply nested optional fields
Schema complexity hurts both reliability and latency. Flatten where possible. If your function takes a complex nested input, consider multiple simpler functions.
Use nullable instead of optional in strict mode
In strict mode, every field must be in required. For “optional” fields:
"middle_name": {"type": ["string", "null"]}The model emits null when the value isn’t applicable.
9. Streaming tool calls
Tool calls arrive incrementally during streaming. The arguments JSON is built up across multiple chunks.
Chat Completions streaming
stream = client.chat.completions.create(model="gpt-5", messages=[...], tools=[...], stream=True)
tool_call_chunks = {}
for chunk in stream:
delta = chunk.choices[0].delta
if delta.tool_calls:
for tc_delta in delta.tool_calls:
idx = tc_delta.index
tool_call_chunks.setdefault(idx, {"id": "", "name": "", "args": ""})
if tc_delta.id: tool_call_chunks[idx]["id"] = tc_delta.id
if tc_delta.function.name: tool_call_chunks[idx]["name"] = tc_delta.function.name
if tc_delta.function.arguments: tool_call_chunks[idx]["args"] += tc_delta.function.argumentsResponses API streaming
Cleaner event-based model:
stream = client.responses.create(model="gpt-5", input="...", tools=[...], stream=True)
for event in stream:
if event.type == "response.function_call_arguments.delta":
print(event.delta, end="")
elif event.type == "response.function_call.done":
# arguments complete
print(f"\ncall: {event.name} args: {event.arguments}")10. Error handling
| Error | Cause | Fix |
|---|---|---|
MALFORMED_TOOL_CALL | Model emitted invalid JSON | Set strict: true; ensure schema is simpler |
| Schema validation error on submit | strict: true schema violates strict rules (e.g. missing additionalProperties: false) | Add required strict-mode fields |
| Tool call but no matching tool | Function name doesn’t match declared | Verify name matches exactly |
tool_choice: "required" returns text | Bug — should not happen on current models | Open ticket; check model version |
| Tool result not folded into next response | Forgot tool_call_id (CC) or call_id (Responses) | Include the matching id |
11. Cost considerations
Function declarations live in the request — they’re billed as input tokens:
- Each function declaration ≈ 50-200 tokens depending on schema complexity
- 20 tools with verbose descriptions can be 5,000+ tokens of overhead
- Caching applies — declared tools become part of the cacheable prefix; reuse the same tool set to maximize caching
Hosted-tool surcharges:
web_search_preview— per-search feefile_search— vector store storage + per-search feecomputer_use_preview— premium per-token + per-action feescode_interpreter— per-container-hour fee
12. Comparison to other vendors
| Aspect | OpenAI | Claude | Gemini |
|---|---|---|---|
| Schema dialect | JSON Schema (lowercase) | JSON Schema (lowercase) | OpenAPI subset (UPPERCASE) |
| Strict mode | strict: true (full enforcement) | strict_input: true (args only) | mode: VALIDATED (constrained decode) |
| Parallel calls | parallel_tool_calls flag | Default | Default |
| Forced specific tool | tool_choice: {type: "function", name} | tool_choice: {type: "tool", name} | mode: ANY + allowed_function_names |
| Hosted tools | Yes (web, file, computer, code, image) | Yes (web, fetch, code, computer, memory) | Yes (search, code, url_context) |
| Unique call IDs | tool_call_id (CC) / call_id (Responses) | tool_use_id | Required on Gemini 3 only |
| SDK sugar (auto-execute) | Agents SDK | None native | Python SDK only |
13. Common pitfalls
- Forgetting
additionalProperties: false— strict mode requires it on every object. Validation error if missing. - Forgetting to mark fields required — strict mode requires all fields in
required; make truly-optional fields nullable. - Recursive schemas — not supported in strict mode. Flatten.
tool_choice: "required"but only one tool — the model still has the option to not call it (returns refusal). Force specific with{type: "function", name}.parallel_tool_calls=Truewith side-effect tools — model may calldeletebeforeconfirm. Setparallel_tool_calls=Falsefor ordered operations, or design tools so they’re commutative.- Streaming JSON arguments mid-call — must accumulate across chunks; can’t
json.loads()mid-stream. - Migrating CC → Responses changes tool shape — flatter,
call_idinstead oftool_call_id. Test thoroughly. - Hosted tools not in Chat Completions —
web_search_preview,file_search,computer_use_previeware Responses-only. CC code can’t use them without migrating. - Tool description matters more than the name — name should be unique-ish, description is what the model reads to decide. Iterate on descriptions if calling is unreliable.
- No
function_call_outputin CC — Chat Completions usesrole: "tool"messages withtool_call_id; Responses usesfunction_call_outputitems withcall_id.
14. Further reading
- Function calling — canonical reference
- Structured outputs — schema constraint for non-tool responses
- Responses API + tools — hosted tools, conversation state
- Agents SDK —
@function_tooldecorator - openai-python tools examples —
parsing_tools.py,parsing_tools_stream.py