Tool calling
Models that support tools can answer a request by calling a function you define instead of
writing prose. On chat completions, Heimdallm uses the OpenAI shapes end to end: you send tools, the model answers
with tool_calls, you run the function and send the result back as a tool message. This page
walks the whole loop.
Coding harnesses that speak Messages or Responses use different tool-call and result shapes. Those references describe each dialect's supported function tools and limits.
The tools a request declares run in your client, never in the marketplace. A web search is such a
function, so the number of searches is the number of calls your client ran. Anthropic's hosted web search is refused on Messages
and a built-in web_search is dropped on Responses, because neither would run.
Defining tools
Each entry in tools is a function with a name, an optional description and a JSON Schema for
its arguments:
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
},
"strict": true
}
}
strict asks the endpoint to constrain generation to the schema, where it supports doing so.
tool_choice
| Value | Meaning |
|---|---|
"auto" | The model decides. Default when tools are present. |
"none" | Never call a tool. |
"required" | Must call some tool. |
{"type":"function","function":{"name":"get_weather"}} | Must call this tool. |
parallel_tool_calls: false limits the model to at most one call per turn, where the endpoint
supports the flag. The Responses API accepts the same member and forwards it
the same way. Response objects, including those in stream events, report parallel_tool_calls: true
as a placeholder rather than a record of the request.
The loop, end to end
1. Send the conversation with your tools.
{
"model": "deepseek/deepseek-v4-flash-0731-abliterated",
"messages": [{ "role": "user", "content": "What's the weather in Lyon, in celsius?" }],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string" },
"unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
},
"required": ["city"]
}
}
}
]
}
2. The model answers with a call. finish_reason is tool_calls; arguments is a JSON
string to parse:
{
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_a1b2c3",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Lyon\",\"unit\":\"celsius\"}" }
}
]
},
"finish_reason": "tool_calls"
}
]
}
3. Run the function and send the result back. Append the assistant message exactly as
received, then one tool message per call, with tool_call_id echoing the call's id:
{
"model": "deepseek/deepseek-v4-flash-0731-abliterated",
"messages": [
{ "role": "user", "content": "What's the weather in Lyon, in celsius?" },
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_a1b2c3",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Lyon\",\"unit\":\"celsius\"}" }
}
]
},
{ "role": "tool", "tool_call_id": "call_a1b2c3", "content": "{\"temp_c\": 24, \"sky\": \"clear\"}" }
],
"tools": [{ "type": "function", "function": { "name": "get_weather", "parameters": { "type": "object" } } }]
}
4. The model answers in prose (finish_reason: "stop"), or with another call — loop until it
stops. Keep sending tools on every turn: the model needs the definitions to keep using them.
Streaming tool calls
With "stream": true, calls arrive incrementally in choices[0].delta.tool_calls. Each entry
carries an index; the id and function name arrive on the first fragment for that index, and
function.arguments arrives as string fragments to concatenate in order:
data: {"...":"...","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_a1b2c3","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]}
data: {"...":"...","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":\"Ly"}}]},"finish_reason":null}]}
data: {"...":"...","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"on\"}"}}]},"finish_reason":"tool_calls"}]}
Buffer per index, and treat the chunk with finish_reason: "tool_calls" as the end of the
calls. Full frame mechanics: Streaming.
Routing to endpoints that can do this
Not every endpoint supports tools, and you do not have to arrange this yourself. An endpoint
declares what it honours in its supported_parameters (visible in the
endpoints listing), and a request carrying
tools is only ever routed to an endpoint that declares them. An endpoint that never declared
tools is not a candidate for your request, however cheap it is — the Tools column on a model page
shows which endpoints those are.
That is on by default because ignoring tools is not a degraded answer but the wrong kind of one:
you would get prose where you needed a tool call, and discover it after paying for the answer.
A demanding tool_choice — "none", "required" or a named function — is routed only to an
endpoint declaring tool_choice. "auto" restates what an endpoint declaring tools does unasked,
and parallel_tool_calls and the sampling controls are advisory: an endpoint declaring them is
preferred, and where none does the request is served without them and the reply reports them
unhonoured. See advisory settings.
Heimdallm also measures tool calling per endpoint: tool_call_success_1d in the endpoints listing
is the share of the last day's tool requests that actually produced a tool call. A day rather than
an hour, because the figure swings hour to hour and only a whole day of asks separates an endpoint
that honours tools from one that drops them. It appears once the day holds 20 counted tool requests;
below that it is null, which means too few tool requests to measure and never that none were
answered. Requests you cancelled, requests whose stream broke, and requests whose outcome was never
recorded count neither way.
A declaration is held to that measurement. An endpoint measuring below 85% has its tools promise
withheld and stops receiving tool-calling traffic, the same way a failed capability check withholds
it — your request routes to an endpoint that does honour tools, with nothing to change on your side.
It takes the traffic back on its own once it measures above the floor again.