Tool calling
The tools and tool_choice request shape your OpenAI client already emits.
Tool calling uses the OpenAI request and response shape unchanged. If your code already calls tools against another OpenAI-compatible provider, it works here after the base URL, key, and model change.
Every model in the catalog supports tool calling. Check
capabilities.tool_calling on GET /v1/models rather than
assuming it.
Making a call
The assistant message comes back with tool_calls, and finish_reason is
tool_calls:
{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Lagos\"}" }
}
]
},
"finish_reason": "tool_calls"
}arguments is a JSON string, not an object — that is the OpenAI shape, and
Atlas does not change it. Parse it defensively: a model can emit arguments that
are not valid JSON, and that is a model outcome rather than an API error.
Returning a result
Append the assistant message verbatim, then one tool message per call, keyed
by tool_call_id:
messages.append(completion.choices[0].message)
messages.append(
{
"role": "tool",
"tool_call_id": call.id,
"content": '{"temp_c": 31, "conditions": "humid"}',
}
)
follow_up = client.chat.completions.create(
model="atlas-mid-1", messages=messages, tools=tools
)tool_choice
| Value | Meaning |
|---|---|
"auto" | The model decides. |
"none" | Tool definitions stay in context; no call is made. |
"required" | The model must call some tool. |
{"type": "function", "function": {"name": "..."}} | The model must call that tool. |
tool_choice: "none" with tools attached is not treated as a tool-calling
request. It is the shape a client sends when it wants the definitions in
context but no call, so it is served even by a model without tool-calling
support — refusing it would refuse a request the model can answer exactly as
asked.
Streaming tool calls
Tool calls stream as fragments in delta.tool_calls, each carrying an index.
Accumulate by index — the name arrives in one or more fragments and the
arguments string is built up across many:
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_abc123","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"ci"}}]}}]}
{"choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"ty\":\"Lagos\"}"}}]}}]}The OpenAI SDKs' accumulation helpers handle this for you. If you assemble the
stream yourself, do not attempt to parse arguments until finish_reason
arrives.
Refusals
You sent tools (or a tool_choice other than "none") to a model whose
capabilities.tool_calling is false.
This is refused before admission, so it costs you no tokens and does not
count against your rate limit. Atlas refuses rather than silently dropping
the field: a caller whose code branches on tool_calls and receives prose
instead has a much harder debugging session than one who gets a 400.
This release serves one choice per request. Send n as 1 or omit it,
and issue repeated requests if you need several candidates.
More than one choice would multiply tool-call assembly and the streaming
counters against a metering path that reads choices[0] — accepting it
would under-bill and mis-assemble at the same time, so it is refused rather
than half-supported.
Combining with structured output
Tool parameter schemas and response_format schemas are not validated the
same way. A tool's parameters document is passed through to the backend as
you send it. Only response_format: {"type": "json_schema"} is checked against
the published JSON Schema subset.