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.
Remote MCP calls are not narrated on a stream yet: a streamed turn runs the same calls and returns the same answer, but emits no mcp_* events and, on Messages, no mcp_tool_use / mcp_tool_result blocks, so a streaming client has nothing to replay and the next turn fetches the server's tool list again. Non-streamed turns carry them in full.
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-v4-flash,qwen,glm, andmuse-spark, whose upstreams have no word for it, and on the Claude heads listed in the table below, 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 droppingtoolssonnet,opus,fableand their twinssonnet-5-5,opus-5-5,fable-5-1Claude Sonnet 5.5, Opus 5.5, and Fable 5.1 reject a forced choice ( anyor a named tool). The pinssonnet-5,opus-5, andfable-5accept it.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.
Remote MCP servers
Point the model at a remote Model Context Protocol server and the platform runs its tools for you, inside the turn, on every model: on Anthropic's connector for the Claude family, on OpenAI's and xAI's for the GPT and Grok families, and through the platform's own MCP client for Gemini and the open-weight models (deepseek-v4-flash, qwen, glm, kimi, muse-spark), which see the server's tools as ordinary functions and get their results back the same way. You declare the server in each API's own shape and get the calls and their results back in the response. The server must be reachable over https on a public address; a bearer token goes in the request and is never stored or traced.
- Chat Completions
- Responses
- Messages
- Chat Completions
- Responses
- Messages
response = client.chat.completions.create(
model="sonnet",
messages=[{"role": "user", "content": "Which wiki sections does anthropics/anthropic-sdk-python have?"}],
tools=[{
"type": "mcp",
"server_label": "deepwiki",
"server_url": "https://mcp.deepwiki.com/mcp",
"require_approval": "never",
}],
)
print(response.choices[0].message.content)
response = client.responses.create(
model="sonnet",
input="Which wiki sections does anthropics/anthropic-sdk-python have?",
tools=[{
"type": "mcp",
"server_label": "deepwiki",
"server_url": "https://mcp.deepwiki.com/mcp",
"require_approval": "never",
}],
)
for item in response.output:
if item.type == "mcp_call":
print(item.name, item.output[:80])
print(response.output_text)
response = client.beta.messages.create(
model="sonnet",
max_tokens=1024,
messages=[{"role": "user", "content": "Which wiki sections does anthropics/anthropic-sdk-python have?"}],
mcp_servers=[{"type": "url", "url": "https://mcp.deepwiki.com/mcp", "name": "deepwiki"}],
tools=[{"type": "mcp_toolset", "mcp_server_name": "deepwiki"}],
betas=["mcp-client-2025-11-20"],
)
for block in response.content:
if block.type == "mcp_tool_use":
print(block.name, block.input)
const response = await client.chat.completions.create({
model: "sonnet",
messages: [{ role: "user", content: "Which wiki sections does anthropics/anthropic-sdk-python have?" }],
// Chat Completions has no MCP tool of its own; this entry is a MindsHub extension.
tools: [{ type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp", require_approval: "never" } as any],
});
console.log(response.choices[0].message.content);
const response = await client.responses.create({
model: "sonnet",
input: "Which wiki sections does anthropics/anthropic-sdk-python have?",
tools: [{ type: "mcp", server_label: "deepwiki", server_url: "https://mcp.deepwiki.com/mcp", require_approval: "never" }],
});
for (const item of response.output) {
if (item.type === "mcp_call") console.log(item.name, item.output?.slice(0, 80));
}
console.log(response.output_text);
const response = await client.beta.messages.create({
model: "sonnet",
max_tokens: 1024,
messages: [{ role: "user", content: "Which wiki sections does anthropics/anthropic-sdk-python have?" }],
mcp_servers: [{ type: "url", url: "https://mcp.deepwiki.com/mcp", name: "deepwiki" }],
tools: [{ type: "mcp_toolset", mcp_server_name: "deepwiki" }],
betas: ["mcp-client-2025-11-20"],
});
for (const block of response.content) {
if (block.type === "mcp_tool_use") console.log(block.name, block.input);
}
What comes back. On Messages, each call is an mcp_tool_use block followed by its mcp_tool_result block, ahead of the text, on a Claude or a GPT target. On Responses, a non-streamed turn reports mcp_list_tools (the tools the server advertised) and one mcp_call item per call, ahead of the message; keep them in your next request's input (or chain with previous_response_id) and the server's tool list is not fetched again. Chat Completions has no slot for the calls: the answer reflects the tool's output and the body is unchanged. Send the previous turn back as usual on every API and the calls stay in the model's context.
Choosing tools. OpenAI's allowed_tools (a list of names, or {"tool_names": [...]}) and Anthropic's mcp_toolset default_config / configs both work on every target. A deny-list (configs: {"delete": {"enabled": false}} under an enabled default) runs on Claude, Gemini and the open-weight models; GPT and Grok targets have no spelling for one, so such a server is dropped rather than run with the denied tools re-enabled, and named on X-MindsHub-Dropped-Params. Where the platform's own client runs the server, one request may declare at most 8 servers and each exposes at most 64 tools to the model; the rest are dropped and reported. A server whose tool list cannot be fetched is skipped for that turn and reported as an mcp_list_tools item carrying the error on Responses.
Approvals. require_approval is read the way each API defines it, and a tool is never run ungated because a target could not gate it:
| API | GPT target | Every other target (Claude, Grok, Gemini, open-weight) |
|---|---|---|
| Messages | never (the dialect has no approval field) | never |
| Responses | OpenAI's semantics untouched, omitted included (mcp_approval_request item, answer with an mcp_approval_response on the next turn) | tools resolving to never run; the rest are dropped and named on X-MindsHub-Dropped-Params |
| Chat Completions | explicit "never" required (the body cannot carry an approval request); otherwise dropped and named | same |
Billing. No per-call charge. The server is yours and no one charges to call it; you pay the tokens the tool schemas and results add to the turn, which usage already reports. Calls are counted (mcp_call) on your usage summary.
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 for function tools. Remote MCP runs on every model: natively on the Claude, GPT and Grok families, through the platform's own client on Gemini and on deepseek-v4-flash, qwen, glm, kimi and muse-spark. OpenAI's forced tool_choice: {"type": "mcp", ...} is honoured on GPT and Grok targets and read as auto elsewhere, where the tools run at the model's discretion. Everything else about tool calling is uniform across the catalog.
Errors you can hit
A malformed tool schema returns the provider's own 400 naming the keyword at fault. A malformed mcp entry (no server_url, or one that is not https) is a 400 with code: invalid_value on Responses; on Messages an mcp_toolset whose mcp_server_name names no mcp_servers entry, or a server no toolset references, is a 400 as on Anthropic. A server that cannot be reached surfaces as the upstream's own error. A mismatched call id on Gemini models fails the follow-up request with a 400. See Errors.