Skip to main content

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.

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)

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 tools array 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

{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "toolu_01V9g8n42TGjbfJmvecVydN3",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"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 own none; on mindshub_air, deepseek, qwen, glm, and muse-spark, whose upstreams have no word for it, it is honored by dropping the tools from that request instead, reported as tool_choice=none>dropped in X-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:

    ModelWhat it restrictsWhat 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 dropping tools

    On 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-Params as tool_choice=required>auto, tool_choice=named>auto, or tool_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
Requestfunctions: [{name, description, parameters}], function_call: "auto" | "none" | {"name": ...}
Responsemessage.function_call: {name, arguments}, finish_reason: "function_call"
Streamingdelta.function_call: the name on the opening chunk, then arguments fragments
Replaythe 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.