Tool calling
One tool schema across vendors, translated on the way out and back.
One schema
Declare tools in OpenAI’s format — tools as an array of { type: "function", function: { name, description, parameters } }, plus an optional tool_choice — and the same body works on every model in the catalogue that advertises the tools capability.
{
"model": "anthropic/claude-sonnet-5",
"messages": [{"role": "user", "content": "What is the weather in Lisbon?"}],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current conditions for a city.",
"parameters": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
}
}
],
"tool_choice": "auto"
}Which models support it is a field on the catalogue, not a guess: capabilities.tools on each row of GET /models.
What translation actually does
For OpenAI, Groq, Together and DeepInfra there is nothing to translate — they all serve /chat/completions with the same body, so the tools block goes upstream verbatim.
Anthropic does not use that format, so the adapter converts in both directions: your function.parameters becomes an input_schema, and the tool_use blocks that come back become OpenAI tool_calls with the arguments re-serialised as a JSON string. From your side the response is the same shape whichever vendor produced it:
All four tool_choice values are mapped, "none" included — it becomes the Messages API’s own {type:"none"} rather than being omitted, which Anthropic would have read as "auto". A named function maps to {type:"tool", name} and "required" to {type:"any"}.
"message": {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "toolu_…",
"type": "function",
"function": { "name": "get_weather", "arguments": "{\"city\":\"Lisbon\"}" }
}
]
}The loop
Unchanged from any OpenAI client: append the assistant message with its tool_calls, then one role: "tool" message per call carrying the result, then send the whole conversation back. Tool result messages are converted into Anthropic tool_result blocks on the way out, so a transcript built against OpenAI replays correctly against Claude.
Billing and streaming
A turn that only calls tools has no text in it, and it is charged normally — it is the expected shape of a tool-using request, not an empty answer. The zero-completion waiver explicitly checks for tool calls before deciding a response was worthless.
On a stream, tool-call deltas are passed through in whatever shape the provider emitted, so an SDK accumulator assembles them the way it already does.
Something here disagrees with what the API actually did? That is a bug in this page, and worth reporting.
Report it