# Tool calling with open models on an OpenAI-compatible API
URL: https://docs.munito.ai/tool-calling

Open models on Munito AI can call your functions through the OpenAI-compatible and Anthropic-compatible APIs. You define each tool with a JSON schema, the model returns the calls, your code runs them, and you send the results back.
Tool calling works on Chat Completions, Responses and Messages, with the OpenAI and Anthropic SDKs.

The OpenAI SDK uses the base URL `https://api.munito.ai/inference/v1`, and the Anthropic SDK uses `https://api.munito.ai/inference`. Both read the key from `MUNITO_API_KEY`.
All public chat models support tools: `qwen/qwen3.8-27b`, `zai-org/glm-5.3-flash`, `deepseek-ai/deepseek-v4-flash`, `deepseek-ai/deepseek-v4.1-flash-1017`, `aleph-alpha/kolibri-1` and `google/gemma-4-12b`.
`google/gemma-4-12b` calls a tool only when it decides to, so a `tool_choice` that forces a call returns `400`.

## The tool loop on Chat Completions [#the-tool-loop-on-chat-completions]

```bash
pip install openai
```

```python
import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.munito.ai/inference/v1",
    api_key=os.environ["MUNITO_API_KEY"],
)
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current temperature in a city, in degrees Celsius.",
            "parameters": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
    }
]


def get_weather(city: str) -> dict:
    return {"city": city, "celsius": {"Paris": 14, "Berlin": 9}.get(city, 12)}


messages = [{"role": "user", "content": "What is the temperature in Paris and in Berlin?"}]
while True:
    response = client.chat.completions.create(
        model="qwen/qwen3.8-27b",
        messages=messages,
        tools=tools,
        max_tokens=2048,
    )
    choice = response.choices[0]
    if choice.finish_reason != "tool_calls":
        print(choice.message.content)
        break
    messages.append(choice.message.model_dump(exclude_none=True))
    for call in choice.message.tool_calls:
        result = get_weather(**json.loads(call.function.arguments))
        messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
```

When the model calls a tool, `finish_reason` is `tool_calls` and `message.tool_calls` holds the calls.
Add the assistant message to the conversation, then add one `tool` message for each call, with the `tool_call_id` of that call.

## Parallel calls [#parallel-calls]

The model can return more than one call in one turn, as in the sample above. Run them all, and send all the results in the next request.
Set `parallel_tool_calls: false` to ask for at most one call in each turn.

## Choose when the model calls a tool [#choose-when-the-model-calls-a-tool]

| `tool_choice`                                               | What the model does                                  |
| ----------------------------------------------------------- | ---------------------------------------------------- |
| `"auto"`                                                    | It decides. This is the default when you send tools. |
| `"none"`                                                    | It answers with text and calls no tool.              |
| `"required"`                                                | It calls one or more tools.                          |
| `{"type": "function", "function": {"name": "get_weather"}}` | It calls this tool.                                  |

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.munito.ai/inference/v1",
    api_key=os.environ["MUNITO_API_KEY"],
)
response = client.chat.completions.create(
    model="qwen/qwen3.8-27b",
    messages=[{"role": "user", "content": "I am going to Lisbon tomorrow."}],
    tools=[
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get the current temperature in a city.",
                "parameters": {
                    "type": "object",
                    "properties": {"city": {"type": "string"}},
                    "required": ["city"],
                },
            },
        }
    ],
    tool_choice={"type": "function", "function": {"name": "get_weather"}},
    max_tokens=2048,
)
call = response.choices[0].message.tool_calls[0]
print(response.choices[0].finish_reason, call.function.name, call.function.arguments)
```

A call that the model makes because of a named `tool_choice` also ends with `finish_reason: "tool_calls"`.

## The tool loop on Messages [#the-tool-loop-on-messages]

```bash
pip install anthropic
```

```python
import json
import os
from anthropic import Anthropic

client = Anthropic(
    base_url="https://api.munito.ai/inference",
    api_key=os.environ["MUNITO_API_KEY"],
)
tools = [
    {
        "name": "get_weather",
        "description": "Get the current temperature in a city, in degrees Celsius.",
        "input_schema": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    }
]


def get_weather(city: str) -> dict:
    return {"city": city, "celsius": {"Paris": 14, "Berlin": 9}.get(city, 12)}


messages = [{"role": "user", "content": "What is the temperature in Paris?"}]
while True:
    message = client.messages.create(
        model="qwen/qwen3.8-27b",
        max_tokens=2048,
        tools=tools,
        messages=messages,
    )
    if message.stop_reason != "tool_use":
        print(next(block.text for block in message.content if block.type == "text"))
        break
    messages.append({"role": "assistant", "content": message.content})
    results = []
    for block in message.content:
        if block.type == "tool_use":
            output = get_weather(**block.input)
            results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(output)})
    messages.append({"role": "user", "content": results})
```

On Messages, a turn that calls a tool ends with `stop_reason: "tool_use"`, also when `tool_choice` forces the tool.
`tool_choice` takes `{"type": "auto"}`, `{"type": "any"}`, `{"type": "tool", "name": "get_weather"}` or `{"type": "none"}`.

## Responses [#responses]

On Responses, a tool has `name`, `description` and `parameters` at the top level.
A call comes back as a `function_call` item in `output`. Send each result as a `function_call_output` item with the `call_id` of the call.

```python
import json
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.munito.ai/inference/v1",
    api_key=os.environ["MUNITO_API_KEY"],
)
tools = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "Get the current temperature in a city, in degrees Celsius.",
        "parameters": {
            "type": "object",
            "properties": {"city": {"type": "string"}},
            "required": ["city"],
        },
    }
]
response = client.responses.create(
    model="qwen/qwen3.8-27b",
    input="What is the temperature in Paris?",
    tools=tools,
    store=True,
    max_output_tokens=2048,
)
outputs = [
    {"type": "function_call_output", "call_id": item.call_id, "output": json.dumps({"celsius": 14})}
    for item in response.output
    if item.type == "function_call"
]
final = client.responses.create(
    model="qwen/qwen3.8-27b",
    previous_response_id=response.id,
    input=outputs,
    tools=tools,
    store=True,
    max_output_tokens=2048,
)
print(final.output_text)
```

Munito AI keeps no response unless you send `store: true`, so `previous_response_id` works only with a stored response.
Without it, the next request returns `400` with `previous response resp_... was not stored. Send store: true to continue a response.`
To keep nothing, send the whole conversation in `input` on each turn.

## What is not available [#what-is-not-available]

Munito AI runs only your own function tools. It runs no hosted tools, such as web search, file search or a code interpreter.

| Request                                                  | Result                                                                                            |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| A non-function tool on Chat Completions                  | `400`: `'tools' is not supported: tool type 'web_search' is not available; only 'function' tools` |
| A server tool on Messages                                | `400`: `'tools' is not supported: only function tools are available`                              |
| A hosted web search tool on Responses                    | Removed from the request. The model answers without a search.                                     |
| `tool_choice: "required"` or a named tool, with no tools | `400`: `tool_choice requires tools.`                                                              |
| A named tool that is not in `tools`                      | `400`: `tool_choice names no tool in tools.`                                                      |
| `functions` or `function_call`                           | `400`. Use `tools` and `tool_choice`.                                                             |
| A forced `tool_choice` on `google/gemma-4-12b`           | `400`: `'tool_choice' is not supported: this model cannot force a tool call. Send auto or none.`  |

See the [chat reference](https://docs.munito.ai/api/createChatCompletion) and the [Messages reference](https://docs.munito.ai/api/createMessage).
