Tool calling
Tool calling (function calling) gives the model access to your data and actions: an orders database, an internal API, a calculator. Instead of answering right away, the model asks your code to run a function, receives the result, and continues from there. This is how assistants, agents, and bots that can “see” your systems are built.
Tools are supported in all three chat protocols: /v1/chat/completions, /v1/messages, and /v1/responses. The first one is the primary; the other two are covered briefly below.
How the round works
Section titled “How the round works”- You send a regular request plus a
toolsarray: function descriptions with a name, a description, and a JSON Schema of arguments. - The model decides it needs a tool and answers with a call instead of text: the function name and a JSON of arguments. The
finish_reasonof such a response is"tool_calls". - Your code executes the function: queries a database, calls an external API, computes — anything.
- You send the result back in the same thread: the entire prior history plus a message with the
toolrole. - The model reads the result and replies with the final text for the user.
The split of responsibility is strict: the model never executes code itself. It only picks a tool and fills in the arguments — execution always happens on your side, and only you decide what to actually run and what to reject. The model has no access to your systems: it sees only the tools you described in the request and only the results you return to the conversation yourself.
/v1/chat/completions (OpenAI)
Section titled “/v1/chat/completions (OpenAI)”A tool is described in the tools field:
{ "type": "function", "function": { "name": "get_weather", "description": "Current weather in a given city", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "City name"} }, "required": ["city"] } }}The model reads name and description to understand when the tool is needed; parameters is the JSON Schema of arguments the model builds the call against.
The tool_choice field controls how the model uses tools:
| Value | Behavior |
|---|---|
auto |
Default: the model decides whether to call a tool or answer in text |
none |
Tools are not used |
required |
The model must call at least one tool |
{"type": "function", "function": {"name": "get_weather"}} |
Call this specific function |
When the model decides to call a tool, the response comes without text — with a list of tool_calls and finish_reason: "tool_calls":
{ "choices": [ { "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_9c2f1e", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"Kazan\"}" } } ] }, "finish_reason": "tool_calls" } ]}A common pitfall: arguments is a JSON string, not an object. Parse it before working with the arguments: json.loads(...) in Python, JSON.parse(...) in JavaScript.
Returning the result
Section titled “Returning the result”The second round is the same endpoint, but messages now carries:
- the original user message;
- the assistant response the API returned, in full — with its
tool_calls(do not rebuild it by hand; pass the object as is); - the result — a message with the
toolrole:
{ "role": "tool", "tool_call_id": "call_9c2f1e", "content": "{\"temp\": 18, \"condition\": \"overcast\"}"}tool_call_id must match the id from tool_calls. content is a string; for structured data, returning JSON is convenient.
Full example
Section titled “Full example”import jsonfrom openai import OpenAI
client = OpenAI( base_url="https://api.mixen.ai/v1", api_key="mxn-...",)
tools = [{ "type": "function", "function": { "name": "get_weather", "description": "Current weather in a given city", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "City name"} }, "required": ["city"] } }}]
def get_weather(city: str) -> str: # a real call to your backend or an external API would live here return json.dumps({"city": city, "temp": 18, "condition": "overcast"})
messages = [{"role": "user", "content": "What's the weather in Kazan? Walk or take an umbrella?"}]
while True: resp = client.chat.completions.create( model="gpt-5.6-luna", messages=messages, tools=tools, ) msg = resp.choices[0].message messages.append(msg) # the assistant response in full, tool_calls included
if not msg.tool_calls: # finish_reason "stop" — final text print(msg.content) break
for call in msg.tool_calls: args = json.loads(call.function.arguments) # arguments is a JSON string result = get_weather(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, })curl https://api.mixen.ai/v1/chat/completions \ -H "Authorization: Bearer $MIXEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-5.6-luna", "messages": [{"role": "user", "content": "What's the weather in Kazan?"}], "tools": [{ "type": "function", "function": { "name": "get_weather", "description": "Current weather in a given city", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } }] }'The response to this request is the tool_calls JSON shown above.
The loop does not have to end after one round: if, given the result, the model decides to call another tool — or another one — the loop continues on its own until a response arrives with text. That is how agentic scenarios work.
Parallel calls
Section titled “Parallel calls”A single response may carry several tool_calls at once — say, weather in three cities or a search across several sources. The client executes all of them (in parallel, if you like) and returns one role: "tool" message per call — in the same order the calls arrived, each with its own tool_call_id.
Streaming
Section titled “Streaming”stream: true works with tools, but behaves differently from plain text. Text still arrives in deltas, while tool_calls do not: the assembled list of calls is emitted whole, in a single chunk near the end of the stream, and finish_reason: "tool_calls" lands in the last chunk. You will not need code to stitch arguments out of deltas — but there is no “streaming of calls” either.
If you do not need to show intermediate text to the user live, calling without stream is simpler — less chunk-assembly code, same behavior.
/v1/messages (the Anthropic protocol)
Section titled “/v1/messages (the Anthropic protocol)”The Anthropic protocol has the same ideas, but the fields are named differently and the arguments arrive as an object, not a string:
- Declaration:
tools: [{"name": "...", "description": "...", "input_schema": <JSON Schema>}]— nofunctionwrapper. - Response: a
{"type": "tool_use", "id": "...", "name": "...", "input": {...}}block inside thecontentarray;stop_reason: "tool_use".inputis already an object — no parsing needed. - Second round: a
{"role": "user", "content": [{"type": "tool_result", "tool_use_id": "...", "content": "<result as a string>"}]}message — onetool_resultpertool_use.
from anthropic import Anthropic
client = Anthropic(base_url="https://api.mixen.ai", api_key="mxn-...")
resp = client.messages.create( model="anthropic/claude-sonnet-5", max_tokens=1024, messages=[{"role": "user", "content": "What's the weather in Kazan?"}], tools=[{ "name": "get_weather", "description": "Current weather in a given city", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }],)block = next(b for b in resp.content if b.type == "tool_use")result = get_weather(**block.input) # input is an object — no json.loadsfinal = client.messages.create( model="anthropic/claude-sonnet-5", max_tokens=1024, messages=[ {"role": "user", "content": "What's the weather in Kazan?"}, {"role": "assistant", "content": [block]}, # the tool_use block as is {"role": "user", "content": [{ "type": "tool_result", "tool_use_id": block.id, "content": result, }]}, ],)print(final.content[0].text)The base URL for the Anthropic SDK has no /v1 suffix: the SDK adds the path itself. See Claude Code.
/v1/responses
Section titled “/v1/responses”In the Responses API the tool declaration is flat — no nested function:
- Declaration:
tools: [{"type": "function", "name": "...", "description": "...", "parameters": <JSON Schema>}] - Response: an
outputitem of the form{"type": "function_call", "call_id": "...", "name": "...", "arguments": "<JSON string>"}— hereargumentsis a string again. - Second round:
inputcarries the whole history — messages with"type": "message", thefunction_callitem itself as is, and next to it{"type": "function_call_output", "call_id": "...", "output": "<result as a string>"}.
import jsonfrom openai import OpenAI
client = OpenAI(base_url="https://api.mixen.ai/v1", api_key="mxn-...")
resp = client.responses.create( model="gpt-5.6-luna", input="What's the weather in Kazan?", tools=[{ "type": "function", "name": "get_weather", "description": "Current weather in a given city", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }],)call = next(o for o in resp.output if o.type == "function_call")result = get_weather(**json.loads(call.arguments)) # arguments is a JSON stringfinal = client.responses.create( model="gpt-5.6-luna", input=[ {"type": "message", "role": "user", "content": "What's the weather in Kazan?"}, call, # the function_call item as is {"type": "function_call_output", "call_id": call.call_id, "output": result}, ],)print(final.output_text)What to keep in mind
Section titled “What to keep in mind”- Billing is the usual one — input and output tokens at the model’s rates. Tool declarations count as input and are billed as input tokens: in agentic loops with a large tool list this adds up; context caching helps.
- A
toolrole with no call is an error. If you send arole: "tool"message that is not preceded by an assistant response withtool_calls, the upstream returns400. Return results only in response to a real call, and in the same thread. - Support depends on the model. A model that cannot use tools returns
400. Nearly all chat models in the catalog support them; check a specific model’s page for nuances. - Further reading: Chat and streaming — the main endpoint’s parameters, Errors — codes, limits, retries.