13 mins
2576 words
Loading views
Xiaomi API 代理开发记录:从零到踩坑到修复

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 格式)

代理的工作:

  1. 接收 Anthropic Messages API 格式请求(/v1/messages
  2. 将模型 ID 映射为小米的模型名
  3. 将请求格式转为 OpenAI Chat Completions API 格式
  4. 转发到小米 API
  5. 将 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-20250506
  • claude-4-sonnet
  • claude-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": [...]
}
}

关键点:

  1. 实际回复在 reasoning_content 字段,而不是 content
  2. 后续请求必须把之前的 reasoning_content 传回去

但 Anthropic 格式没有 reasoning_content 字段,经过一次转换后就丢失了。

解决方案h3

使用特殊标记 [REASONING]...[/REASONING] 来保存 reasoning_content:

响应时(OpenAI → Anthropic):

// 小米推理模型:用特殊标记保存 reasoning_content
if (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_content
const 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 },
});
}
// 普通 content
if (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_content
const 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

Terminal window
# ❌ 错误(bash 语法)
DEBUG_PROXY=true node xiaomi-proxy.cjs
# ✅ 正确(PowerShell 语法)
$env:DEBUG_PROXY="true"; node xiaomi-proxy.cjs

curl 命令h3

Terminal window
# ❌ 错误(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

Terminal window
# 终端 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

  1. 推理模型的特殊性:小米 mimo-v2.5-pro 等推理模型需要特殊处理 reasoning_content 字段
  2. 协议差异:Anthropic 和 OpenAI 的工具结果格式不同,需要正确转换
  3. 调试的重要性:通过添加详细日志,快速定位了问题根源
  4. Windows 环境:PowerShell 的语法和 bash 不同,需要注意
  5. 历史兼容性:处理历史对话时,需要为缺失的字段提供默认值
  6. 多层保障:关键数据采用标记 + 缓存 + 默认值三层保障,提高鲁棒性

附录:完整代码结构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

Terminal window
# 开启调试模式启动代理
$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