Claude Code 自动化开发小米 API 网关代理的完整记录,涵盖模型映射、reasoning_content 处理、三层保障机制等六个核心问题的诊断与修复过程。
背景h2
Claude Code Desktop 是 Anthropic 官方的 AI 编程助手客户端,它默认只能调用 Claude 系列模型。为了让它能调用小米的 mimo-v2.5-pro 推理模型,需要开发一个协议转换代理,将 Anthropic API 格式转换为 OpenAI API 格式。
技术原理h3
Claude Code Desktop → 本地代理 (xiaomi-proxy.cjs) → 小米 API (Anthropic 格式) (协议转换) (OpenAI 格式)代理的工作:
- 接收 Anthropic Messages API 格式请求(
/v1/messages) - 将模型 ID 映射为小米的模型名
- 将请求格式转为 OpenAI Chat Completions API 格式
- 转发到小米 API
- 将 OpenAI 响应转回 Anthropic 格式返回
第一版:基础实现h2
配置信息h3
const CONFIG = { listenPort: 8081, targetHost: 'token-plan-sgp.xiaomimimo.com', targetPath: '/v1/chat/completions', apiKey: 'tp-sdcyuxmgwfbyb5tj0q1slchv2mnctwq0ro60de68p27p5870', model: 'mimo-v2.5-pro',};模型映射h3
const ANTHROPIC_TO_IMODEL = { 'claude-sonnet-4-6': CONFIG.model, 'claude-opus-4': CONFIG.model, 'claude-sonnet-4-6-lite': CONFIG.model, 'claude-3-opus-4-video': CONFIG.model,};核心转换函数h3
实现了以下转换逻辑:
anthropicToOpenAI()- 请求格式转换openAIToAnthropic()- 响应格式转换createAnthropicStreamTransformer()- 流式响应转换
问题 1:模型映射表不完整h2
症状h3
Claude Code Desktop 发送的模型名不在映射表中,导致所有请求都走兜底逻辑。
原因h3
Claude Code 会发送多种模型名,如:
claude-sonnet-4-6-20250506claude-4-sonnetclaude-3-5-sonnet-20241022- 等等…
第一版只映射了 4 个名字。
修复h3
补全了 15 个常见 Anthropic 模型名的映射:
const ANTHROPIC_TO_IMODEL = { // 主力模型 'claude-sonnet-4-6': CONFIG.model, 'claude-sonnet-4-6-20250506': CONFIG.model, 'claude-4-sonnet': CONFIG.model, 'claude-4-sonnet-20250506': CONFIG.model, 'claude-3-5-sonnet': CONFIG.model, 'claude-3-5-sonnet-20241022': CONFIG.model, 'claude-3-opus': CONFIG.model,
// 高级模型 'claude-opus-4': CONFIG.model, 'claude-opus-4-20250514': CONFIG.model, 'claude-4-opus': CONFIG.model, 'claude-3-opus-20240229': CONFIG.model,
// 轻量模型 'claude-sonnet-4-6-lite': CONFIG.model, 'claude-3-5-haiku': CONFIG.model, 'claude-3-5-haiku-20241022': CONFIG.model, 'claude-3-haiku': CONFIG.model,
// 视频/图片 'claude-3-opus-4-video': CONFIG.model,};问题 2:缺少 stream_optionsh2
症状h3
流式请求时,无法获取 token 用量信息。
原因h3
原版代码在流式请求时会发送 stream_options: { include_usage: true },让上游在流式响应中返回 usage 信息。第一版漏掉了这个字段。
修复h3
const openaiReq = { model: targetModel, messages, max_tokens: anthropicReq.max_tokens || 4096, stream: anthropicReq.stream === true, // 修复:恢复 stream_options stream_options: anthropicReq.stream ? { include_usage: true } : undefined,};问题 3:reasoning_content 丢失(核心问题)h2
症状h3
API Error: 400 The reasoning_content in the thinking mode must be passed back to the API.原因分析h3
小米的 mimo-v2.5-pro 是推理模型,它的响应格式特殊:
{ "message": { "content": "", "reasoning_content": "嗯,用户发来一个简单的问候...", "tool_calls": [...] }}关键点:
- 实际回复在
reasoning_content字段,而不是content - 后续请求必须把之前的
reasoning_content传回去
但 Anthropic 格式没有 reasoning_content 字段,经过一次转换后就丢失了。
解决方案h3
使用特殊标记 [REASONING]...[/REASONING] 来保存 reasoning_content:
响应时(OpenAI → Anthropic):
// 小米推理模型:用特殊标记保存 reasoning_contentif (message.reasoning_content) { content.push({ type: 'text', text: `[REASONING]${message.reasoning_content}[/REASONING]` });}
if (message.content) { content.push({ type: 'text', text: message.content });}请求时(Anthropic → OpenAI):
if (block.type === 'text') { // 检查是否是 reasoning_content 标记 if (block.text.startsWith('[REASONING]') && block.text.endsWith('[/REASONING]')) { reasoningContent = block.text.slice(11, -12); // 提取 reasoning_content } else { contentParts.push({ type: 'text', text: block.text }); }}
// 构建 assistant 消息时包含 reasoning_contentconst assistantMsg = { role: 'assistant' };if (reasoningContent) { assistantMsg.reasoning_content = reasoningContent;}流式处理:
// 追踪推理内容和普通内容的状态let inReasoningMode = false;
// 小米推理模型:reasoning_content 需要用标记保存if (delta.reasoning_content) { if (!inReasoningMode) { // 开始推理模式,添加前缀标记 writeAnthropicEvent({ type: 'content_block_delta', index: currentContentIndex, delta: { type: 'text_delta', text: '[REASONING]' }, }); inReasoningMode = true; } writeAnthropicEvent({ type: 'content_block_delta', index: currentContentIndex, delta: { type: 'text_delta', text: delta.reasoning_content }, });}
// 普通 contentif (delta.content) { if (inReasoningMode) { // 从推理模式切换到内容模式,添加后缀标记 writeAnthropicEvent({ type: 'content_block_delta', index: currentContentIndex, delta: { type: 'text_delta', text: '[/REASONING]' }, }); inReasoningMode = false; } // ... 处理普通内容}问题 5:旧消息缺少 reasoning_contenth2
症状h3
API Error: 400 The reasoning_content in the thinking mode must be passed back to the API.即使已经实现了 [REASONING] 标记和缓存机制,仍然报错。
原因分析h3
通过详细日志发现,Claude Code 发送的对话历史中,所有旧的 assistant 消息都没有 [REASONING] 标记:
[2] role: assistant, hasReasoning: false, content: 我是 **Claude Code**...[4] role: assistant, hasReasoning: false, content: 看起来你遇到的问题...[6] role: assistant, hasReasoning: false, content: 让我先看看你提到的支持文章...这些消息是之前对话生成的,不是当前代理生成的,所以没有 [REASONING] 标记。
但小米 API 要求所有 assistant 消息都必须有 reasoning_content 字段,否则拒绝请求。
解决方案h3
采用三层保障策略:
// 小米推理模型需要传回 reasoning_content// 优先从标记中提取,其次从缓存中查找,最后使用默认值if (reasoningContent) { // 第一层:从 [REASONING] 标记中提取 assistantMsg.reasoning_content = reasoningContent; log(` Added reasoning_content from marker`);} else if (msg.id && reasoningCache.has(msg.id)) { // 第二层:从内存缓存中查找 assistantMsg.reasoning_content = reasoningCache.get(msg.id); log(` Added reasoning_content from cache`);} else { // 第三层:为旧消息提供空的 reasoning_content,满足 API 要求 assistantMsg.reasoning_content = ''; log(` Added empty reasoning_content (no marker or cache found)`);}缓存机制h3
// reasoning_content 缓存:key 是 Anthropic 消息 ID,value 是 reasoning_contentconst reasoningCache = new Map();
// 非流式响应时缓存function openAIToAnthropic(openaiResp, originalModel) { const messageId = `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
if (message.reasoning_content) { reasoningCache.set(messageId, message.reasoning_content); log(`Cached reasoning_content for ${messageId}`); }
return { id: messageId, ... };}
// 流式响应时缓存function createAnthropicStreamTransformer(anthropicRes, originalModel) { let accumulatedReasoning = '';
// 在 onChunk 中累积 if (delta.reasoning_content) { accumulatedReasoning += delta.reasoning_content; }
// 在 onDone 中缓存 onDone() { if (accumulatedReasoning) { reasoningCache.set(messageId, accumulatedReasoning); log(`Cached streaming reasoning_content for ${messageId}`); } }}问题 4:工具结果消息格式错误h2
症状h3
API Error: 400 messages[5] user content only supports a string or an array of content parts原因分析h3
Claude Code 发送工具结果时,使用的是 user 角色 + tool_result 内容块:
{ "role": "user", "content": [ { "type": "tool_result", "tool_use_id": "call_xxx", "content": "命令执行结果..." } ]}但 OpenAI 格式要求工具结果是单独的 tool 消息:
{ "role": "tool", "tool_call_id": "call_xxx", "content": "命令执行结果..."}第一版的 convertAnthropicContentToOpenAI 函数对 tool_result 返回了特殊对象,导致格式错误。
修复h3
更新 convertAnthropicMessages 函数,正确拆分包含 tool_result 的用户消息:
if (role === 'user') { // 检查是否包含 tool_result 内容块 if (Array.isArray(msg.content) && msg.content.some(b => b.type === 'tool_result')) { // 拆分成多个消息 const textParts = []; const toolResults = [];
for (const block of msg.content) { if (block.type === 'tool_result') { toolResults.push(block); } else if (block.type === 'text') { textParts.push(block.text); } }
// 添加 user 消息(文本内容) if (textParts.length > 0) { openaiMessages.push({ role: 'user', content: textParts.join('') }); }
// 添加 tool 消息 for (const tr of toolResults) { const content = typeof tr.content === 'string' ? tr.content : (Array.isArray(tr.content) ? tr.content.filter(b => b.type === 'text').map(b => b.text).join('') : ''); openaiMessages.push({ role: 'tool', content, tool_call_id: tr.tool_use_id, }); } } else { // 普通 user 消息 const content = convertAnthropicContentToOpenAI(msg.content); openaiMessages.push({ role: 'user', content }); }}Windows PowerShell 踩坑h2
环境变量设置h3
# ❌ 错误(bash 语法)DEBUG_PROXY=true node xiaomi-proxy.cjs
# ✅ 正确(PowerShell 语法)$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjscurl 命令h3
# ❌ 错误(PowerShell 的 curl 是 Invoke-WebRequest 的别名)curl http://127.0.0.1:8081/health
# ✅ 正确(使用 curl.exe 或文件方式)curl.exe http://127.0.0.1:8081/health
# 发送 JSON 请求(避免 PowerShell 转义问题)curl.exe -X POST http://127.0.0.1:8081/v1/messages ` -H "Content-Type: application/json" ` -H "x-api-key: sk-test" ` -d "@path\to\body.json"注册表配置h2
Claude Code Desktop 通过 Windows 注册表配置代理地址:
Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\SOFTWARE\Policies\Claude]"inferenceProvider"="gateway""inferenceGatewayBaseUrl"="http://127.0.0.1:8081""inferenceGatewayApiKey"="sk-session-token"双击 .reg 文件即可合并,无需先删除。
最终验证h2
测试命令h3
# 终端 1:启动代理$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjs
# 终端 2:测试请求curl.exe -X POST http://127.0.0.1:8081/v1/messages ` -H "Content-Type: application/json" ` -H "x-api-key: sk-test" ` -d "@test-body.json"成功响应h3
{ "id": "msg_xxx", "type": "message", "role": "assistant", "content": [ { "type": "text", "text": "[REASONING]The user is asking for the current time.[/REASONING]" }, { "type": "tool_use", "id": "call_xxx", "name": "Bash", "input": { "command": "date" } } ], "model": "claude-sonnet-4-6", "stop_reason": "tool_use", "usage": { "input_tokens": 39163, "output_tokens": 54 }}总结h2
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 模型映射不完整 | 只映射了 4 个模型名 | 补全 15 个常见模型名 |
| 缺少 stream_options | 流式请求漏掉 usage 配置 | 添加 stream_options: { include_usage: true } |
| reasoning_content 丢失 | 推理模型的特殊字段在转换中丢失 | 使用 [REASONING] 标记保存和恢复 |
| 工具结果格式错误 | Claude Code 的 tool_result 格式与 OpenAI 不兼容 | 拆分 user 消息为 user + tool 消息 |
| 旧消息缺少 reasoning_content | 历史对话没有标记,API 要求所有消息都有该字段 | 三层保障:标记 → 缓存 → 默认空值 |
关键经验h3
- 推理模型的特殊性:小米 mimo-v2.5-pro 等推理模型需要特殊处理
reasoning_content字段 - 协议差异:Anthropic 和 OpenAI 的工具结果格式不同,需要正确转换
- 调试的重要性:通过添加详细日志,快速定位了问题根源
- Windows 环境:PowerShell 的语法和 bash 不同,需要注意
- 历史兼容性:处理历史对话时,需要为缺失的字段提供默认值
- 多层保障:关键数据采用标记 + 缓存 + 默认值三层保障,提高鲁棒性
附录:完整代码结构h2
xiaomi-proxy.cjs├── 配置区 (CONFIG)├── 模型映射表 (ANTHROPIC_TO_IMODEL)├── reasoning_content 缓存 (reasoningCache)├── HTTP 工具函数│ ├── upstreamRequest() - 非流式请求│ └── upstreamStreamRequest() - 流式请求├── 请求格式转换│ ├── convertAnthropicContentToOpenAI()│ ├── convertAnthropicMessages() ← 包含 reasoning_content 三层保障│ ├── convertAnthropicTools()│ └── anthropicToOpenAI() ← 包含请求日志├── 响应格式转换│ ├── openAIToAnthropic() ← 包含 reasoning_content 标记 + 缓存│ └── createAnthropicStreamTransformer() ← 包含流式推理内容处理 + 缓存└── HTTP 服务器 ├── /health - 健康检查 ├── /v1/models - 模型列表 └── /v1/messages - 核心端点 ← 包含详细日志核心设计模式h3
三层保障机制(用于 reasoning_content):
┌─────────────────────────────────────────────────────────────┐│ reasoning_content 处理流程 │├─────────────────────────────────────────────────────────────┤│ 响应时(小米 API → Claude Code) ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ reasoning │ → │ [REASONING] │ → │ Anthropic │ ││ │ _content │ │ 标记包装 │ │ 格式响应 │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ↓ ││ ┌─────────────┐ ││ │ 缓存到 Map │ ││ └─────────────┘ │├─────────────────────────────────────────────────────────────┤│ 请求时(Claude Code → 小米 API) ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ││ │ Anthropic │ → │ 提取标记 │ → │ reasoning │ ││ │ 格式请求 │ │ 或查缓存 │ │ _content │ ││ └─────────────┘ └─────────────┘ └─────────────┘ ││ ↓ ││ ┌─────────────┐ ││ │ 默认空字符串 │ ││ └─────────────┘ │└─────────────────────────────────────────────────────────────┘附录:调试技巧h2
添加详细日志h3
在关键位置添加日志,帮助定位问题:
// 1. 显示原始请求log('Anthropic messages:');for (let i = 0; i < anthropicReq.messages.length; i++) { const msg = anthropicReq.messages[i]; log(` [${i}] role: ${msg.role}, content: ${JSON.stringify(msg.content).substring(0, 100)}...`);}
// 2. 显示转换后的请求log('OpenAI request messages:');for (let i = 0; i < openaiReq.messages.length; i++) { const msg = openaiReq.messages[i]; log(` [${i}] role: ${msg.role}, hasReasoning: ${!!msg.reasoning_content}`);}
// 3. 显示上游响应log('Upstream response:', JSON.stringify(openaiResp, null, 2));
// 4. 显示 reasoning_content 状态log(` Found reasoning_content in block: ${reasoningContent.substring(0, 80)}...`);log(` Added reasoning_content from cache (msg.id=${msg.id})`);测试命令h3
# 开启调试模式启动代理$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjs
# 发送测试请求(使用文件避免 PowerShell 转义问题)curl.exe -X POST http://127.0.0.1:8081/v1/messages ` -H "Content-Type: application/json" ` -H "x-api-key: sk-test" ` -d "@test-body.json"
# 健康检查curl.exe http://127.0.0.1:8081/health
# 查看模型列表curl.exe http://127.0.0.1:8081/v1/models
Comments