All articles

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.

· TheRouter

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

DimensionOpenAIAnthropic ClaudeDashScope (Qwen)DeepSeekSiliconFlow
TerminologyFunction callingTool useFunction callingTool callsFunction calling
Request fieldtools arraytools arraytools arraytools arraytools array
Schema keyparametersinput_schemaparametersparametersparameters
Response formattool_calls on assistant messagetool_use content blockstool_calls on assistant messagetool_calls on assistant messagetool_calls on assistant message
Arguments typeJSON stringParsed objectJSON stringJSON stringJSON string
Parallel callsYesYesYesYes (non-thinking)Yes
Strict modeYes (strict: true)NoNoYes (Beta, /beta endpoint)No
Streaming tool chunkstool_calls delta chunkscontent_block_delta with input_json_deltatool_calls delta chunkstool_calls delta chunkstool_calls delta chunks
Tool choiceauto / required / none / namedauto / any / tool (named)auto / required / none / namedauto / required / none / namedauto / required / none / named
Max toolsNo hard limit (practical: ~128)64+Model-dependentModel-dependentModel-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 content blocks, not a separate tool_calls array
  • The stop_reason is "tool_use", not "tool_calls" (OpenAI uses finish_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:

  1. content_block_start with type: "tool_use", the tool id, and name
  2. content_block_delta events with type: "input_json_delta" carrying argument fragments
  3. content_block_stop marking 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.

ProviderForce tool usePrevent tool useSpecific toolAuto (default)
OpenAItool_choice: "required"tool_choice: "none"tool_choice: {"type": "function", "function": {"name": "X"}}tool_choice: "auto"
Anthropictool_choice: {"type": "any"}tool_choice: {"type": "none"}tool_choice: {"type": "tool", "name": "X"}tool_choice: {"type": "auto"}
DashScopetool_choice: "required"tool_choice: "none"tool_choice: {"type": "function", "function": {"name": "X"}}tool_choice: "auto"
DeepSeektool_choice: "required"tool_choice: "none"tool_choice: {"type": "function", "function": {"name": "X"}}tool_choice: "auto"
SiliconFlowtool_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):

  1. Tool definition format: Map parametersinput_schema and flatten/wrap the function object for Anthropic
  2. Tool choice format: Translate "required"{"type": "any"} and named tool formats
  3. Request structure: OpenAI uses messages with tools; Anthropic uses messages with tools but different message roles for tool results

Outbound (provider → gateway → client):

  1. Response format: Map Anthropic's tool_use content blocks → OpenAI-style tool_calls array
  2. Arguments parsing: Anthropic returns parsed objects; OpenAI-compatible providers return JSON strings
  3. Stop reason: Map stop_reason: "tool_use"finish_reason: "tool_calls"
  4. Streaming events: Translate Anthropic's content_block_start/content_block_delta events → OpenAI-style delta.tool_calls chunks

Tool result submission:

  1. Role mapping: Anthropic expects role: "user" with tool_result blocks; OpenAI uses role: "tool" with tool_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 optionWhy
Strict schema validationOpenAI or DeepSeek (Beta)Only providers with server-side strict: true support
Interleaved text + tool callsAnthropicContent-block architecture natively supports it
OpenAI SDK compatibilityDashScope, DeepSeek, SiliconFlowZero code changes for tool definitions and responses
Maximum tool countOpenAIPractical limit ~128+; tool_search for larger registries
Budget function callingDeepSeek or SiliconFlowCheapest per-token pricing with full tool call support
Thinking + tool callingDeepSeek V3.2+ or Qwen3Both support tool calls in reasoning mode
Multi-provider fallbackTheRouterNormalizes tool call formats across all providers

Gotchas and Known Issues

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

  2. Tool call ID formats: OpenAI uses call_ prefix, Anthropic uses toolu_ prefix, DeepSeek uses call_ prefix. If your system stores or references these IDs, don't assume a universal format.

  3. Empty content on tool call messages: When OpenAI-compatible models return tool calls, content is typically null. Some client libraries choke on null content — always handle it.

  4. 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 return tool_calls at all.

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

  6. Anthropic tool_result role: Tool results must be sent as role: "user" messages, not role: "tool". This catches developers migrating from OpenAI every time.

  7. Streaming tool call accumulation: When streaming, you must accumulate partial arguments strings 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).

Models covered in this article

Customer Support