Tool calling

Tool calling

Tools let a model ask your code to do something: read a file, run a command, call an API.
The gateway passes your schemas through and returns the model’s calls in the shape your
client expects.

Check capabilities.tools on the model first. A model without it cannot use tools, and
sending schemas to it wastes prompt tokens.

A complete loop

{
  "model": "glm-5.3-flash",
  "messages": [{ "role": "user", "content": "Read /etc/hostname" }],
  "tools": [{
    "type": "function",
    "function": {
      "name": "read_file",
      "description": "Read a file from disk.",
      "parameters": {
        "type": "object",
        "properties": { "path": { "type": "string", "description": "Absolute path." } },
        "required": ["path"]
      }
    }
  }]
}

The reply carries the call:

{
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": null,
      "tool_calls": [{
        "id": "call-1",
        "type": "function",
        "function": { "name": "read_file", "arguments": "{\"path\":\"/etc/hostname\"}" }
      }]
    },
    "finish_reason": "tool_calls"
  }]
}

Then send the result back and let the model finish:

{
  "model": "glm-5.3-flash",
  "messages": [
    { "role": "user", "content": "Read /etc/hostname" },
    { "role": "assistant", "content": null, "tool_calls": [{ "id": "call-1", "type": "function", "function": { "name": "read_file", "arguments": "{\"path\":\"/etc/hostname\"}" } }] },
    { "role": "tool", "tool_call_id": "call-1", "content": "vps-prod" }
  ],
  "tools": [ /* same schemas */ ]
}

The second turn must end with finish_reason: "stop" and text. If it asks for another
tool, that is the model doing more work, not a loop.

arguments as string or object

Clients disagree: some send function.arguments as a JSON string, some as an
object. Both are accepted and normalised before the upstream call.

This matters because forwarding an object verbatim produced a provider 400 that the
router then retried across every account, which in a dashboard reads as a full outage when
the cause was one caller’s payload shape.

tool_choice

Value Behaviour
omitted the model decides, and may answer without a tool
"auto" same as omitted
"none" the model must not call a tool
"required" the model must call some tool
{ "type": "function", "function": { "name": "read_file" } } that one tool
required is how an agent loops forever

With tool_choice: "required" the model is forbidden from answering without a tool call.
Send the tool result and it is still obliged to call a tool, so it calls another one, gets
a result, and calls another: an endless loop that produces valid 200 responses and no
visible error.

The gateway never rewrites an omitted tool_choice into required. If your agent loops,
check the client setting before the gateway.

Reasoning models and tools

A reasoning model may spend its budget thinking before calling a tool. If the cap is too
small it returns empty content with no tool call and finish_reason: "stop".

The gateway raises a cap below 256 for a model that reasons, and reports the change as
cap_raised_from in the response. Leave max_tokens out rather than setting it low.

Streaming tool calls

In a stream, one call arrives as several frames. The name appears once, on the frame that
opens the call; later frames carry arguments fragments to concatenate. See
Streaming for the exact frames.

The Anthropic path

/messages takes Anthropic tool definitions and returns Anthropic tool_use blocks with
stop_reason: "tool_use". The gateway does not translate one dialect into the other, so
Claude Code sees what it expects and an OpenAI client does too.

What is refused

Situation Answer
tools sent to a model without capabilities.tools the call still succeeds, but the model ignores them
malformed schema provider 400, surfaced as invalid_request
an arguments object alongside a string-only client accepted; normalised before sending