---
title: Tools
description: Function calling across the gateway's three dialects — declaring a tool, the call-and-result round, and why tool support is proven per model.
keywords: tools, function calling, tool_choice, tool_use, function_call
group: gateway
---

## Quick {#quick keywords="tools, declaration, function, parameters"}

Declare a tool in `tools` and the model can ask for it to be called instead of answering with text. The description is the only thing that tells the model when to call it.

```json title=Declaration
{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Returns the current weather in the named city.",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string", "description": "The city, for example Kazan." },
            "units": { "type": "string", "enum": ["celsius", "fahrenheit"] }
          },
          "required": ["city"],
          "additionalProperties": false
        }
      }
    }
  ]
}
```

:::note
`parameters` is **your own JSON Schema**, on all three dialects. Its vocabulary is JSON Schema's and is not narrowed here: nested objects, arrays with `items`, `enum`, `const`, `anyOf`, `format`, `pattern`, numeric bounds and `$schema` all travel to the model exactly as you wrote them. Two things are judged — that the value is a JSON object, and that it is at most 65536 bytes when encoded. Everything else is between you and the model. A schema the upstream dislikes is refused by the upstream, and you are told the request failed there.
:::

## The whole round {#round keywords="tool_calls, tool_call_id, tool role, result"}

A round consists of three turns, and all of its mechanics amount to replaying back what the model said.

:::steps
- **You ask** — An ordinary request with `tools` added to it.
- **The model asks for a call** — The reply arrives with `finish_reason: "tool_calls"`, `content` is `null`, and `message.tool_calls` carries the calls: each with its own `id`, a name, and `arguments` as JSON text.
- **You answer with the result** — Send the conversation again: the same assistant turn with its `tool_calls`, followed by a turn with the `tool` role carrying that call's `tool_call_id` and the result in `content`.
:::

```json title=Response
{
  "id": "chatcmpl-2b90c1f7ad",
  "object": "chat.completion",
  "created": 1756900000,
  "model": "<model>",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_a1",
            "type": "function",
            "function": { "name": "get_weather", "arguments": "{\"city\":\"Kazan\",\"units\":\"celsius\"}" }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}
```

```python title=Python
import json
import os

from openai import OpenAI

client = OpenAI(base_url="https://api.kumorouter.com/v1", api_key=os.environ["KUMO_API_KEY"])

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Returns the current weather in the named city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "The city, for example Kazan."},
                    "units": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                },
                "required": ["city"],
                "additionalProperties": False,
            },
        },
    }
]

messages = [{"role": "user", "content": "What is the weather in Kazan right now?"}]

first = client.chat.completions.create(
    model="<model>", max_tokens=256, tools=tools, messages=messages
)
call = first.choices[0].message.tool_calls[0]

# This is where you really fetch the weather; the result goes back as text.
result = json.dumps({"temperature": 12, "units": "celsius"})

messages.append(
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [
            {
                "id": call.id,
                "type": "function",
                "function": {"name": call.function.name, "arguments": call.function.arguments},
            }
        ],
    }
)
messages.append({"role": "tool", "tool_call_id": call.id, "content": result})

second = client.chat.completions.create(
    model="<model>", max_tokens=256, tools=tools, messages=messages
)
print(second.choices[0].message.content)
```

`arguments` arrive as text rather than a parsed object: their shape is your own schema, so they cross the boundary as text. Parse them yourself, and validate them before you execute anything.

`tool_call_id` is required on a `tool` turn and refused on every other: a supplier matches results to calls by it rather than by position.

## Forcing a call {#choice keywords="tool_choice, requirement, named tool"}

`tool_choice` states what a request requires of its tool list, and Chat Completions and Responses accept both spellings their protocol defines for it. The bare string is one of three modes: `auto` leaves the choice to the model, `none` forbids a call while the declarations stay visible to it, and `required` requires a call to some declared tool. The object form requires a call to the one tool it names, and that name must be declared by the same request. `required` with no tool declared is refused, because no answer could satisfy it. Omit the member to leave the choice to the model, which is what `auto` asks for explicitly.

```json title=Request
{ "tool_choice": { "type": "function", "function": { "name": "get_weather" } } }
```

Messages additionally accepts the native object modes below. They are request instructions, not ignored compatibility fields.

| Native `tool_choice` | Meaning |
| --- | --- |
| `{"type": "auto"}` | Let the model choose whether to call a tool. |
| `{"type": "any"}` | Require a call to some declared tool. |
| `{"type": "none"}` | Forbid a tool call while keeping declarations visible. |
| `{"type": "tool", "name": "get_weather"}` | Require this declared tool. |

`name` is required only for `tool` and rejected for the other three modes. An omitted `tool_choice` stays absent. Model capability checks still apply.

## The same on the other dialects {#dialects keywords="responses, messages, input_schema, function_call"}

:::matrix
| What | Chat Completions | Responses | Messages |
| --- | --- | --- | --- |
| Declaration | `tools[].function` with `parameters` | flat: `type`, `name`, `parameters` | `name` and `input_schema` |
| Parameter schema | your own JSON Schema | your own JSON Schema | your own JSON Schema |
| Forcing a call | `"auto"`, `"none"`, `"required"`, or `tool_choice.type: "function"` with the name inside `function` | the same three strings, or `tool_choice.type: "function"` with the name beside it | `tool_choice.type: "tool"`, name beside it |
| The call in the reply | `message.tool_calls` | a `function_call` item | a `tool_use` block |
| The result back | a turn with the `tool` role and `tool_call_id` | a `function_call_output` item with `call_id` | a `tool_result` block with `tool_use_id` |
:::

On the Messages dialect a call's arguments arrive as an object in `input` rather than as text; in a stream they are assembled from `input_json_delta` fragments. The Responses declaration carries a `strict` member that is accepted and not forwarded: this surface proves strict schemas only for structured output.

Parallel calls are promised nowhere. On Responses, `parallel_tool_calls` is accepted and not forwarded; the other two dialects have no such member at all. Every model family decides it for itself, so write code that survives one call and several equally well.

## Tools are proven per model {#proof keywords="catalog, tools_status, proven, unproven"}

:::note
Tool support is a proven capability of an exact model and channel, not a property of the protocol. The catalog keeps a `tools_status` for each such pair: `proven`, `unproven` or `unsupported`. A request carrying `tools` will not be routed to a pair that has not proven them — it is refused before anything upstream is contacted, and "unproven" never means "probably supported". What a given model has proven is on its card in the catalog.
:::

## Next {#next keywords="structured output, streaming, errors"}

:::cards
- [Structured output](/en/structured-output) — when you want an answer to a schema rather than a call.
- [Streaming](/en/streaming) — how calls are assembled from fragments in a stream.
- [Chat Completions](/en/chat-completions) — the request members and the reply shape.
- [Messages](/en/messages) — `input_schema` and this dialect's blocks.
- [Model catalog](/en/models) — what each model has proven.
:::
