Tool calling
Ship agents that call your services mid-conversation. You define the functions, you run them, and the model uses the results to decide what to do next. That loop lets you build multi-turn agents that gather live information, take actions, and adapt based on outcomes.
This page covers developer-defined tools: functions you describe in tools and execute in your own code. The API also offers two built-in tools that run server-side: web_search (see search grounding) and tool_search, which loads your tools on demand (see Tool search below). You can combine built-in and developer-defined tools in a single request.
How it works
- You send a request with
toolscontaining one or more tool definitions. - The model evaluates the user message and decides whether to use a tool.
- If a tool is needed, the model returns a
tool_callsarray on the assistant message. The array can hold multiple calls, each with a function name and arguments. - You execute each call locally and return one result per call as a
toolmessage. - The model incorporates those results into its next response.
The model does not execute developer-defined tools itself. Your app handles execution for every tool you define in tools. The built-in web_search tool used by search grounding is the exception: it runs server-side and returns results directly to the model.
When to use tool calling
Finding information
Give the model access beyond its training data:
- Real-time data: Weather, stock prices, sports scores, news headlines.
- Calculations: Arithmetic, financial projections, or domain-specific computations.
- Database access: Query your own data for pricing, inventory, or customer records.
- Web search: Search engines or knowledge bases for general information retrieval.
For web search, search grounding is a built-in alternative that requires no custom tool implementation.
Performing actions
Let the model trigger side effects on the user's behalf:
- Sending messages: Emails, notifications, chat messages.
- Triggering jobs: Background workflows, CI/CD pipelines, batch processing.
- Calling APIs: External services, payment processors, third-party integrations.
Defining tools
Define tools in tools using the function or custom type. Each function tool specifies a name, description, and JSON Schema for its parameters. A custom tool takes freeform text input instead of structured JSON parameters — use it when your tool accepts arbitrary text rather than a typed schema.
With the recommended Responses API, these fields sit at the top level of the tool object:
json{"type": "function","name": "get_weather","description": "Return current weather for the given location.","parameters": {"type": "object","properties": {"location": {"type": "string","description": "City name, e.g. 'Seattle'"}},"required": ["location"],"additionalProperties": false}}
Chat Completions uses the same fields but nests them under a function key — see Tool calling with Chat Completions.
Write clear, specific descriptions for the tool and for each parameter. The model uses them to decide when and how to call the tool.
Custom tools
A custom tool is a client-defined, client-executed tool that takes freeform text input instead of structured JSON parameters. Use it when your tool accepts arbitrary text rather than a typed schema. Custom tools are Responses-API-only; sending {"type": "custom", ...} to Chat Completions returns HTTP 400.
A custom tool carries the same name, description, strict, and defer_loading fields as a function tool, but omits parameters:
json{"type": "custom","name": "run_shell","description": "Execute a shell command supplied as freeform text."}
Custom tools follow the same parallel_tool_calls and tool_choice behavior as function tools, and their names are exempt from the single-dot restriction described in Function name rules.
Function name rules
Function tool names must match ^[a-zA-Z0-9_.-]+$ (alphanumeric, underscores, hyphens, and dots) and contain at most one dot. Names with two or more dots return HTTP 400.
If your tooling uses multi-segment namespaced names, common in MCP and Agents SDK integrations, use a namespace tool instead of encoding dots in the function name. The namespace tool groups related functions under a shared prefix: the outer name must contain zero dots, while inner function name values may contain up to one dot. For a full example of defining and calling a namespace tool, see Defer tools and group them into namespaces.
Tool calling with the Responses API
Define tools in the flat format shown in Defining tools. When the model calls a tool, the response output array contains a function_call item with the name, arguments, call_id, and status. status is "in_progress" while streaming and "completed" on the final output_item.done event. Return the result in a follow-up request with previous_response_id and a function_call_output input item referencing call_id.
The example below calls the Responses API, executes the returned function call, and returns the result with previous_response_id.
Python (OpenAI SDK)import osimport jsonfrom openai import OpenAIclient = OpenAI(base_url="https://api.meta.ai/v1",api_key=os.environ["MODEL_API_KEY"],)def get_weather(location: str) -> dict:"""Placeholder -- replace with a real API call."""return {"location": location, "temperature": "15°C", "condition": "Cloudy"}tools = [{"type": "function","name": "get_weather","description": "Return current weather for the given location.","parameters": {"type": "object","properties": {"location": {"type": "string","description": "City name",}},"required": ["location"],},}]# First request -- model requests a tool callresponse = client.responses.create(model="muse-spark-1.3",input="What is the weather in Seattle?",tools=tools,)# Check for function_call items in the outputfor item in response.output:if item.type == "function_call":args = json.loads(item.arguments)result = get_weather(**args)# Second request -- return the tool result with previous_response_idresponse = client.responses.create(model="muse-spark-1.3",input=[{"type": "function_call_output","call_id": item.call_id,"output": json.dumps(result),}],previous_response_id=response.id,tools=tools,)print(response.output_text)# "The current weather in Seattle is 15°C and cloudy."
Key differences from chat completion tool calling:
- Flat tool definitions:
name,description, andparameterssit at the top level, not nested underfunction. function_calloutput items: instead oftool_callson the assistant message, the Responses API returnsfunction_callitems in theoutputarray.function_call_outputinput items: instead of appending atoolmessage, you send afunction_call_outputitem withcall_idand the result.previous_response_idfor context: you don't rebuild message history. The server reconstructs the conversation from the prior response.
Tool calling with Chat Completions
If your application or agent harness is built on Chat Completions, tool calling is fully supported there too — use the pattern below. Chat Completions uses the nested tool definition format (function metadata under a function key, as shown in Defining tools). The model returns tool calls as a tool_calls array on the assistant message, and you return each result as a tool message.
Single-turn tool call
Send a request with tools. When the model needs a tool, it returns a tool_calls array on the assistant message.
pythonimport osfrom openai import OpenAIclient = OpenAI(base_url="https://api.meta.ai/v1",api_key=os.environ["MODEL_API_KEY"],)response = client.chat.completions.create(model="muse-spark-1.3",messages=[{"role": "user","content": "What is the weather in Seattle?",},],tools=[{"type": "function","function": {"name": "get_weather","description": "Return current weather for the given location.","parameters": {"type": "object","properties": {"location": {"type": "string","description": "City name",},},"required": ["location",],},},},],)print(response.model_dump_json(indent=2))
typescriptimport OpenAI from 'openai';const apiKey = process.env.MODEL_API_KEY;if (!apiKey) {throw new Error('MODEL_API_KEY is not set');}const client = new OpenAI({baseURL: 'https://api.meta.ai/v1',apiKey,});const response = await client.chat.completions.create({model: 'muse-spark-1.3',messages: [{role: 'user',content: 'What is the weather in Seattle?',},],tools: [{type: 'function',function: {name: 'get_weather',description: 'Return current weather for the given location.',parameters: {type: 'object',properties: {location: {type: 'string',description: 'City name',},},required: ['location',],},},},],});console.log(JSON.stringify(response, null, 2));
pythonimport jsonimport osimport requestsresponse = requests.post("https://api.meta.ai/v1/chat/completions",headers={"Authorization": f"Bearer {os.environ['MODEL_API_KEY']}","Content-Type": "application/json",},json={"model": "muse-spark-1.3","messages": [{"role": "user","content": "What is the weather in Seattle?",},],"tools": [{"type": "function","function": {"name": "get_weather","description": "Return current weather for the given location.","parameters": {"type": "object","properties": {"location": {"type": "string","description": "City name",},},"required": ["location",],},},},],},)response.raise_for_status()print(json.dumps(response.json(), indent=2))
shellcurl -X POST "https://api.meta.ai/v1/chat/completions" \-H "Authorization: Bearer $MODEL_API_KEY" \-H "Content-Type: application/json" \-d '{"model": "muse-spark-1.3","messages": [{"role": "user","content": "What is the weather in Seattle?"}],"tools": [{"type": "function","function": {"name": "get_weather","description": "Return current weather for the given location.","parameters": {"type": "object","properties": {"location": {"type": "string","description": "City name"}},"required": ["location"]}}}]}'
Example response
json{"id": "chatcmpl-abc123","object": "chat.completion","created": 1714502400,"model": "muse-spark-1.3","choices": [{"index": 0,"message": {"role": "assistant","content": null,"tool_calls": [{"id": "call_abc123","type": "function","function": {"name": "get_weather","arguments": "{\"location\": \"Seattle, WA\"}"}}]},"finish_reason": "tool_calls"}],"usage": {"prompt_tokens": 85,"completion_tokens": 24,"total_tokens": 109}}
When the model requests tool calls, finish_reason is "tool_calls" and message.content is typically null or empty. The array can contain multiple calls in one turn; execute each one and return a tool result per call as shown in multi-turn tool results.
Multi-turn tool results
After executing the tool, send the result back so the model can produce a final answer. This is the core pattern for agentic tool use.
The steps:
- Append the full assistant message (including
tool_calls) to the history. - Append a
toolmessage withtool_call_idand the result ascontent. - Send the updated history in the next request.
Python (OpenAI SDK)import osimport jsonfrom openai import OpenAIclient = OpenAI(base_url="https://api.meta.ai/v1",api_key=os.environ["MODEL_API_KEY"],)def get_weather(location: str) -> dict:"""Placeholder -- replace with a real API call."""return {"location": location, "temperature": "15°C", "condition": "Cloudy"}tools = [{"type": "function","function": {"name": "get_weather","description": "Return current weather for the given location.","parameters": {"type": "object","properties": {"location": {"type": "string","description": "City name",}},"required": ["location"],},},}]messages = [{"role": "user", "content": "What is the weather in Seattle?"},]# First request -- model requests a tool callresponse = client.chat.completions.create(model="muse-spark-1.3",messages=messages,tools=tools,)msg = response.choices[0].message# Append the assistant message with its tool callsmessages.append({"role": "assistant","content": msg.content or "","tool_calls": [{"id": tc.id,"type": "function","function": {"name": tc.function.name,"arguments": tc.function.arguments,},}for tc in msg.tool_calls],})# Execute each tool and append resultsfor tc in msg.tool_calls:args = json.loads(tc.function.arguments)result = get_weather(**args)messages.append({"role": "tool","tool_call_id": tc.id,"content": json.dumps(result),})# Second request -- model incorporates the tool resultresponse = client.chat.completions.create(model="muse-spark-1.3",messages=messages,tools=tools,)print(response.choices[0].message.content)# "The current weather in Seattle is 15°C and cloudy."
Strict tool schemas
Function tools accept an optional strict flag. Today it controls only schema validation: whether the server checks your parameters against the supported strict subset.
strictdefaults tofalse: when you omitstrictor set it tofalse, the schema is accepted as-is. A schema that omits a roottypeis normalized rather than rejected (see the tip under Defining tools).- Set
strict: true: the server validates your schema against the strict subset, for exampleadditionalProperties: falseand every property listed inrequired. A schema that violates the subset returnsHTTP 400, but only whenstrict: true; the same schema withstrictomitted is accepted. See Structured output → Strict mode for the full subset rules.
This strict default applies to both Responses and Chat Completions API function tools.
Tool search
Build agents with hundreds of tools without loading the whole library on every turn. Tool search keeps deferred definitions out of the prompt until the model needs them, so you cut input tokens and keep the cache prefix intact.
How tool search works
By default every tool in tools loads into context up front and you pay for those definitions each turn. Tool search defers that cost: you mark tools as deferred and the model loads only what it decides it needs.
Turn it on with two changes:
- Add
{"type": "tool_search"}to yourtoolsarray. - Mark each tool you want to defer with
defer_loading: true.
A deferred tool's name and description stay visible from the start; only its parameter schema is withheld until tool search loads it. When the model loads a tool, the API appends it at the end of the context, preserving the cache prefix from earlier turns and keeping cost and latency low.
There are two execution modes:
- Hosted: the API searches the deferred tools you declared and loads the matching subset in the same response. Use this when you know the full inventory at request time.
- Client-executed: the model issues a search request, your app performs the lookup, and you return the tools to load. Use this when availability depends on project or tenant state the API can't see.
Defer tools and group them into namespaces
You can defer standalone function tools, but grouping them into a namespace gives the model a cleaner surface to search and saves more tokens. For a namespace the model sees only the namespace name and description up front; the functions inside load through tool search.
Set defer_loading: true on the functions inside the namespace, not on the namespace object itself. A namespace can mix deferred and non-deferred functions: non-deferred functions are callable immediately, deferred ones load on demand.
json{"tools": [{"type": "namespace","name": "crm","description": "CRM tools for customer lookup and order management.","tools": [{"type": "function","name": "list_open_orders","description": "List open orders for a customer ID.","defer_loading": true,"parameters": {"type": "object","properties": { "customer_id": { "type": "string" } },"required": ["customer_id"],"additionalProperties": false}}]},{ "type": "tool_search" }]}
Write clear, high-level namespace descriptions. The model uses them to decide which namespace to load. Keep each namespace focused, aim for fewer than 10 functions, and put richer detail in the deferred function descriptions that load only when needed.
Hosted tool search
Hosted tool search is the simplest option when you know the full set of tools up front. Declare your tools, add {"type": "tool_search"}, and let the API decide what to load.
pythonimport osfrom openai import OpenAIclient = OpenAI(base_url="https://api.meta.ai/v1",api_key=os.environ["MODEL_API_KEY"],)response = client.responses.create(model="muse-spark-1.3",input="List open orders for customer CUST-12345.",parallel_tool_calls=False,tools=[{"type": "namespace","name": "crm","description": "CRM tools for customer lookup and order management.","tools": [{"type": "function","name": "get_customer_profile","description": "Fetch a customer profile by customer ID.","parameters": {"type": "object","properties": {"customer_id": {"type": "string",},},"required": ["customer_id",],"additionalProperties": False,},},{"type": "function","name": "list_open_orders","description": "List open orders for a customer ID.","defer_loading": True,"parameters": {"type": "object","properties": {"customer_id": {"type": "string",},},"required": ["customer_id",],"additionalProperties": False,},},],},{"type": "tool_search",},],)print(response.model_dump_json(indent=2))
typescriptimport OpenAI from 'openai';const apiKey = process.env.MODEL_API_KEY;if (!apiKey) {throw new Error('MODEL_API_KEY is not set');}const client = new OpenAI({baseURL: 'https://api.meta.ai/v1',apiKey,});const response = await client.responses.create({model: 'muse-spark-1.3',input: 'List open orders for customer CUST-12345.',parallel_tool_calls: false,tools: [{type: 'namespace',name: 'crm',description: 'CRM tools for customer lookup and order management.',tools: [{type: 'function',name: 'get_customer_profile',description: 'Fetch a customer profile by customer ID.',parameters: {type: 'object',properties: {customer_id: {type: 'string',},},required: ['customer_id',],additionalProperties: false,},},{type: 'function',name: 'list_open_orders',description: 'List open orders for a customer ID.',defer_loading: true,parameters: {type: 'object',properties: {customer_id: {type: 'string',},},required: ['customer_id',],additionalProperties: false,},},],},{type: 'tool_search',},],});console.log(JSON.stringify(response, null, 2));
pythonimport jsonimport osimport requestsresponse = requests.post("https://api.meta.ai/v1/responses",headers={"Authorization": f"Bearer {os.environ['MODEL_API_KEY']}","Content-Type": "application/json",},json={"model": "muse-spark-1.3","input": "List open orders for customer CUST-12345.","parallel_tool_calls": False,"tools": [{"type": "namespace","name": "crm","description": "CRM tools for customer lookup and order management.","tools": [{"type": "function","name": "get_customer_profile","description": "Fetch a customer profile by customer ID.","parameters": {"type": "object","properties": {"customer_id": {"type": "string",},},"required": ["customer_id",],"additionalProperties": False,},},{"type": "function","name": "list_open_orders","description": "List open orders for a customer ID.","defer_loading": True,"parameters": {"type": "object","properties": {"customer_id": {"type": "string",},},"required": ["customer_id",],"additionalProperties": False,},},],},{"type": "tool_search",},],},)response.raise_for_status()print(json.dumps(response.json(), indent=2))
shellcurl -X POST "https://api.meta.ai/v1/responses" \-H "Authorization: Bearer $MODEL_API_KEY" \-H "Content-Type: application/json" \-d '{"model": "muse-spark-1.3","input": "List open orders for customer CUST-12345.","parallel_tool_calls": false,"tools": [{"type": "namespace","name": "crm","description": "CRM tools for customer lookup and order management.","tools": [{"type": "function","name": "get_customer_profile","description": "Fetch a customer profile by customer ID.","parameters": {"type": "object","properties": {"customer_id": {"type": "string"}},"required": ["customer_id"],"additionalProperties": false}},{"type": "function","name": "list_open_orders","description": "List open orders for a customer ID.","defer_loading": true,"parameters": {"type": "object","properties": {"customer_id": {"type": "string"}},"required": ["customer_id"],"additionalProperties": false}}]},{"type": "tool_search"}]}'
If the model needs a deferred tool, output contains two items before the final function call:
tool_search_call: records the search the model ran, including thepathsit searched.tool_search_output: the loaded definitions, now callable.
json[{"type": "tool_search_call","id": "tsc_abc123","call_id": null,"execution": "server","status": "completed","arguments": { "paths": ["crm"] }},{"type": "tool_search_output","id": "tso_abc123","call_id": null,"execution": "server","status": "completed","tools": [{"type": "namespace","name": "crm","description": "CRM tools for customer lookup and order management.","tools": [{"type": "function","name": "list_open_orders","description": "List open orders for a customer ID.","defer_loading": true,"parameters": {"type": "object","properties": { "customer_id": { "type": "string" } },"required": ["customer_id"],"additionalProperties": false}}]}]},{"type": "function_call","name": "list_open_orders","namespace": "crm","call_id": "call_abc123","arguments": "{\"customer_id\":\"CUST-12345\"}"}]
In hosted mode execution is "server" and call_id is null. The API runs the search and load step for you; you handle the resulting function_call the same way you handle ordinary tool calling.
Client-executed tool search
Client-executed tool search hands the lookup to your app. Use it when available tools depend on state the API can't see at request time, such as which integrations a tenant has enabled.
Set execution: "client" on the tool_search tool and provide a description and parameters schema for the search arguments your app expects. The model emits a tool_search_call and stops; you run the search and return a tool_search_output with the tools to load.
Python (OpenAI SDK)import osfrom openai import OpenAIclient = OpenAI(base_url="https://api.meta.ai/v1",api_key=os.environ["MODEL_API_KEY"],)# First request -- the model asks your app to find toolsfirst = client.responses.create(model="muse-spark-1.3",input="Find the shipping ETA tool, then use it for order_42.",tools=[{"type": "tool_search","execution": "client","description": "Find the project-specific tools needed to continue the task.","parameters": {"type": "object","properties": {"goal": {"type": "string"}},"required": ["goal"],"additionalProperties": False,},}],parallel_tool_calls=False,)# The model emits a tool_search_call and stopssearch_call = next(item for item in first.output if item.type == "tool_search_call")# Your application performs the lookup and picks the tools to loadloaded_tools = [{"type": "function","name": "get_shipping_eta","description": "Look up shipping ETA details for an order.","defer_loading": True,"parameters": {"type": "object","properties": {"order_id": {"type": "string"}},"required": ["order_id"],"additionalProperties": False,},}]# Second request -- return a tool_search_output echoing the same call_idsecond = client.responses.create(model="muse-spark-1.3",input=[*first.output,{"type": "tool_search_output","execution": "client","call_id": search_call.call_id,"status": "completed","tools": loaded_tools,},],)print(second.output)
On the first turn the model returns only the search request:
json[{"type": "tool_search_call","id": "tsc_xyz789","call_id": "call_abc123","execution": "client","status": "completed","arguments": { "goal": "Find the shipping ETA tool for order_42." }}]
Return the tools to load in a tool_search_output that echoes the same call_id. On the next turn the loaded tool is callable like any other function. The tools array in your tool_search_output defines exactly what becomes available; anything you omit stays unavailable. Changing the loaded set on a later turn breaks the cache from that point forward, so keep it stable once loaded.
Tool search constraints
- Responses and Messages APIs only: tool search is not available on the Chat Completions API. On the Messages API, add
{"type": "tool_search"}totoolsand setdefer_loading: trueon the tools to defer; hosted searches come back asserver_tool_useblocks. - One
tool_searchtool per request: declaring more than one returnsHTTP 400. - Hosted tool search needs at least one deferred tool: a
{"type": "tool_search"}tool with no tool markeddefer_loading: truereturnsHTTP 400. - Deferred tools need a
tool_searchtool: marking a tooldefer_loading: truewithout addingtool_searchreturnsHTTP 400. tool_searchis a reserved name: you cannot also declare afunctiontool namedtool_search.- Not combinable with structured output: a request that uses tool search cannot also set a
json_schemaresponse format (text.format); the combination returnsHTTP 400. This applies to both hosted and client-executed modes. - Needs reasoning: a request that uses tool search cannot set
reasoning.effortto"none"; the combination returnsHTTP 400.
Guidelines
- Handle parallel tool calls: The model may return multiple calls in
tool_callsin one turn, for example looking up three cities at once. Execute every call, then append onetoolmessage per call, each with its owntool_call_id, before the next request. The multi-turn example above already loops overtool_callsto do this. To limit the model to one call per turn, setparallel_tool_calls: false(defaulttrue). This flag takes effect only when you provide functiontools; with no function tools it is accepted but has no effect, matching OpenAI. It applies to both Chat Completions and the Responses API. max_tool_callslimits built-in tools only:max_tool_callscaps the model's built-in tool calls (such asweb_search), counted across all built-in tools; the model ignores calls beyond the limit. It has a minimum of 1 and no enforced maximum. It does not limit client-side function-tool calls. To bound function-tool calls, setparallel_tool_calls: falseor cap turns in your own loop.tool_choicemust be"auto": Only"auto"(the default) is supported on both Chat Completions and the Responses API;"none","required", and named function choices returnHTTP 400("only"auto"is supported fortool_choice"). With"auto", the model decides whether to call a tool, so your app should handle both a tool call and a plain text response.- Tool-call arguments stream: With
stream: true, the model streams arguments incrementally. On Chat Completions, accumulatetool_callsdeltas across chunks byindex; on the Responses API, consumeresponse.function_call_arguments.deltaevents and finalize onresponse.function_call_arguments.done. - Always include the full assistant message: When returning tool results, include the complete assistant message with its
tool_callsarray before thetoolmessage. Omitting it returnsHTTP 400. - Match
tool_call_id/call_idto the originating call: On Chat Completions, eachtoolmessage must carry thetool_call_idof the call it answers. On the Responses API, when you construct input manually withoutprevious_response_id, everyfunction_call_output.call_idmust match afunction_call.call_idin the same request; a mismatch returnsHTTP 400. Withprevious_response_id, the server resolves these from stored history. - Keep
call_idwithin 1–64 characters: Everycall_idon bothfunction_callandfunction_call_outputitems must be 1 to 64 characters. An empty or overlengthcall_idreturnsHTTP 400. - Tool name validation applies only to the most recent assistant turn: When you provide a
toolsarray, the server validates thattool_calls[].function.namematches a defined tool name. Historical assistant messages with tool calls that reference tools no longer in the currenttoolsarray do not cause validation errors, as long as those calls already have corresponding tool results in history. - Use
tools, not deprecated fields:functionsandfunction_callare deprecated. Usetoolsandtool_choice. - Use structured output for complex arguments: If you need strict control over tool argument formats, combine tool calling with structured output via
response_format. - Defer large tool sets: When you have many tools, front-loading every definition wastes tokens. Mark tools with
defer_loading: trueand add thetool_searchtool so the model loads only what it needs on demand. See tool search.
Next steps
Now that you can call tools, keep your agent fast and reliable.
- Lock argument shapes with structured output for predictable parsing. With tool search, pair a loaded tool with structured output on a follow-up turn, once tool search is out of the loop.
- Check the full request and response schema in the Chat Completions API reference.
- Check the Responses API reference for the full
tool_search,tool_search_call, andtool_search_outputschemas.