跳转到内容

请求整流与多协议格式转换

请求整流与多协议格式转换

前置知识

需要了解 HTTP 请求/响应的基本概念,以及 Server-Sent Events (SSE) 流式传输机制。


学习目标

  • 理解三大 AI API 协议的核心差异(消息格式、流式传输、工具调用)
  • 掌握请求整流的核心策略:请求体转换、响应体转换、流式转换
  • 了解 thinking 模式等特殊字段的整流处理
  • 识别协议转换中的陷阱(如工具调用格式不兼容)

项目实践

核心问题

不同的 AI API 供应商使用不同的请求/响应协议:

协议端点请求格式响应格式流式
Claude (/v1/messages)anthropic-version{role, content} 数组{content: [{type, text}]}SSE: event: content_block_delta
OpenAI (/v1/chat/completions)Authorization: Bearer{role, content} 消息列表{choices: [{message}]}SSE: data: {"delta": {...}}
Gemini (/v1beta/models/...:streamGenerateContent)URL 参数 key={contents: [{role, parts}]}{candidates: [{content}]}SSE: 每行一个 JSON 对象

请求整流(Request Rectification)是指:代理层接收客户端发送的某种协议的请求,将其转换为上游供应商能理解的格式,再将上游的响应转换回客户端期望的格式。

整流层级

请求整流发生在多个层面:

1. 请求头整流

不同供应商认证方式不同:

客户端发送:Authorization: Bearer <token>
代理整流后:
→ Claude: 添加 anthropic-version: 2023-06-01, x-api-key: <token>
→ OpenAI: 保持 Authorization: Bearer <token>
→ Gemini: URL 参数 key=<token>,移除 Authorization 头

2. 请求体整流

消息格式的差异是最主要的整流工作:

  • 角色名映射:Claude 使用 user/assistant,OpenAI 使用 user/assistant/system,Gemini 使用 user/model
  • 内容结构:Claude 的 content 是数组(支持 type: "text" / type: "tool_use"),OpenAI 的 content 可以是字符串或数组
  • 工具调用:Claude 使用 tool_use/tool_result 块,OpenAI 使用 function 对象

3. 响应体整流

上游响应需要转换回客户端期望的格式。这包括:

  • 字段名映射
  • 嵌套结构重组
  • 缺失字段填充

4. 流式响应整流

流式传输的整流最为复杂,因为需要在每个 chunk 到达时进行转换:

  • Claude SSE 事件类型message_startcontent_block_startcontent_block_deltacontent_block_stopmessage_deltamessage_stop
  • OpenAI SSE 数据格式:每行 data: {"choices": [{"delta": {...}}]}
  • Gemini SSE 格式:每行一个 JSON 对象,无 data: 前缀

整流器需要在接收到上游 chunk 后,即时生成对应的客户端格式 chunk,并保持事件序列的一致性。

Thinking 模式整流

Claude 的 thinking 模式是一个特殊场景:

  • 上游:使用 thinking 参数控制 thinking 预算
  • 客户端:可能不理解 thinking 字段
  • 整流策略:代理层可以选择:
    • 透传(客户端和上游都支持 thinking)
    • 移除 thinking 字段(客户端不支持时)
    • 整流 thinking 响应块(将 thinking 内容从流中过滤或标记)

Thinking 整流器需要处理的情况:

  1. 客户端发送了 thinking 请求但上游不支持 → 移除 thinking 参数
  2. 上游返回了 thinking 内容块但客户端不期望 → 过滤或转换为普通文本
  3. thinking 预算超限 → 截断或返回错误

模型名整流

不同供应商对同一模型可能有不同的命名:

客户端请求:model: "claude-sonnet-4-6"
代理映射:
→ Claude: "claude-sonnet-4-6-20250514" (完整版本名)
→ 兼容端: "claude-sonnet-4-6" (别名)

模型名整流需要维护一个映射表,支持别名、版本号和回退策略。


问题与规避

陷阱 1:流式转换中的事件顺序

问题:Claude 的 SSE 是严格的事件序列(content_block_start 必须在 content_block_delta 之前),而 OpenAI 的流式只是简单的 delta 累加。从 OpenAI 格式转换为 Claude 格式时,如果消息顺序错误,客户端可能崩溃。

规避策略

  • 维护一个事件状态机,确保转换后的事件序列合法
  • 对每个 chunk 进行格式验证后再发送

陷阱 2:工具调用格式不可互转

问题:Claude 的 tool_use(包含 idnameinput 结构)与 OpenAI 的 function_call(包含 namearguments JSON 字符串)结构差异很大,无法无损转换。

规避策略

  • 如果客户端和上游使用不同的工具调用格式,代理层需要实现完整的格式转换逻辑
  • 对于无法转换的字段,选择丢弃并记录警告日志

陷阱 3:Thinking 预算整流遗漏

问题:如果代理层只整流了请求体中的 thinking 参数,但忘记整流响应体中的 thinking 内容块,客户端可能收到不理解的 type: "thinking" 内容块。

规避策略

  • 请求和响应的整流必须成对处理
  • 整流器应该有一个”能力矩阵”,记录每个上游支持哪些特性、客户端期望哪些特性

陷阱 4:SSE 缓冲区污染

问题:SSE 流式传输中,如果多个 chunk 在一个 TCP 包中到达,可能导致 SSE 行解析器将多个事件当作一个事件处理。

规避策略

  • 严格按照 SSE 规范解析:以 \n\n 分隔事件
  • 使用流式解析器,不要假设每个 read() 只包含一个完整事件

设计取舍

整流 vs 直通

方案优势劣势
全量整流客户端无需关心上游差异代理层复杂度极高,需要维护所有转换逻辑
最小整流只整流认证和 URL客户端需要适配不同上游的格式
按需整流根据客户端类型动态决定整流程度需要维护客户端能力矩阵

实践建议:如果代理的目标是”让任意上游能假装成 Claude API”,全量整流是必要的。如果只是”在多个 OpenAI-兼容的上游之间路由”,最小整流即可。

替代方案:统一协议层

另一种思路是定义一个中间协议(Internal Protocol),所有上游和下游都转换为这个中间格式。好处是转换逻辑只需要 N 个适配器(每个协议一个),而不是 N×N 个两两转换器。

缺点是引入了额外的抽象层,调试和维护成本增加。对于只有 3-4 个协议的场景,直接的两两转换更简单。


参考来源