Skip to content
kenari.

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, Messages and Responses.

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:

Terminal window
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.

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:

{
"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:

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.

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.

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.

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

Chat completionsMessagesEffect
"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.

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.

With "stream": true, a call arrives in pieces that you join before parsing. See Streaming for the fields and events of each API format.

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.

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