你构建了一个具备正常工作调用能力的智能体,随后更换了后端的模型,工具调用便开始失败。你的工具定义并未更改。然而,模型所期望的请求和响应格式发生了变化,因为每个提供商都使用其独有的形状来定义工具、工具调用响应、参数编码以及工具结果。如果你的技术栈中没有任何组件能在这些不同的形状之间进行转换,那么更换模型就意味着需要编写新的解析代码并面对新的故障模式。
本文比较了六种智能体框架如何定义工具模式并在不同提供商之间进行转换,以及每种情况下转换发生的位置。接着,它展示了我们如何在 API 层面对工具调用进行标准化处理,使得你的应用程序发送给每个支持工具的 OpenRouter 模型的形状保持一致。
简而言之
OpenAI、Anthropic 和 Google 各自以不同的网络格式定义工具并返回工具调用。为其中一个提供商编写的工具定义无法在不修改的情况下直接用于另一个提供商。
LangChain 和 LangGraph 针对每个提供商翻译一种工具定义。CrewAI 将翻译工作委托给其路由到的客户端。OpenAI Agents SDK、Claude Agent SDK 和 Google ADK 都是围绕单一提供商的格式构建的。Microsoft Agent Framework 则委托给配置的模型连接器处理。
OpenRouter 接受 OpenAI 风格的 tools 数组,并为每个支持工具调用的模型返回标准的 tool_calls 响应,因此切换模型只需更改模型字符串即可。
在包含工具的请求中,Auto Exacto 默认会根据吞吐量、工具调用成功率和基准数据对提供商进行重新排序,其背后的“工具调用错误率”指标在每个模型的 Performance(性能)选项卡上均可查看。
通过订阅,你同意接收 OpenRouter 通讯:包括模型使用数据、产品更新和研究报告,频率约为每周一次。您可以通过每封电子邮件中的链接随时退订。请参阅我们的隐私政策。
各提供商对“什么是工具”达成一致。工具具有名称、描述以及说明其接受哪些参数的参数模式。模型读取这三部分信息,决定是否调用工具,并生成相应的参数。
协议仅止于此。每个提供商将这些部分封装在其独有的请求和响应格式中,且这些格式互不兼容。
在 OpenAI 的 Chat Completions API 中,你传递一个 tools 数组,其中每个条目包含 type: "function" 和一个 function 对象,该对象持有名称、描述和 JSON Schema 参数。当模型希望调用工具时,助手消息会携带一个 tool_calls 数组,且每次调用的 arguments 字段均为 JSON 编码的字符串。
{
"tools" : [
{
"type" : "function" ,
"function" : {
"name" : "get_weather" ,
"description" : "获取城市的当前天气" ,
"parameters" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" } },
"required" : [ "city" ]
}
}
}
]
}
Anthropic 的 Messages API 使用更扁平的定义,模式位于 input_schema 下。模型的请求以助手消息内的 tool_use 内容块形式返回,参数已解析为 input 对象,且回合以 stop_reason: "tool_use" 结束。
{
"tools" : [
{
"name" : "get_weather" ,
"description" : "获取城市的当前天气" ,
"input_schema" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" } },
"required" : [ "city" ]
}
}
]
}
Google 的 Gemini API 在 generateContent 请求中将工具定义嵌套在 function_declarations 数组下,模型的请求以 response content 中的 functionCall 部分返回,而非作为单独的顶级字段。Google 较新的 Interactions API 又接受了不同的工具条目形状,在每个工具的顶层包含 type: "function"。
这是一个工具、三种请求格式和三种响应格式。开源模型增加了另一种情况。未经过训练以输出工具调用格式的模型只能以文本形式生成工具调用。此类模型的工具调用支持取决于服务层将工具定义放入提示词中,并将模型的输出解析回调用。这种解析不属于提供商的传输格式的一部分,其失败方式也与原生格式不同。
您的应用程序定义了一个工具,但它必须生成的确切请求格式取决于请求背后的模型。要么堆栈中的某些内容将该定义重写为每个模型的正确格式,要么您自己编写并维护该转换。
模式翻译可以位于何处
翻译可以在三个地方运行。
在您的应用程序中。您自己编写每个提供商的请求和响应映射,并在提供商更改时维护它。
在框架中。代理框架或它委托的提供商客户端接受一个工具定义,并为每个提供商生成格式。
在 API 层。在每个模型前面的网关接受一种格式,并在向每个提供商发送时进行翻译。我们的工具调用和结构化输出文档完整展示了标准化形状。
以下框架的主要区别在于它们承担第二种选项的程度以及将哪个提供商视为其原生格式。
这里的所有框架都支持工具调用。不同之处在于翻译位于何处以及框架拥有多少部分。
LangChain 和 LangGraph
您使用带有类型提示的 Python 函数定义一次工具,并使用 @tool 装饰器,类型提示定义了工具的输入模式。您通过 bind_tools() 将工具附加到模型,create_agent 运行工具调用循环。LangChain 的聊天模型集成将定义转换为每个提供商的格式,因此相同的代理代码可以通过 init_chat_model 在 OpenAI、Anthropic 和 Google 模型上运行。
这种抽象从您的代码中隐藏了传输格式的差异。任何给定翻译的质量取决于特定的提供商集成,因此请测试您计划发布的特定模型。LangChain 还在 langchain-openrouter 包中记录了 OpenRouter 聊天模型集成,因此您可以通过 init_chat_model 选择任何 OpenRouter 模型,并将每个提供商的翻译留给我们 API 处理。
对于 MCP,LangChain 记录了基于 FastMCP 构建的 MCPAdapter,它发现服务器的工具并将其适配为 LangChain 工具。langchain.mcp 命名空间需要 langchain[mcp]>=1.4.0,并记录为测试版。
CrewAI
CrewAI 围绕角色和任务组织。您将工具分配给代理,团队协调工作。CrewAI 本身不实现特定于提供商的工具格式。
CrewAI 的 LLM 文档描述了 OpenAI、Anthropic、Google 的 Gemini API、Azure、AWS Bedrock 和 Snowflake Cortex 的原生 SDK 集成,通过您配置的 provider/model-id 字符串进行选择。所有其他提供商都通过 LiteLLM 运行。在任何一种情况下,模式处理属于 CrewAI 路由到的客户端,而不是 CrewAI 本身。
对于 MCP,crewai-tools 包为 stdio、SSE 和可流式 HTTP 服务器提供了 MCPServerAdapter 和传输类。
OpenAI Agents SDK
OpenAI Agents SDK 围绕 OpenAI 的工具格式构建。您使用 @function_tool 装饰器定义函数工具,SDK 从函数签名和文档字符串生成参数的 JSON Schema。它在 OpenAI 模型上提供最完整的支持,包括在 OpenAI 端运行的托管工具。
这并不仅限于 OpenAI。该 SDK 的模型文档描述了通往其他提供商的三个内置路径。set_default_openai_client 通过设置 base_url 和 api_key 将 SDK 指向兼容 OpenAI 的端点,ModelProvider 对单次运行应用自定义提供商,而 Agent.model 则为单个智能体设置模型。Any-LLM 和 LiteLLM 被记录为尽最大努力的第三方适配器,用于覆盖内置路径未涵盖的情况。当你通过 Chat Completions 而非 Responses API 进行路由时,SDK 会丢弃仅适用于 Responses 的字段,且文档警告称某些提供商不支持 JSON Schema 结构化输出。你离 OpenAI 的模型越远,就越依赖于兼容性层而非官方支持。
MCP 支持是内置的,涵盖了通过 OpenAI 的 Responses API 托管的 MCP 服务器工具以及通过 stdio 和 HTTP 传输协议直接连接到 MCP 服务器的功能。
Claude Agent SDK
Claude Agent SDK 是 Claude Code 背后的智能体框架,打包为 Python 和 TypeScript 库。与 Anthropic Messages API 不同,它为你运行工具执行循环,内置了工具、上下文管理、权限、钩子和子智能体。你可以在 Python 中使用 @tool 装饰器或在 TypeScript 中使用 tool() 定义自定义工具,使用 create_sdk_mcp_server 将其包装为进程内 MCP 服务器,并将该服务器传递给查询。
它针对 Claude 模型,因此工具调用直接使用 Anthropic 的原生 tool_use 格式。不存在跨提供商的翻译层。将其指向非 Anthropic 模型并非其设计用途。
Microsoft Agent Framework
微软将 Agent Framework 描述为 AutoGen 和 Semantic Kernel 的直接继任者,结合了 AutoGen 的智能体抽象与 Semantic Kernel 的企业级功能,并添加了显式工作流。其概述列出了 Microsoft Foundry、Anthropic、Azure OpenAI、OpenAI 和 Ollama 作为支持的模型提供商,工具调用和 MCP 服务器通过智能体抽象进行处理。
工具作为类型化函数附加到智能体上,框架会将其转换为带有 JSON Schema 的函数工具。工具调用翻译属于你配置的任意模型连接器,因此更改提供商也会随之改变模式处理方式。微软发布了从 AutoGen 迁移的指南,供从旧框架转移的团队使用。
Google ADK
Google 的 Agent Development Kit 专为 Gemini 及其原生的 function_declarations 格式构建。当你将 Python 函数传递给智能体的工具列表时,ADK 会将其包装为 FunctionTool,并根据函数的签名和文档字符串生成模式。
对于 Gemini 之外的模型,ADK 记录了包括 LiteLLM 连接器在内的连接器页面。通过该路径,对 Claude 或 GPT 模型的调用将通过 LiteLLM 的翻译而非 ADK 的原生功能运行,因此这些模型的模式行为取决于该集成。请测试你计划使用的确切模型。
对于 MCP,ADK 提供了 McpToolset,它连接到 MCP 服务器并将其工具暴露给智能体。
框架比较
在选择框架之前,请先根据工具调用支持筛选我们的模型目录。列出提供商适配器的框架与支持原生工具调用的模型是两回事,它们之间的差距正是意外发生的地方。
| 框架 | 你如何定义工具 | 跨提供商翻译模式的组件 | MCP 支持 |
|---|---|---|---|
| LangChain 和 LangGraph | 带有类型提示和 @tool 装饰器的 Python 函数 |
LangChain 的每个提供商聊天模型集成 | langchain.mcp 命名空间中的 MCPAdapter,文档标记为 beta |
| CrewAI | 按角色分配给智能体的工具 | 路由客户端,即原生提供商 SDK 或 LiteLLM | crewai-tools 中的 MCPServerAdapter |
| OpenAI Agents SDK | 带有生成 JSON Schema 的 @function_tool |
无官方翻译。其他提供商通过兼容 OpenAI 的端点或 beta 版 Any-LLM 和 LiteLLM 适配器 | 内置,托管和直接 MCP 服务器 |
Claude Agent SDK @tool 在进程内 MCP 服务器中:无。单提供商设计,内置 MCP 客户端与进程内服务器
Microsoft Agent Framework 将类型化函数转换为函数工具:配置的模型连接器,通过代理抽象内置 MCP 服务器
Google ADK Python 函数包装为 FunctionTool:无原生跨提供商路径。通过 LiteLLM 等连接器支持非 Gemini 模型 McpToolset
将此表视为起点,在做出决定前请查阅最新文档。这些框架发布频繁,工具调用行为是其中变化较大的部分之一。
切换模型时会发生什么故障
故障呈现多种形态,上述框架均未消除这些问题。
一个提供商接受的 schema 可能被另一个更严格的提供商拒绝。深层嵌套的参数对象、不寻常的类型,或一个提供商忽略而另一个强制执行的约束,都足以在新模型上产生请求错误。
没有原生工具调用能力的模型会将调用返回为纯文本。管道中的任何组件都无法将其识别为 tool_call,因此没有可捕获的错误,只有不符合循环预期的响应。除非框架或你自己的代码添加了文本解析的回退机制,否则代理将停止推进,而不是抛出错误。
修复行为各不相同。某些代码路径在调用返回格式错误时会抛出可捕获的错误。其他情况则要求你自己负责检测和恢复。如果你的代理循环假设了某种行为,而框架提供了另一种,那么模型切换可能导致错误未被察觉。
一种更微妙的情况出现在未切换模型家族时。同一模型由不同提供商提供服务时,可能以不同的速率返回有效的工具调用。自 2025 年 8 月以来,我们对 OpenRouter 上的每次工具调用响应进行了评分,并在我们的 Auto Exacto 公告中指出,在受影响的提供商上,GLM-5 和 GLM-4.7 的工具调用错误率在我们开始将工具调用流量从较弱的端点路由走后,从约 8% 降至接近 1%。
我们将每个失败的工具调用分类为三类,同样的三项检查也适用于你自己的日志记录。InvalidJson 表示参数无法解析为 JSON。UnknownName 表示调用的函数名称不在请求的工具列表中。SchemaMismatch 表示参数未通过工具参数 schema 的验证。关于循环机制,包括重试、停止条件和逐轮控制,请参阅我们关于构建工具调用代理循环的指南。
翻译存在的第三个位置是 API 层,位于框架之下。我们在每个模型之前统一标准化工具调用,因此上层不会看到各提供商之间的差异。
你发送一个带有 tools 数组的 OpenAI 风格请求。我们将其转换为目标提供商的格式,运行它,并返回标准的 tool_calls 响应。对于所有支持工具调用的模型,你发送和接收的形状是相同的,因此切换模型只需更改模型字符串即可。
下面的示例以 OpenAI 格式定义一次工具,并根据运行请求的模型无关地从 message.tool_calls 读取结果。
import json
import os
from openai import OpenAI
client = OpenAI(
base_url = "https://openrouter.ai/api/v1" ,
api_key = os.environ[ "OPENROUTER_API_KEY" ],
)
def get_weather (city: str ) -> dict :
return { "city" : city, "forecast" : "sunny" , "temperature_c" : 24 }
tools = [{
"type" : "function" ,
"function" : {
"name" : "get_weather" ,
"description" : "Get the current weather for a city" ,
"parameters" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" }},
"required" : [ "city" ],
},
},
}]
messages = [{ "role" : "user" , "content" : "What's the weather in Lisbon?" }]
response = client.chat.completions.create(
model = "openai/gpt-5.5" ,
messages = messages,
tools = tools,
)
choice = response.choices[ 0 ]
messages.append(choice.message)
for call in choice.message.tool_calls or []:
args = json.loads(call.function.arguments) # arguments 是一个 JSON 字符串
result = get_weather( ** args)
messages.append({
"role" : "tool" ,
"tool_call_id" : call.id,
"content" : json.dumps(result),
})
有两点在所有模型中保持一致。工具定义和你解析的响应保持不变。tool_calls 始终是一个数组,每个 arguments 的值是
You build an agent with working tool calling, then change the model behind it and the tool calls start failing. Your tool definition did not change. The request and response format the model expects did, because each provider uses its own shape for tool definitions, tool call responses, argument encoding, and tool results. If nothing in your stack translates between those shapes, a model swap turns into new parsing code and new failure modes.
This article compares how six agent frameworks define tool schemas and translate them across providers, and where the translation runs in each case. It then shows how we normalize tool calling at the API layer so the shape your application sends and receives stays the same for every tool-capable model on OpenRouter.
Tl;dr
OpenAI, Anthropic, and Google each define tools and return tool calls with a different wire format. A tool definition written for one does not work unchanged on another.
LangChain and LangGraph translate one tool definition per provider. CrewAI delegates translation to the client it routes to. The OpenAI Agents SDK, the Claude Agent SDK, and Google ADK are built around one provider’s format. Microsoft Agent Framework delegates to the configured model connector.
OpenRouter accepts an OpenAI-style tools array and returns a standard tool_calls response for every model that supports tool calling, so switching models is a change to the model string.
On requests that include tools, Auto Exacto reorders providers using throughput, tool-calling success rate, and benchmark data by default, and the Tool Call Error Rate metric behind it is visible on each model’s Performance tab.
By subscribing you agree to receive the OpenRouter newsletter: model usage data, product updates, and research reports, about one email a week. Unsubscribe anytime via the link in every email. See our Privacy Policy .
The providers agree on what a tool is. A tool has a name, a description, and a parameter schema that says which arguments it takes. The model reads those three pieces, decides whether to call the tool, and produces the arguments.
The agreement ends there. Each provider wraps those pieces in its own request and response format, and the formats don’t interchange.
In OpenAI’s Chat Completions API , you pass a tools array where each entry has type: "function" and a function object holding the name, description, and JSON Schema parameters . When the model wants a tool, the assistant message carries a tool_calls array, and each call’s arguments field is a JSON-encoded string.
{
"tools" : [
{
"type" : "function" ,
"function" : {
"name" : "get_weather" ,
"description" : "Get the current weather for a city" ,
"parameters" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" } },
"required" : [ "city" ]
}
}
}
]
}
Anthropic’s Messages API uses a flatter definition with the schema under input_schema . The model’s request comes back as a tool_use content block inside the assistant message, with the arguments already parsed into an input object, and the turn ends with stop_reason: "tool_use" .
{
"tools" : [
{
"name" : "get_weather" ,
"description" : "Get the current weather for a city" ,
"input_schema" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" } },
"required" : [ "city" ]
}
}
]
}
Google’s Gemini API nests tool definitions under a function_declarations array in the generateContent request, and the model’s request comes back as a functionCall part inside the response content rather than as a separate top-level field. Google’s newer Interactions API accepts a different tool entry shape again, with type: "function" at the top level of each tool.
{
"tools" : [
{
"function_declarations" : [
{
"name" : "get_weather" ,
"description" : "Get the current weather for a city" ,
"parameters" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" } },
"required" : [ "city" ]
}
}
]
}
]
}
That is one tool, three request shapes, and three response shapes. Open-weight models add a further case. A model that was not trained to emit a tool-calling format can only produce a tool call as text. Any tool-calling support for such a model depends on the serving layer placing the tool definitions in the prompt and parsing the model’s output back into a call. That parsing is not part of a provider wire format, and it fails differently from a native format.
Your application defines one tool, but the exact request shape it has to produce depends on the model behind the request. Either something in your stack rewrites that one definition into the right per-model format, or you write and maintain that translation yourself.
Where schema translation can live
There are three places the translation can run.
In your application. You write the per-provider request and response mapping yourself and maintain it as providers change.
In the framework. The agent framework, or the provider client it delegates to, takes one tool definition and produces each provider’s format.
In the API layer. A gateway in front of every model accepts one format and translates on the way to each provider. Our tool calling and structured outputs documentation shows the normalized shape in full.
The frameworks below differ mainly in how much of the second option they take on, and which provider they take as their native format.
Every framework here supports tool calling. What differs is where the translation lives and how much of it the framework owns.
LangChain and LangGraph
You define a tool once as a Python function with type hints using the @tool decorator, and the type hints define the tool’s input schema. You attach tools to a model with bind_tools() , and create_agent runs the tool-calling loop. LangChain’s chat model integrations translate the definition into each provider’s format, so the same agent code runs on OpenAI, Anthropic, and Google models through init_chat_model .
The abstraction hides the wire-format differences from your code. The quality of any given translation depends on the specific provider integration, so test the exact model you plan to ship on. LangChain also documents an OpenRouter chat model integration in the langchain-openrouter package, so you can select any OpenRouter model through init_chat_model and leave the per-provider translation to our API.
For MCP, LangChain documents MCPAdapter , built on FastMCP, which discovers a server’s tools and adapts them into LangChain tools. The langchain.mcp namespace requires langchain[mcp]>=1.4.0 and is documented as beta.
CrewAI
CrewAI is organized around roles and tasks. You assign tools to an agent, and the crew coordinates the work. CrewAI does not implement provider-specific tool formatting itself.
CrewAI’s LLM documentation describes native SDK integrations for OpenAI, Anthropic, Google’s Gemini API, Azure, AWS Bedrock, and Snowflake Cortex, selected by the provider/model-id string you configure. All other providers run through LiteLLM. In either case, schema handling belongs to the client CrewAI routes to, not to CrewAI.
For MCP, the crewai-tools package provides MCPServerAdapter and transport classes for stdio, SSE, and streamable HTTP servers.
OpenAI Agents SDK
The OpenAI Agents SDK is built around OpenAI’s tool format. You define function tools with the @function_tool decorator, and the SDK generates the JSON Schema for the arguments from the function signature and docstring. It gives you the most complete support on OpenAI models, including hosted tools that run on OpenAI’s side.
It is not limited to OpenAI. The SDK’s model documentation describes three built-in paths to other providers. set_default_openai_client points the SDK at an OpenAI-compatible endpoint by setting base_url and api_key , ModelProvider applies a custom provider to one run, and Agent.model sets the model for one agent. Any-LLM and LiteLLM are documented as best-effort, beta third-party adapters for cases the built-in paths do not cover. When you route through Chat Completions rather than the Responses API, the SDK drops Responses-only fields, and the documentation warns that some providers don’t support JSON Schema structured outputs. The further you move from OpenAI’s models, the more you rely on a compatibility layer rather than first-party support.
MCP support is built in , covering hosted MCP server tools through OpenAI’s Responses API and direct connections to MCP servers over stdio and HTTP transports.
Claude Agent SDK
The Claude Agent SDK is the agent harness behind Claude Code, packaged as a Python and TypeScript library. Unlike the Anthropic Messages API, it runs the tool-execution loop for you, with built-in tools, context management, permissions, hooks, and subagents. You define custom tools with the @tool decorator in Python or tool() in TypeScript, wrap them in an in-process MCP server with create_sdk_mcp_server , and pass that server to the query.
It targets Claude models, so tool calls use Anthropic’s native tool_use format directly. There is no cross-provider translation layer. Pointing it at a non-Anthropic model is not what it is built for.
Microsoft Agent Framework
Microsoft describes Agent Framework as the direct successor to AutoGen and Semantic Kernel, combining AutoGen’s agent abstractions with Semantic Kernel’s enterprise features and adding explicit workflows. Its overview lists Microsoft Foundry, Anthropic, Azure OpenAI, OpenAI, and Ollama among the supported model providers, with tool calls and MCP servers handled through the agent abstraction.
Tools attach to an agent as typed functions the framework turns into function tools with JSON Schemas. Tool-calling translation belongs to whichever model connector you configure, so changing the provider changes the schema handling with it. Microsoft publishes a migration guide from AutoGen for teams moving from the older framework.
Google ADK
Google’s Agent Development Kit is built for Gemini and its native function_declarations format. When you pass a Python function to an agent’s tool list, ADK wraps it as a FunctionTool and generates the schema from the function’s signature and docstring.
For models outside Gemini, ADK documents connector pages including a LiteLLM connector . On that path, a call to a Claude or GPT model runs through LiteLLM’s translation rather than through anything native to ADK, so schema behavior on those models belongs to that integration. Test the exact model you plan to use.
For MCP, ADK provides McpToolset , which connects to an MCP server and exposes its tools to an agent.
Framework comparison
Before you pick a framework, filter our model catalog by tool-calling support . A framework listing a provider adapter and a model supporting native tool calling are two different things, and the gap between them is where the surprises come from.
Framework How you define a tool Who translates the schema across providers MCP support
LangChain and LangGraph Python function with type hints and the @tool decorator LangChain’s per-provider chat model integrations MCPAdapter in the langchain.mcp namespace, documented as beta
CrewAI Tool assigned to an agent by role The routed client, either a native provider SDK or LiteLLM MCPServerAdapter in crewai-tools
OpenAI Agents SDK @function_tool with a generated JSON Schema No first-party translation. Other providers through OpenAI-compatible endpoints or beta Any-LLM and LiteLLM adapters Built in, hosted and direct MCP servers
Claude Agent SDK @tool in an in-process MCP server None. Single-provider by design Built in, MCP client with in-process servers
Microsoft Agent Framework Typed functions turned into function tools The configured model connector Built in, MCP servers through the agent abstraction
Google ADK Python function wrapped as FunctionTool No native cross-provider path. Non-Gemini models through connectors such as LiteLLM McpToolset
Treat this table as a starting point and check the current documentation before you commit. These frameworks release often, and tool-calling behavior is one of the parts that changes.
What breaks when you switch models
The failures take a few shapes, and none of the frameworks above remove them.
A schema that one provider accepts is rejected by a stricter one. A deeply nested parameter object, an unusual type, or a constraint that one provider ignores and another enforces is enough to produce a request error on the new model.
A model without native tool calling returns the call as plain text. Nothing in the pipeline recognizes it as a tool_call , so there is no error to catch, only a response that isn’t what your loop expected. Unless the framework or your own code adds a text-parsing fallback, the agent stops making progress instead of raising an error.
Repair behavior varies. Some code paths raise a catchable error when a call comes back malformed. Others leave detection and recovery to you. If your agent loop assumes one behavior and the framework provides the other, a model swap can make an error go unnoticed.
A subtler version appears without switching model families. The same model served by two different providers can return valid tool calls at different rates. We have scored every tool call response across OpenRouter since August 2025, and in our Auto Exacto announcement we reported that the tool call error rate for GLM-5 and GLM-4.7 on the affected providers fell from approximately 8% to closer to 1% after we began routing tool-calling traffic away from weaker endpoints.
We classify each failed tool call into three categories, and the same three checks work for your own logging. InvalidJson means the arguments don’t parse as JSON. UnknownName means the called function name isn’t in the request’s tool list. SchemaMismatch means the arguments don’t validate against the tool’s parameter schema. For the loop mechanics, including retries, stop conditions, and turn-by-turn control, see our guide to building a tool-calling agent loop .
The third place the translation can live is the API layer, below the framework. We normalize tool calling once, in front of every model, so nothing above it sees the per-provider differences.
You send an OpenAI-style request with a tools array. We transform it into the target provider’s format, run it, and return a standard tool_calls response. For every model that supports tool calling, the shape you send and receive is the same, so switching models is a change to the model string.
The example below defines the tool once in OpenAI format and reads the result from message.tool_calls regardless of which model ran the request.
import json
import os
from openai import OpenAI
client = OpenAI(
base_url = "https://openrouter.ai/api/v1" ,
api_key = os.environ[ "OPENROUTER_API_KEY" ],
)
def get_weather (city: str ) -> dict :
return { "city" : city, "forecast" : "sunny" , "temperature_c" : 24 }
tools = [{
"type" : "function" ,
"function" : {
"name" : "get_weather" ,
"description" : "Get the current weather for a city" ,
"parameters" : {
"type" : "object" ,
"properties" : { "city" : { "type" : "string" }},
"required" : [ "city" ],
},
},
}]
messages = [{ "role" : "user" , "content" : "What's the weather in Lisbon?" }]
response = client.chat.completions.create(
model = "openai/gpt-5.5" ,
messages = messages,
tools = tools,
)
choice = response.choices[ 0 ]
messages.append(choice.message)
for call in choice.message.tool_calls or []:
args = json.loads(call.function.arguments) # arguments is a JSON string
result = get_weather( ** args)
messages.append({
"role" : "tool" ,
"tool_call_id" : call.id,
"content" : json.dumps(result),
})
Two things carry across every model. The tool definition and the response you parse stay the same. tool_calls is always an array and each arguments value is
| 刊期 | 得分 | 排名 | 结果 |
|---|---|---|---|
| 2026-10-04 | 9.22 | 21 | 入选 |
| 2026-10-03 | 9.95 | 27 | 未入选 |