Function calling looks identical across providers —until it isn’t. OpenAI sends tool_calls as a delta you accumulate across streaming chunks. Anthropic returns tool_use as a content block peer to text blocks. Google wraps everything in candidates with functionCall objects. DeepSeek follows OpenAI closely, until it doesn’t on parallel calls.
Your agent code breaks every time you switch models. This guide fixes that. Working code for all four providers. A differences table that tells you what breaks where. And a unified wrapper pattern that lets you write tool definitions once and use them everywhere. Function calling is the foundation of every building AI agents —master the tool loop, and agent architecture becomes straightforward.
How Function Calling Actually Works
The pattern is the same across all providers. Understanding it once is more important than memorizing each provider’s syntax.
The tool loop:
- You define tools —name, description, JSON Schema for parameters
- You send a user message + tool definitions to the model
- The model decides whether to respond with text or request a tool call
- If tool call: your code parses the function name and arguments —executes the function —sends the result back
- The model processes the result —decides: respond with text, or call another tool
- Repeat until the model responds with text or you hit a max iteration limit
Function calling vs. structured outputs. Function calling: the model decides when to use a tool. Structured outputs: the model always returns your schema. Use function calling when the model needs autonomy —“figure out what information you need and get it.” Use structured outputs when you need guaranteed format —“always return a JSON object with these fields.”
Provider 1: OpenAI Function Calling
OpenAI’s function calling implementation is the most mature and the reference standard that others follow.
from openai import OpenAI
import json
client = OpenAI(
base_url="https://api.tokspan.com/v1",
api_key="ts-your-key-here"
)
tools = [{
"type": "function",
"function": {
"name": "get_stock_price",
"description": "Get the current stock price for a ticker symbol. Returns price in USD.",
"parameters": {
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "Stock ticker, e.g. AAPL"}
},
"required": ["symbol"]
}
}
}]
response = client.chat.completions.create(
model="gpt-5.5",
messages=[{"role": "user", "content": "What's Apple's stock price?"}],
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
if msg.tool_calls:
for tool_call in msg.tool_calls:
args = json.loads(tool_call.function.arguments)
result = execute_stock_lookup(args["symbol"])
# Send result back
messages = [
{"role": "user", "content": "What's Apple's stock price?"},
msg,
{"role": "tool", "tool_call_id": tool_call.id, "content": str(result)}
]
final = client.chat.completions.create(model="gpt-5.5", messages=messages)
print(final.choices[0].message.content)
OpenAI specifics. Parallel tool calls: GPT-5.5 can request multiple tools in one response —check for multiple items in msg.tool_calls. Streaming: tool_calls arrive as deltas; accumulate index —function.name —function.arguments across chunks. Structured Outputs + function calling: define tool parameters with strict: true for guaranteed schema compliance.
Provider 2: Anthropic Tool Use
Claude’s tool use is structurally different —tools appear as content blocks within messages, not as a separate field.
import anthropic
client = anthropic.Anthropic(
base_url="https://api.tokspan.com/anthropic",
api_key="ts-your-key-here"
)
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1000,
tools=[{
"name": "get_stock_price",
"description": "Get the current stock price for a ticker symbol.",
"input_schema": {
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "Stock ticker, e.g. AAPL"}
},
"required": ["symbol"]
}
}],
messages=[{"role": "user", "content": "What's Apple's stock price?"}]
)
for block in response.content:
if block.type == "tool_use":
# Execute the tool Claude requested
result = execute_stock_lookup(block.input["symbol"])
# Build the conversation continuation —the full cycle:
# 1. The assistant message contains ALL content blocks from Claude's response
# 2. The user message contains tool_result blocks matching each tool_use
assistant_msg = {"role": "assistant", "content": response.content}
tool_result_msg = {
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": str(result)
}]
}
# Send the result back and get Claude's final response
follow_up = client.messages.create(
model="claude-opus-4-8",
max_tokens=1000,
messages=[
{"role": "user", "content": "What's Apple's stock price?"},
assistant_msg,
tool_result_msg
]
)
# Claude will return a text block with the final answer
for follow_block in follow_up.content:
if follow_block.type == "text":
print(follow_block.text)
Key differences from OpenAI. Tool definitions use input_schema instead of parameters. Tool calls are tool_use content blocks within response.content —they’re peers to text blocks, not a separate field. Tool results are sent as tool_result content blocks in a user message. Streaming includes partial tool_use blocks —you get the tool name and arguments incrementally.
Provider 3: Google Gemini Function Calling
# Gemini uses a different structure —function declarations with OpenAPI-like schema
tools = [{
"function_declarations": [{
"name": "get_stock_price",
"description": "Get the current stock price for a ticker symbol.",
"parameters": {
"type": "object",
"properties": {
"symbol": {"type": "string", "description": "Stock ticker, e.g. AAPL"}
},
"required": ["symbol"]
}
}]
}]
# Response structure:
# response.candidates[0].content.parts[0].function_call.name
# response.candidates[0].content.parts[0].function_call.args
Gemini specifics. Automatic function calling: Gemini can call and execute functions in a single API request —set automatic_function_calling in the tool configuration. Search grounding: built-in “tool” that grounds responses in Google Search results without you implementing a search API.
Provider 4: DeepSeek Function Calling
DeepSeek follows the OpenAI format. Same tool definition, same response structure. The practical difference: parallel tool calling is less reliable than GPT-5.5 —tools that should be called in parallel are sometimes called sequentially instead. Test your multi-tool scenarios specifically if you’re switching from GPT-5.5 to DeepSeek.
# Identical to OpenAI code —just change base_url and model
client = OpenAI(
base_url="https://api.tokspan.com/v1",
api_key="ts-your-key-here"
)
# Same tool definitions, same response handling as OpenAI example above
Cross-Provider Differences Table
| Feature | OpenAI | Anthropic | DeepSeek | |
|---|---|---|---|---|
| Tool definition format | function.parameters (JSON Schema) | input_schema (JSON Schema) | function_declarations.parameters | Same as OpenAI |
| Response location | message.tool_calls[] | content[] blocks | candidates[].content.parts[] | Same as OpenAI |
| Parallel tool calls | Yes, reliable | Yes, reliable | Yes | Partial, less reliable |
| Streaming tools | Deltas, accumulate | Partial blocks | Partial candidates | Same as OpenAI |
| Tool choice control | tool_choice: "auto"/"required"/"none" | tool_choice with similar options | function_calling_config | Same as OpenAI |
| Max tools per request | 128 | Undocumented (large) | Undocumented | Follows OpenAI |
| Code changes to switch | — | 100% (different SDK) | ~80% | 0% (from OpenAI) |
Common Pitfalls
These are the bugs that ship to production. Every one of them has a fix you can implement in an afternoon —but only if you know to look for it before your users do.
1. Streaming tool_calls accumulation bugs. In streaming mode, tool_calls arrive across multiple chunks —each carries an index, a partial function.name, and a partial function.arguments string. The mistake: calling json.loads() on the arguments string before the final delta chunk with finish_reason: "tool_calls" lands. You get a JSONDecodeError every time, and retry logic makes it worse because the partial state gets corrupted. Accumulate arguments by index across chunks. Parse only when the stream signals completion. This is one of the most common production tool-calling failure modes.
2. Provider-specific JSON Schema differences. OpenAI supports $ref, anyOf, and nested oneOf in tool parameter schemas. Gemini silently ignores $ref definitions —your tool still works, but the model never sees the referenced schema. Anthropic runs stricter server-side validation than OpenAI; a schema that passes on GPT-5.5 returns a 400 on Claude with an opaque validation error. Test your schemas against every provider in CI, not manually the day before launch. A CI schema-validation step with each provider’s API catches this in minutes.
3. Parallel tool call ID mismatch. The model returns get_price("AAPL") and get_price("GOOGL") in one response. You execute both concurrently. Results arrive out of order. You map them back to the wrong tool_call_id because you assumed position matches execution order. The model receives GOOGL’s price under AAPL’s ID and generates a confident, plausible, and completely wrong answer. Always index results by tool_call_id before building the result messages. Never rely on array position.
4. Tool errors that pass as legitimate data. Your get_stock_price function’s HTTP call times out. You catch the exception and return the string "Error: connection timeout". The model reads that string as data and responds: “The current price is Error: connection timeout.” Format tool errors with a recognizable prefix like TOOL_ERROR: <type> —<message>. Describe error handling in the tool’s description field so the model knows to retry or tell you the tool failed. A model cannot distinguish between a bug and unusual data unless you give it a signal.
A Unified Function-Calling Wrapper
The wrapper pattern: define tools once in a provider-agnostic format. Translate to each provider’s native format at call time. Normalize responses back to a unified format.
class UnifiedToolClient:
"""One tool definition. Any provider. Automatic translation."""
def __init__(self, base_url: str, api_key: str):
self.openai_client = OpenAI(base_url=base_url, api_key=api_key)
def call_with_tools(self, model: str, messages: list, tools: list):
"""Provider-agnostic tool calling. Handles translation internally."""
# Tools defined in OpenAI format —works for OpenAI, DeepSeek, and
# platforms that translate to Anthropic/Google natively
response = self.openai_client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice="auto"
)
return self._normalize_response(response)
def _normalize_response(self, response):
"""Return a unified format regardless of which provider served the request."""
msg = response.choices[0].message
return {
"text": msg.content,
"tool_calls": [
{"name": tc.function.name, "arguments": json.loads(tc.function.arguments)}
for tc in (msg.tool_calls or [])
] if msg.tool_calls else []
}
The aggregation platform shortcut. This wrapper is 30 lines of code. But it only handles translation for providers that speak OpenAI-compatible format. For Anthropic-native features (thinking + tool use together, streaming partial tool results) and Google-native features (automatic function calling), you need a platform with native protocol support for each provider —otherwise you’re maintaining three separate code paths. Platforms with multi-protocol support handle this at the infrastructure level. Your code stays provider-agnostic while each provider’s unique features remain available. If you’re just getting started with unified tool calling, the TokSpan quickstart guide walks through setting up your first multi-provider tool request in under five minutes.
FAQ
Which provider has the best function calling?
GPT-5.5: most reliable, best parallel calling, strongest ecosystem. Claude Opus: best for complex multi-step tool chains where reasoning depth matters. Gemini: automatic function calling is a convenience win for simple tools. DeepSeek: good enough for simple tools, occasionally unreliable for parallel calls. Use GPT-5.5 when tool reliability is critical. Use Claude when tool reasoning depth matters more than raw reliability.
Can I use the same tool definitions across all providers?
Not natively. JSON Schema is shared but the wrapper format differs. Use a translation layer (30 lines of Python) or an aggregation platform that translates automatically. Your tool definitions —names, descriptions, parameter schemas —are portable even when the wrapper format isn’t.
How many tools can I define per request?
OpenAI: 128. Anthropic: undocumented but large. Google: no hard limit. In practice, more than 10 tools degrades selection accuracy —the model starts confusing similarly-named tools. Keep your active tool set focused.
Should I build my own wrapper or use a platform?
Build if you use 1–2 providers and need specific control over the tool-calling loop. Use a platform if you want to freely switch providers and avoid maintaining four code paths. The wrapper pattern in this guide takes 30 minutes to implement and maintain. A platform makes it zero —see the TokSpan docs for the platform-level API that handles translation and normalization across all four providers.
Function calling is the foundation of every AI agent. But here is the uncomfortable question the industry has not answered: why, in 2026, does every LLM provider still have a slightly different format for tool definitions? JSON Schema is shared. The concept of “tool_call” is shared. Yet the wrapper format —input_schema vs. parameters, tool_use blocks vs. tool_calls array —remains stubbornly provider-specific. A standards body could fix this in a six-month working group. So far, no one has convened one. The question: will the market force standardization through the OpenAI-compatible default, or will native tool-use features become so differentiated that cross-provider compatibility is permanently abandoned?
Try unified function calling —One tool definition. Four providers. Zero wrapper code —while the industry figures out whether standardization is actually coming.