记录一次claude code->litellm->openrouter->claude/gpt模型调用bug的修复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 lvzhongrenjie@MacBook-Pro ~/python_project/litellm-roger main claude ▐▛███▜▌ Claude Code v2.1.69 ▝▜█████▛▘ claude-opus-4.6 · API Usage Billing ▘▘ ▝▝ ~/python_project/litellm-roger /model to try Opus 4.6 ❯ list the files in the directory ⎿ API Error: 400 {"error" :{"message" :"litellm.BadRequestError: OpenAIException - {\"error\":{\"message\":\"litellm.BadRequestError: OpenrouterException - {\\\"error\\\":{\\\"message\\\":\\\"Provider returned error\\\",\\\"code\\\":400,\\\"metadata\\\":{\\\"ra w\\\":\\\"{\\\\\\\"type\\\\\\\":\\\\\\\"error\\\\\\\",\\\\\\\"error\\\\\\\":{\\\\\\\"type\\\\\\\":\\\\\\\ " invalid_request_error\\\\\\\",\\\\\\\"message\\\\\\\":\\\\\\\"messages.1.content.1: `tool_use` ids must be unique\\\\\\\"},\\\\\\\"request_id\\\\\\\":\\\\\\\"req_011CYzLfsNDGTxJ38CPcaZpg\\\\\\\"}\\\",\\\"provi der_name\\\":\\\"Anthropic\\\",\\\"is_byok\\\":false }},\\\"user_id\\\":\\\"user_2y1ZByYtj77ms63zgitDw0v6Y Pc\\\"}. Received Model Group=claude-opus-4.6\\nAvailable Model Group Fallbacks=None\",\"type \":null,\"param\":null,\"code\":\"400\"}}. Received Model Group=claude-opus-4.6\nAvailable Model Group Fallbacks=None"," type ":null," param":null," code":" 400"}}
当用户通过 LiteLLM 使用 OpenRouter 调用 Claude 模型时,LiteLLM 以 OpenAI 格式发送消息。如果 assistant message 同时包含:
1. content 是一个 list,其中包含 Anthropic 风格的 tool_use 块
2. tool_calls 字段(OpenAI 风格)
OpenRouter 在转换时会处理这两个部分,可能导致 tool_use id 重复。
概述
OpenAI 和 Anthropic 使用不同的消息格式 API。LiteLLM 作为统一接口层,需要在两种格式之间进行转换。本报告详细对比两种格式的差异。
1. 基本消息结构
OpenAI 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "model" : "gpt-4" , "messages" : [ { "role" : "system" , "content" : "You are a helpful assistant." } , { "role" : "user" , "content" : "Hello!" } , { "role" : "assistant" , "content" : "Hi there!" } ] }
Anthropic 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "model" : "claude-3-sonnet" , "system" : "You are a helpful assistant." , "messages" : [ { "role" : "user" , "content" : "Hello!" } , { "role" : "assistant" , "content" : "Hi there!" } ] }
关键差异
特性
OpenAI
Anthropic
System 消息
作为 messages 数组中的第一条消息
独立的 system 字段
Content 类型
string 或 array
始终为 array
用户/助手交替
无强制要求
强制要求 user/assistant 交替
2. 多模态内容(图片)
OpenAI 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "role" : "user" , "content" : [ { "type" : "text" , "text" : "What's in this image?" } , { "type" : "image_url" , "image_url" : { "url" : "https://example.com/image.png" } } ] }
Anthropic 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "role" : "user" , "content" : [ { "type" : "text" , "text" : "What's in this image?" } , { "type" : "image" , "source" : { "type" : "url" , "url" : "https://example.com/image.png" } } ] }
关键差异
特性
OpenAI
Anthropic
图片类型
image_url
image
图片字段
image_url.url
source.url 或 source.data
Base64 支持
data:image/png;base64,... URL
source.type: "base64"
这是导致本次 bug 的核心区域。
OpenAI 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "role" : "assistant" , "content" : "Let me check the weather." , "tool_calls" : [ { "id" : "call_abc123" , "type" : "function" , "function" : { "name" : "get_weather" , "arguments" : "{\"location\": \"San Francisco\"}" } } ] }
工具结果返回:
1 2 3 4 5 { "role" : "tool" , "tool_call_id" : "call_abc123" , "content" : "72°F, sunny" }
Anthropic 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "role" : "assistant" , "content" : [ { "type" : "text" , "text" : "Let me check the weather." } , { "type" : "tool_use" , "id" : "toolu_abc123" , "name" : "get_weather" , "input" : { "location" : "San Francisco" } } ] }
工具结果返回:
1 2 3 4 5 6 7 8 9 10 { "role" : "user" , "content" : [ { "type" : "tool_result" , "tool_use_id" : "toolu_abc123" , "content" : "72°F, sunny" } ] }
关键差异对比表
特性
OpenAI
Anthropic
工具调用位置
独立的 tool_calls 数组
嵌入 content 数组中
工具调用类型
function
tool_use
ID 字段名
tool_calls[].id
content[].id
函数名字段
function.name
name
参数字段
function.arguments (JSON 字符串)
input (JSON 对象)
参数格式
字符串 (需要解析)
对象 (直接使用)
结果角色
tool
user
结果类型
无特殊类型
tool_result
结果 ID 字段
tool_call_id
tool_use_id
4. 本次 Bug 的根本原因
问题场景
当消息历史中同时存在两种格式时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { "role" : "assistant" , "content" : [ { "type" : "text" , "text" : "Let me check." } , { "type" : "tool_use" , "id" : "toolu_abc123" , "name" : "get_weather" , "input" : { "location" : "SF" } } ] , "tool_calls" : [ { "id" : "toolu_abc123" , "type" : "function" , "function" : { "name" : "get_weather" , "arguments" : "{\"location\": \"SF\"}" } } ] }
问题链路
1 2 3 4 5 6 7 8 9 用户请求 (混合格式) ↓ LiteLLM (可能未清理重复) ↓ OpenRouter (格式转换) ↓ Anthropic API ↓ ❌ "tool_use ids must be unique"
原因分析
消息历史混合格式 :可能来自之前的 Anthropic 响应(content 中有 tool_use),被存储后又添加了 tool_calls 字段
OpenRouter 转换逻辑 :OpenRouter 将 OpenAI 的 tool_calls 转换为 Anthropic 的 tool_use 块,但没有检测 content 中已存在的 tool_use
结果 :同一个 tool_use id 出现两次 → Anthropic 报错
5. LiteLLM 的转换逻辑
转换函数位置
Anthropic 消息转换 : litellm/litellm_core_utils/prompt_templates/factory.py
anthropic_messages_pt() - 主转换函数
convert_to_anthropic_tool_invoke() - 工具调用转换
convert_to_anthropic_tool_result() - 工具结果转换
转换流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 OpenAI 消息格式 │ ▼ ┌─────────────────────────────────────┐ │ anthropic_messages_pt() │ │ │ │ 1. 提取 system 消息到单独字段 │ │ 2. 转换图片格式 │ │ 3. 转换 tool_calls → tool_use │ │ 4. 转换 tool 消息 → tool_result │ │ 5. 去重 tool_use ids (本次修复) │ └─────────────────────────────────────┘ │ ▼ Anthropic 消息格式
关键转换代码片段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def convert_to_anthropic_tool_invoke (tool_calls ): anthropic_tool_invoke = [] for tool in tool_calls: tool_id = tool["id" ] tool_name = tool["function" ]["name" ] tool_input = json.loads(tool["function" ]["arguments" ]) anthropic_tool_invoke.append({ "type" : "tool_use" , "id" : tool_id, "name" : tool_name, "input" : tool_input }) return anthropic_tool_invoke
Anthropic 的限制
1 2 3 4 5 6 7 def _sanitize_anthropic_tool_use_id (tool_use_id: str ) -> str : sanitized = re.sub(r"[^a-zA-Z0-9_-]" , "_" , tool_use_id) if not sanitized: sanitized = "tool_use_id" return sanitized
ID 前缀约定
前缀
类型
来源
toolu_
普通工具调用
Anthropic 原生
srvtoolu_
服务端工具
Anthropic 原生
call_
OpenAI 工具调用
OpenAI 原生
任意字符串
用户自定义
需要清洗
7. Thinking/Reasoning 块
OpenAI (o1/o3 等推理模型)
1 2 3 4 5 { "role" : "assistant" , "content" : null , "reasoning_content" : "Let me think about this step by step..." }
Anthropic (Claude Extended Thinking)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "role" : "assistant" , "content" : [ { "type" : "thinking" , "thinking" : "Let me think about this step by step..." , "signature" : "..." } , { "type" : "redacted_thinking" , "data" : "..." } ] }
8. Cache Control (Prompt Caching)
OpenAI 格式
1 2 3 4 5 { "role" : "system" , "content" : "Long context..." , "cache_control" : { "type" : "ephemeral" } }
Anthropic 格式
1 2 3 4 5 6 7 8 9 10 { "role" : "user" , "content" : [ { "type" : "text" , "text" : "Long context..." , "cache_control" : { "type" : "ephemeral" } } ] }
OpenRouter 的特殊处理
OpenRouter 要求 cache_control 在 content 块内部,不在消息级别:
1 2 3 4 def _move_cache_control_to_content (messages ): ...
9. 完整对照表
特性
OpenAI
Anthropic
System 消息
messages 数组中
独立 system 字段
Content 类型
string | array
array only
图片块类型
image_url
image
图片数据结构
image_url.url
source.{url|data}
工具调用位置
tool_calls[] 数组
content[] 数组内
工具调用类型
function
tool_use
参数格式
JSON 字符串
JSON 对象
工具结果角色
tool
user
工具结果类型
无
tool_result
思考块
reasoning_content 字段
thinking content 块
Cache Control
消息级别
content 块级别
ID 字符合法性
无限制
[a-zA-Z0-9_-]+
消息交替规则
宽松
严格 user/assistant 交替
10. 最佳实践建议
1. 避免混合格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "content" : [{"type" : "tool_use" , ...}], "tool_calls" : [...] } { "content" : [{"type" : "tool_use" , ...}] } { "tool_calls" : [...] }
2. 确保工具 ID 唯一
1 2 3 4 5 6 7 8 9 10 unique_tool_ids = set () for item in tool_invoke_results: item_id = item.get("id" ) if item_id and item_id in unique_tool_ids: continue if item_id: unique_tool_ids.add(item_id) assistant_content.append(item)
3. 使用 LiteLLM 的转换函数
1 2 3 4 5 6 7 8 from litellm.litellm_core_utils.prompt_templates.factory import anthropic_messages_ptconverted = anthropic_messages_pt( messages=openai_format_messages, model="claude-3-sonnet" , llm_provider="anthropic" )
11. 本次修复摘要
修复点 1: Anthropic 消息转换
文件 : litellm/litellm_core_utils/prompt_templates/factory.py
1 2 3 4 5 6 7 8 elif m.get("type" , "" ) == "tool_use" : _tool_use_id = m.get("id" ) if _tool_use_id: if _tool_use_id in unique_tool_ids: continue unique_tool_ids.add(_tool_use_id) assistant_content.append(m)
修复点 2: OpenRouter 消息预处理
文件 : litellm/llms/openrouter/chat/transformation.py
1 2 3 4 5 6 7 8 9 10 def _deduplicate_tool_use_in_messages (messages ): """移除 content 中的 tool_use 块(当 tool_calls 存在时)""" for message in messages: if message.get("role" ) == "assistant" and message.get("tool_calls" ): content = message.get("content" ) if isinstance (content, list ): filtered = [b for b in content if b.get("type" ) != "tool_use" ] message["content" ] = filtered return messages
12. 相关代码文件
文件路径
功能
litellm/litellm_core_utils/prompt_templates/factory.py
消息格式转换主逻辑
litellm/llms/openrouter/chat/transformation.py
OpenRouter 请求转换
litellm/llms/anthropic/chat/transformation.py
Anthropic 直接调用转换
litellm/types/llms/anthropic.py
Anthropic 类型定义
litellm/types/llms/openai.py
OpenAI 类型定义
结论
OpenAI 和 Anthropic 的消息格式差异主要在于:
结构设计哲学不同 :OpenAI 倾向于扁平结构,Anthropic 倾向于嵌套结构
工具调用的处理方式不同 :这是最容易出问题的地方
类型严格程度不同 :Anthropic 更严格
在 LiteLLM 这样的统一接口层中,必须仔细处理这些差异,特别是在消息历史可能混合两种格式的情况下。本次修复确保了即使存在混合格式,也能正确去重,避免 Anthropic API 报错。