LLM Function Calling Across Providers: Tool Definition Formats, Execution Quirks, and What Breaks in Multi-Provider Routing (2026)
A cross-provider comparison of function calling (tool use) formats across OpenAI, Anthropic, DashScope, DeepSeek, and SiliconFlow. We cover tool definition schemas, response formats, parallel tool calls, streaming tool chunks, strict mode, and what a gateway needs to normalize across all of them.
Function calling — or "tool use," depending on which provider you ask — is the mechanism that turns an LLM from a text generator into an agent that can query databases, hit APIs, and trigger workflows. Every major provider supports it. None of them agree on the format.
We route function-calling traffic across OpenAI, Anthropic, DashScope, DeepSeek, and SiliconFlow daily. This post is the reference we wished existed when we first had to normalize tool calls across all of them: the exact schema differences, the response format mismatches, the streaming quirks, and the practical gotchas that break multi-provider routing.
OpenAI-compatible means a provider exposes a chat-completions endpoint whose request and response shape matches the OpenAI API contract closely enough that an unmodified OpenAI SDK call works against it after swapping three values: API key, base URL, and model name. The minimum surface in practice is POST /v1/chat/completions with messages, model, and an OpenAI-shaped streaming response.
TL;DR Comparison Table
| Dimension | OpenAI | Anthropic Claude | DashScope (Qwen) | DeepSeek | SiliconFlow |
|---|---|---|---|---|---|
| Terminology | Function calling | Tool use | Function calling | Tool calls | Function calling |
| Request field | tools array | tools array | tools array | tools array | tools array |
| Schema key | parameters | input_schema | parameters | parameters | parameters |
| Response format | tool_calls on assistant message | tool_use content blocks | tool_calls on assistant message | tool_calls on assistant message | tool_calls on assistant message |
| Arguments type | JSON string | Parsed object | JSON string | JSON string | JSON string |
| Parallel calls | Yes | Yes | Yes | Yes (non-thinking) | Yes |
| Strict mode | Yes (strict: true) | No | No | Yes (Beta, /beta endpoint) | No |
| Streaming tool chunks | tool_calls delta chunks | content_block_delta with input_json_delta | tool_calls delta chunks | tool_calls delta chunks | tool_calls delta chunks |
| Tool choice | auto / required / none / named | auto / any / tool (named) | auto / required / none / named | auto / required / none / named | auto / required / none / named |
| Max tools | No hard limit (practical: ~128) | 64+ | Model-dependent | Model-dependent | Model-dependent |
Tool Definition Schemas: The First Divergence Point
The core idea is the same everywhere: you describe a function with a name, a description, and a JSON Schema for its parameters. The structural differences start at the schema key name.
OpenAI wraps the function definition inside a tools array entry with type: "function":
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get current weather for a city",
"parameters": {
"type": "object",
"properties": {
"location": { "type": "string" }
},
"required": ["location"],
"additionalProperties": false
}
}
}
Anthropic uses the same top-level structure but replaces parameters with input_schema and drops the function wrapper:
{
"name": "get_weather",
"description": "Get current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"location": { "type": "string" }
},
"required": ["location"]
}
}
DashScope (Qwen) and DeepSeek both follow the OpenAI format exactly — type: "function", nested function object, parameters key. This is the benefit of OpenAI-compatible APIs: if your code works with OpenAI, it works with DashScope and DeepSeek with zero tool-definition changes.
SiliconFlow also follows the OpenAI-compatible format for tool definitions.
The practical takeaway: if you're building a gateway that normalizes tool definitions, you have two formats to support — OpenAI-style (used by OpenAI, DashScope, DeepSeek, SiliconFlow) and Anthropic-style (unique to Anthropic). The mapping is straightforward: rename parameters to input_schema, flatten the function wrapper, and you're done.
Response Formats: Where Things Actually Diverge
Tool definitions are easy. Response formats are where multi-provider routing gets interesting.
OpenAI / DashScope / DeepSeek / SiliconFlow (OpenAI-compatible)
When the model decides to call a tool, the response comes back as an assistant message with a tool_calls array:
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\": \"Tokyo\"}"
}
}
]
}
Key detail: arguments is a JSON string, not a parsed object. You need to JSON.parse() it before using the values. This trips up developers coming from Anthropic.
You return results with a tool role message referencing the tool_call_id:
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "{\"temperature\": 24, \"unit\": \"celsius\"}"
}
Anthropic Claude
Anthropic uses a content-block architecture. Tool calls and text appear as separate blocks within a single assistant response:
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Let me check the weather for you."
},
{
"type": "tool_use",
"id": "toolu_01abc",
"name": "get_weather",
"input": { "location": "Tokyo" }
}
],
"stop_reason": "tool_use"
}
Key differences from OpenAI format:
- Arguments are a parsed object (
input), not a JSON string (arguments) - Tool calls live inside
contentblocks, not a separatetool_callsarray - The
stop_reasonis"tool_use", not"tool_calls"(OpenAI usesfinish_reason: "tool_calls") - Text and tool calls can be interleaved in the same response — the model might explain its reasoning before calling the tool
You return results as a user message with tool_result content blocks:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01abc",
"content": "{\"temperature\": 24, \"unit\": \"celsius\"}"
}
]
}
This is fundamentally different from OpenAI's role: "tool" approach. In Anthropic's model, tool results are sent as part of the user turn.
Parallel vs Sequential Tool Calls
All major providers support parallel tool calls — the model can request multiple tool invocations in a single response. But the implementation semantics differ.
OpenAI returns multiple entries in the tool_calls array. The model expects you to execute all of them and return all results before the next turn:
{
"tool_calls": [
{ "id": "call_1", "function": { "name": "get_weather", "arguments": "{\"location\": \"Tokyo\"}" } },
{ "id": "call_2", "function": { "name": "get_weather", "arguments": "{\"location\": \"Paris\"}" } }
]
}
You can use parallel_tool_calls: false in the request to force sequential calling.
Anthropic returns multiple tool_use blocks in the content array. Same concept, different structure:
{
"content": [
{ "type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": { "location": "Tokyo" } },
{ "type": "tool_use", "id": "toolu_2", "name": "get_weather", "input": { "location": "Paris" } }
]
}
Anthropic also supports tool_choice with disable_parallel_tool_use: true.
DeepSeek supports parallel tool calls in non-thinking mode. In thinking mode (as of V3.2+), tool calling is supported but parallel behavior may differ — the model tends to issue sequential calls when reasoning is active.
DashScope follows OpenAI's parallel tool call format. Qwen models support multiple tool_calls entries in a single response.
Streaming Tool Call Chunks
Streaming function calls introduces another layer of format divergence. When streaming is enabled, providers send partial tool call data as incremental chunks.
OpenAI / DashScope / DeepSeek / SiliconFlow stream tool calls as delta objects:
{
"delta": {
"tool_calls": [
{
"index": 0,
"id": "call_abc",
"type": "function",
"function": { "name": "get_weather", "arguments": "" }
}
]
}
}
Followed by argument fragments:
{
"delta": {
"tool_calls": [
{
"index": 0,
"function": { "arguments": "{\"loc" }
}
]
}
}
You accumulate the arguments string across chunks and parse the complete JSON when the stream ends.
Anthropic uses a different streaming event model entirely:
content_block_startwithtype: "tool_use", the toolid, andnamecontent_block_deltaevents withtype: "input_json_delta"carrying argument fragmentscontent_block_stopmarking the end of that tool call
{"type": "content_block_start", "content_block": {"type": "tool_use", "id": "toolu_01", "name": "get_weather", "input": {}}}
{"type": "content_block_delta", "delta": {"type": "input_json_delta", "partial_json": "{\"location\":"}}
{"type": "content_block_delta", "delta": {"type": "input_json_delta", "partial_json": " \"Tokyo\"}"}}
{"type": "content_block_stop"}
For gateway implementations, this is the hardest part to normalize. The OpenAI-compatible providers all use the same chunk format with delta.tool_calls[index].function.arguments, but Anthropic's event-driven model requires a completely different parser.
Strict Mode and JSON Schema Validation
Strict mode guarantees that the model's tool call arguments conform exactly to the declared JSON Schema. Not all providers support it.
OpenAI supports strict mode by adding "strict": true to the function definition. When enabled, the model is guaranteed to produce arguments that validate against the schema. All properties must be required, and additionalProperties must be false.
DeepSeek offers strict mode as a Beta feature. You need to use the /beta base URL (https://api.deepseek.com/beta) and set "strict": true on each function. DeepSeek's strict mode supports pattern, format, minimum/maximum, enum, and anyOf constraints.
Anthropic, DashScope, and SiliconFlow do not currently offer a strict mode equivalent. The models generally produce well-formed arguments, but there's no server-side guarantee. You should validate arguments in your application code.
Tool Choice Control
All providers let you control whether the model should call tools, and which ones.
| Provider | Force tool use | Prevent tool use | Specific tool | Auto (default) |
|---|---|---|---|---|
| OpenAI | tool_choice: "required" | tool_choice: "none" | tool_choice: {"type": "function", "function": {"name": "X"}} | tool_choice: "auto" |
| Anthropic | tool_choice: {"type": "any"} | tool_choice: {"type": "none"} | tool_choice: {"type": "tool", "name": "X"} | tool_choice: {"type": "auto"} |
| DashScope | tool_choice: "required" | tool_choice: "none" | tool_choice: {"type": "function", "function": {"name": "X"}} | tool_choice: "auto" |
| DeepSeek | tool_choice: "required" | tool_choice: "none" | tool_choice: {"type": "function", "function": {"name": "X"}} | tool_choice: "auto" |
| SiliconFlow | tool_choice: "required" | tool_choice: "none" | tool_choice: {"type": "function", "function": {"name": "X"}} | tool_choice: "auto" |
Anthropic's tool_choice format is structurally different — it uses {"type": "any"} instead of "required", and named tool selection uses {"type": "tool", "name": "X"} instead of the nested function object.
GPT-5.6 Sol Programmatic Tool Calling
OpenAI's GPT-5.6 Sol introduced a significant evolution: programmatic tool calling. Instead of the model choosing when to call tools and producing JSON arguments, Sol can generate and execute JavaScript code in a sandboxed V8 environment that orchestrates tool calls programmatically.
This represents a different paradigm from traditional function calling. Rather than the model emitting a single get_weather({"location": "Tokyo"}) call, it can write a program that calls multiple tools in sequence, handles errors, and transforms data — all within a single turn.
No other provider has shipped an equivalent feature. For multi-provider routing, traditional function calling remains the universal interface.
Gateway Normalization: What a Router Needs to Handle
If you're routing function-calling requests across multiple providers — which is what we do at TheRouter — here's what the normalization layer needs to handle:
Inbound (client → gateway → provider):
- Tool definition format: Map
parameters↔input_schemaand flatten/wrap thefunctionobject for Anthropic - Tool choice format: Translate
"required"↔{"type": "any"}and named tool formats - Request structure: OpenAI uses
messageswithtools; Anthropic usesmessageswithtoolsbut different message roles for tool results
Outbound (provider → gateway → client):
- Response format: Map Anthropic's
tool_usecontent blocks → OpenAI-styletool_callsarray - Arguments parsing: Anthropic returns parsed objects; OpenAI-compatible providers return JSON strings
- Stop reason: Map
stop_reason: "tool_use"→finish_reason: "tool_calls" - Streaming events: Translate Anthropic's
content_block_start/content_block_deltaevents → OpenAI-styledelta.tool_callschunks
Tool result submission:
- Role mapping: Anthropic expects
role: "user"withtool_resultblocks; OpenAI usesrole: "tool"withtool_call_id
The OpenAI-compatible providers (DashScope, DeepSeek, SiliconFlow) are the easy case — they all follow the same format, so routing between them requires zero normalization on the function calling layer.
Code Example: Minimal Multi-Provider Tool Calling Client
Here's a TypeScript example that handles tool calls from both OpenAI-compatible and Anthropic providers:
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
// Tool definition — same logic, two formats
const openaiTool: OpenAI.ChatCompletionTool = {
type: "function",
function: {
name: "get_weather",
description: "Get weather for a city",
parameters: {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"],
additionalProperties: false
}
}
};
const anthropicTool: Anthropic.Tool = {
name: "get_weather",
description: "Get weather for a city",
input_schema: {
type: "object" as const,
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}
};
// Execute the tool (same for all providers)
function executeGetWeather(location: string): string {
return JSON.stringify({ temperature: 24, unit: "celsius", location });
}
// Parse tool calls from OpenAI-compatible response
function parseOpenAIToolCalls(message: OpenAI.ChatCompletionMessage) {
return (message.tool_calls ?? []).map(tc => ({
id: tc.id,
name: tc.function.name,
args: JSON.parse(tc.function.arguments) // JSON string → object
}));
}
// Parse tool calls from Anthropic response
function parseAnthropicToolCalls(response: Anthropic.Message) {
return response.content
.filter((b): b is Anthropic.ToolUseBlock => b.type === "tool_use")
.map(b => ({
id: b.id,
name: b.name,
args: b.input as Record<string, unknown> // Already an object
}));
}
The key difference in the parsing layer: OpenAI gives you JSON.parse(tc.function.arguments), while Anthropic gives you b.input directly as an object. If you forget to parse OpenAI's string, you'll get "location" as a raw string instead of the value. If you try to parse Anthropic's object, you'll get a runtime error.
Decision Matrix: Pick X if You Need Y
| You need... | Best option | Why |
|---|---|---|
| Strict schema validation | OpenAI or DeepSeek (Beta) | Only providers with server-side strict: true support |
| Interleaved text + tool calls | Anthropic | Content-block architecture natively supports it |
| OpenAI SDK compatibility | DashScope, DeepSeek, SiliconFlow | Zero code changes for tool definitions and responses |
| Maximum tool count | OpenAI | Practical limit ~128+; tool_search for larger registries |
| Budget function calling | DeepSeek or SiliconFlow | Cheapest per-token pricing with full tool call support |
| Thinking + tool calling | DeepSeek V3.2+ or Qwen3 | Both support tool calls in reasoning mode |
| Multi-provider fallback | TheRouter | Normalizes tool call formats across all providers |
Gotchas and Known Issues
-
Double-JSON encoding: Some OpenAI-compatible clients double-encode tool call arguments when forwarding through proxies. DashScope rejects these — a known issue reported by multiple client libraries.
-
Tool call ID formats: OpenAI uses
call_prefix, Anthropic usestoolu_prefix, DeepSeek usescall_prefix. If your system stores or references these IDs, don't assume a universal format. -
Empty
contenton tool call messages: When OpenAI-compatible models return tool calls,contentis typicallynull. Some client libraries choke onnullcontent — always handle it. -
DashScope GLM models: When using GLM models through DashScope, you must include
extra_body={"tool_stream": True}in your request. Without it, the model will not returntool_callsat all. -
DeepSeek thinking mode limitations: While DeepSeek V3.2+ supports tool calls in thinking mode, parallel tool calling behavior may differ from non-thinking mode. Test your specific use case.
-
Anthropic
tool_resultrole: Tool results must be sent asrole: "user"messages, notrole: "tool". This catches developers migrating from OpenAI every time. -
Streaming tool call accumulation: When streaming, you must accumulate partial
argumentsstrings across chunks before parsing. Attempting to parse each chunk individually will fail with JSON syntax errors.
FAQ
Q: Can I use the same tool definitions across all providers?
A: Across OpenAI, DashScope, DeepSeek, and SiliconFlow — yes, identical. For Anthropic, you need to rename parameters to input_schema and remove the function wrapper.
Q: Which providers support function calling in reasoning/thinking mode?
A: DeepSeek (V3.2+) and DashScope (Qwen3 series with enable_thinking: true) both support tool calls during reasoning. OpenAI's o3/o4-mini support tool use. Anthropic's extended thinking with Claude also supports tool use.
Q: How many tools can I define per request?
A: OpenAI has no hard limit but performance degrades beyond ~128. Anthropic recommends keeping it under 64 for optimal performance. DashScope and DeepSeek limits are model-dependent. For very large tool registries, OpenAI's tool_search feature (GPT-5.4+) defers loading until needed.
Q: Do all providers support additionalProperties: false in tool schemas?
A: OpenAI requires it for strict mode. DeepSeek requires it for strict mode on /beta. Anthropic, DashScope, and SiliconFlow accept it but don't enforce it.
Q: What happens if the model generates invalid tool call arguments? A: Without strict mode, the model may produce arguments that don't match the schema. Your application should always validate arguments before executing the tool. With strict mode (OpenAI, DeepSeek Beta), the API guarantees valid arguments.
Sources: OpenAI Function Calling docs (retrieved 2026-08-01), Anthropic Tool Use docs (retrieved 2026-08-01), DeepSeek Tool Calls docs (retrieved 2026-08-01), DashScope Function Calling docs (retrieved 2026-08-01), Qveris Function Calling Guide (retrieved 2026-08-01), Digital Applied AI Function Calling Guide (retrieved 2026-08-01).