Tool calling with open models on an OpenAI-compatible API

Function calling with open models. Define tools, control tool_choice, handle parallel calls and send the results back with the OpenAI or Anthropic SDK.

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

pip install openai
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

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

tool_choiceWhat 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.
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

pip install anthropic
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

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.

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

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

RequestResult
A non-function tool on Chat Completions400: 'tools' is not supported: tool type 'web_search' is not available; only 'function' tools
A server tool on Messages400: 'tools' is not supported: only function tools are available
A hosted web search tool on ResponsesRemoved from the request. The model answers without a search.
tool_choice: "required" or a named tool, with no tools400: tool_choice requires tools.
A named tool that is not in tools400: tool_choice names no tool in tools.
functions or function_call400. Use tools and tool_choice.
A forced tool_choice on google/gemma-4-12b400: 'tool_choice' is not supported: this model cannot force a tool call. Send auto or none.

See the chat reference and the Messages reference.