# Function calling

Function calling lets a model ask your code to do something it cannot do itself, such as look up an order or read a database. You describe your functions. The model picks one and returns its arguments. You run the function, then send the result back so the model can write the final answer. It works on [Chat completions](/en/docs/chat), [Messages](/en/docs/messages) and [Responses](/en/docs/responses).

## Pick a model that supports tools

A model supports function calling when `tool_call` is `true` in `GET /v1/models`. The call is public, so you can filter it without a key:

```bash
curl -s https://kenari.id/v1/models | jq -r '.data[] | select(.tool_call == true) | .id'
```

kenari does not block a model that lacks the flag, but such a model may ignore your tools. The examples on this page use `step-3-7-flash:free`, which supports tools and costs nothing to call.

## The loop with Chat completions

A tool call is a short conversation: your request, the model's call, your result, the model's answer.

1. Send the user message and a `tools` array.
2. The model returns an assistant message with `tool_calls` and `finish_reason: "tool_calls"`.
3. Run each function. Append the assistant message, then one `tool` message per call, each with the matching `tool_call_id`.
4. Send the whole conversation again. The model answers in plain text.

Each tool has a `name`, a `description` that tells the model when to use it, and a JSON Schema for `parameters`. This is the model's reply to step 2:

```json
{
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\": \"Jakarta\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}
```

`arguments` is a string that contains JSON, not an object. Parse it yourself. The complete loop with the OpenAI SDK:

```python
import json
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://kenari.id/v1",
    api_key=os.environ["KENARI_API_KEY"],
)

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name, for example Jakarta"}
                },
                "required": ["city"],
            },
        },
    }
]


def get_weather(city: str) -> dict:
    return {"city": city, "temperature_c": 31, "conditions": "cloudy"}


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

for _ in range(5):  # stop a runaway loop
    response = client.chat.completions.create(
        model="step-3-7-flash:free",
        messages=messages,
        tools=tools,
    )
    message = response.choices[0].message
    if not message.tool_calls:
        print(message.content)
        break

    messages.append(
        {
            "role": "assistant",
            "content": message.content,
            "tool_calls": [call.model_dump() for call in message.tool_calls],
        }
    )
    for call in message.tool_calls:
        arguments = json.loads(call.function.arguments)
        result = get_weather(**arguments)
        messages.append(
            {
                "role": "tool",
                "tool_call_id": call.id,
                "content": json.dumps(result),
            }
        )
else:
    print("Stopped after 5 rounds: the model is still asking for tool calls.")
```

The `tool` message `content` is a string. Send JSON as text, or a plain error message if the function failed, so the model can recover.

## The loop with Messages

On `/v1/messages` a tool is `{name, description, input_schema}`. The model returns `tool_use` blocks and `stop_reason: "tool_use"`. You answer with `tool_result` blocks in the next `user` message.

Point the Anthropic SDK at `https://kenari.id`, without `/v1`. The SDK adds the path itself.

```python
import json
import os

import anthropic

client = anthropic.Anthropic(
    base_url="https://kenari.id",
    api_key=os.environ["KENARI_API_KEY"],
)

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 is the weather in Jakarta?"}]

for _ in range(5):
    response = client.messages.create(
        model="step-3-7-flash:free",
        max_tokens=2048,
        tools=tools,
        messages=messages,
    )
    if response.stop_reason != "tool_use":
        print("".join(block.text for block in response.content if block.type == "text"))
        break

    messages.append({"role": "assistant", "content": response.content})
    results = []
    for block in response.content:
        if block.type == "tool_use":
            output = {"city": block.input["city"], "temperature_c": 31}
            results.append(
                {
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(output),
                }
            )
    messages.append({"role": "user", "content": results})
else:
    print("Stopped after 5 rounds: the model is still asking for tool calls.")
```

On `/v1/responses` the model returns `function_call` items and you reply with `function_call_output` items that carry the same `call_id`. Tools are declared flat, as `{"type": "function", "name": ..., "parameters": ...}`. See [Responses](/en/docs/responses).

## Control when the model calls a tool

`tool_choice` decides whether and which tool the model calls. Without it the model decides.

| Chat completions | Messages | Effect |
| --- | --- | --- |
| `"auto"` | `{"type": "auto"}` | The model decides. |
| `"required"` | `{"type": "any"}` | The model must call at least one tool. |
| `{"type": "function", "function": {"name": "get_weather"}}` | `{"type": "tool", "name": "get_weather"}` | The model must call that tool. |
| `"none"` | `{"type": "none"}` | The model answers without calling a tool. |

A forced choice without a name is rejected with `400`. Unknown shapes are handled per API format. Chat completions treats a shape it does not know, such as `allowed_tools`, as `"auto"`. Messages and Responses reject an unknown `tool_choice` type with `400`.

## Parallel tool calls

A model can return several calls in one reply: several entries in `tool_calls`, or several `tool_use` blocks. Run all of them, then answer every call. On Chat completions that is one `tool` message per `tool_call_id`. On Messages it is every `tool_result` block in a single `user` message.

kenari forwards a `parallel_tool_calls` field only to models served through an OpenAI-style API and drops it for the rest. It is also dropped whenever the request lists a `kenari:` server tool. Do not rely on it to prevent parallel calls. Write your loop so it handles one call or many.

## Streaming tool calls

With `"stream": true`, a call arrives in pieces that you join before parsing. See [Streaming](/en/docs/streaming) for the fields and events of each API format.

## Tools that kenari runs for you

A tool with the type `kenari:<name>`, for example `{"type": "kenari:web_search"}`, runs on kenari. You do not write the loop: the model calls it, kenari runs it and continues the answer. You can mix server tools with your own functions in one `tools` array. See [Server tools](/en/docs/server-tools).

## Limits and pitfalls

- Send the same `tools` array on every request of the loop, including the one that returns the final answer.
- Define every tool that your code runs as a `function` tool. Apart from `kenari:` server tools, a tool type that has no function definition is not run by kenari, so do not depend on it.
- The model can produce invalid JSON in `arguments`, or a value outside your schema. Validate before you run anything with side effects.
- A request that declares tools, or a conversation that already holds tool calls and results, is only sent to providers that can carry them, so your tools and results are not silently dropped.

## See also

- [Server tools](/en/docs/server-tools)
- [Structured output](/en/docs/structured-output)
- [Streaming](/en/docs/streaming)
- [Models and pricing](/en/docs/models)
