Tool calling
Declare tools, and when the model wants one the response carries a call with JSON arguments. You run the function and send the result back as the next turn. The round trip works on every model, in each API's own shape: OpenAI's tools/tool_calls on Chat Completions, the flat function_call items on Responses, tool_use/tool_result blocks on Messages.
Works with
Every generation model. What differs per model is how far a forced tool_choice is honored and whether "none" has a native spelling; see Per-model differences and the capability matrix.
Request
Declare a function, call the model, run the tool, and send the result back with the same tools array.
- Chat Completions
- Responses
- Messages
- Chat Completions
- Responses
- Messages
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
messages = [{"role": "user", "content": "What's the weather in Lisbon?"}]
response = client.chat.completions.create(model="mindshub_air", messages=messages, tools=tools)
reply = response.choices[0].message
if not reply.tool_calls:
raise RuntimeError(f"Expected a tool call, got prose: {reply.content}")
call = reply.tool_calls[0]
result = "19°C, light rain" # run the real function here
messages.append(reply)
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})
final = client.chat.completions.create(model="mindshub_air", messages=messages, tools=tools)
print(final.choices[0].message.content)
tools = [{
"type": "function",
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
first = client.responses.create(model="mindshub_air", input="What's the weather in Lisbon?", tools=tools)
calls = [item for item in first.output if item.type == "function_call"]
if not calls:
raise RuntimeError(f"Expected a tool call, got prose: {first.output_text}")
call = calls[0]
result = "19°C, light rain" # run the real function here
final = client.responses.create(
model="mindshub_air",
previous_response_id=first.id,
input=[{"type": "function_call_output", "call_id": call.call_id, "output": result}],
tools=tools,
)
print(final.output_text)
tools = [{
"name": "get_weather",
"description": "Get the current weather for a city",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
messages = [{"role": "user", "content": "What's the weather in Lisbon?"}]
first = client.messages.create(model="mindshub_air", max_tokens=1024, messages=messages, tools=tools)
uses = [block for block in first.content if block.type == "tool_use"]
if not uses:
raise RuntimeError("Expected a tool call, got prose")
use = uses[0]
result = "19°C, light rain" # run the real function here
messages.append({"role": "assistant", "content": first.content})
messages.append({"role": "user", "content": [
{"type": "tool_result", "tool_use_id": use.id, "content": result},
]})
final = client.messages.create(model="mindshub_air", max_tokens=1024, messages=messages, tools=tools)
print(final.content[-1].text)
const tools = [{
type: "function" as const,
function: {
name: "get_weather",
description: "Get the current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
}];
const messages: any[] = [{ role: "user", content: "What's the weather in Lisbon?" }];
const response = await client.chat.completions.create({ model: "mindshub_air", messages, tools });
const reply = response.choices[0].message;
if (!reply.tool_calls?.length) throw new Error(`Expected a tool call, got prose: ${reply.content}`);
const call = reply.tool_calls[0];
const result = "19°C, light rain"; // run the real function here
messages.push(reply, { role: "tool", tool_call_id: call.id, content: result });
const final = await client.chat.completions.create({ model: "mindshub_air", messages, tools });
console.log(final.choices[0].message.content);
const tools = [{
type: "function" as const,
name: "get_weather",
description: "Get the current weather for a city",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}];
const first = await client.responses.create({ model: "mindshub_air", input: "What's the weather in Lisbon?", tools });
const call = first.output.find((item) => item.type === "function_call");
if (!call) throw new Error(`Expected a tool call, got prose: ${first.output_text}`);
const result = "19°C, light rain"; // run the real function here
const final = await client.responses.create({
model: "mindshub_air",
previous_response_id: first.id,
input: [{ type: "function_call_output", call_id: call.call_id, output: result }],
tools,
});
console.log(final.output_text);
const tools = [{
name: "get_weather",
description: "Get the current weather for a city",
input_schema: {
type: "object" as const,
properties: { city: { type: "string" } },
required: ["city"],
},
}];
const messages: any[] = [{ role: "user", content: "What's the weather in Lisbon?" }];
const first = await client.messages.create({ model: "mindshub_air", max_tokens: 1024, messages, tools });
const use = first.content.find((block) => block.type === "tool_use");
if (!use) throw new Error("Expected a tool call, got prose");
const result = "19°C, light rain"; // run the real function here
messages.push(
{ role: "assistant", content: first.content },
{ role: "user", content: [{ type: "tool_result", tool_use_id: use.id, content: result }] },
);
const final = await client.messages.create({ model: "mindshub_air", max_tokens: 1024, messages, tools });
console.log(final.content.at(-1));
Rules that hold on every API:
- A single response can contain multiple tool calls. Execute them all and return one result per call.
- Echo call ids back exactly as you received them. On Gemini models the ids carry provider state, and a mismatched id fails the request.
- Send the same
toolsarray on the follow-up call. - The reply can come back as prose instead of a call on any model, so the guard above is a real branch, not defensive noise.
Response
- Chat Completions
- Responses
- Messages
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "toolu_01V9g8n42TGjbfJmvecVydN3",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Lisbon\"}"}
}
]
}
{"type": "function_call", "id": "fc_a91b", "call_id": "call_a91b",
"name": "get_weather", "arguments": "{\"city\": \"Lisbon\"}"}
{"type": "tool_use", "id": "toolu_01V9g8n42TGjbfJmvecVydN3",
"name": "get_weather", "input": {"city": "Lisbon"}}
On Chat Completions the turn ends with finish_reason: "tool_calls"; on Messages with stop_reason: "tool_use"; on Responses the function_call item sits in output and status is completed. Use role: "tool" for Chat Completions results (role: "function" is accepted for backward compatibility but not handled as a tool result).
Streaming
OpenAI- and Claude-family models stream a tool call as an opening delta (with id, type, and the function name) followed by argument fragments; Gemini models deliver each call as a single frame carrying the complete arguments. Responses streams response.function_call_arguments.delta events between an output_item.added/output_item.done pair. Details in Streaming.
Controlling which tool runs
tool_choice takes "auto", "required" ("any" on Messages), "none", or a named tool. Two behaviors are MindsHub-specific:
-
"none"is honored everywhere. On the Claude family it is sent as Anthropic's ownnone; onmindshub_air,deepseek,qwen,glm, andmuse-spark, whose upstreams have no word for it, it is honored by dropping thetoolsfrom that request instead, reported astool_choice=none>droppedinX-MindsHub-Clamped-Params. -
A forced choice a model can't accept is rewritten rather than rejected. The request is served instead of returning the provider's 400:
Model What it restricts What we send instead kimiA named choice 400s while the model's thinking is on, which is the provider default. "required"works.Named → "auto"muse-sparkOnly "auto"is accepted; named,"required", and"none"all 400.Named and "required"→"auto";"none"honored by droppingtoolsOn these models a forced choice becomes a strong hint, so the model may answer in prose. Keep the "call the tool first" instruction in your prompt, and validate that you got a call before relying on one. The rewrite is reported on
X-MindsHub-Clamped-Paramsastool_choice=required>auto,tool_choice=named>auto, ortool_choice=none>dropped.
parallel_tool_calls is honored wherever the model can express it, including on the Claude family, which spells it inverted (disable_parallel_tool_use). Gemini models can't express it and report it as dropped.
Legacy function calling (Chat Completions only)
functions and function_call are OpenAI's original function-calling fields, deprecated in favour of tools/tool_choice. They still work here, in all four places OpenAI switches shape when you use them:
| You send / receive | |
|---|---|
| Request | functions: [{name, description, parameters}], function_call: "auto" | "none" | {"name": ...} |
| Response | message.function_call: {name, arguments}, finish_reason: "function_call" |
| Streaming | delta.function_call: the name on the opening chunk, then arguments fragments |
| Replay | the assistant turn with its function_call, then {"role": "function", "name": ..., "content": ...} |
Two limits: one call per turn (the legacy field is a single object, so extras are dropped), and sending tools alongside functions is read as a modern request and answered with tool_calls. New code should use tools.
Per-model differences
The forced-choice table above is the whole list today. Everything else about tool calling is uniform across the catalog. The per-model columns for named tool_choice, tool_choice: none, and parallel calls are on the capability matrix.
Errors you can hit
A malformed tool schema returns the provider's own 400 naming the keyword at fault. A mismatched call id on Gemini models fails the follow-up request with a 400. See Errors.