请求整流与多协议格式转换
请求整流与多协议格式转换
前置知识
需要了解 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_start→content_block_start→content_block_delta→content_block_stop→message_delta→message_stop - OpenAI SSE 数据格式:每行
data: {"choices": [{"delta": {...}}]} - Gemini SSE 格式:每行一个 JSON 对象,无
data:前缀
整流器需要在接收到上游 chunk 后,即时生成对应的客户端格式 chunk,并保持事件序列的一致性。
Thinking 模式整流
Claude 的 thinking 模式是一个特殊场景:
- 上游:使用
thinking参数控制 thinking 预算 - 客户端:可能不理解
thinking字段 - 整流策略:代理层可以选择:
- 透传(客户端和上游都支持 thinking)
- 移除 thinking 字段(客户端不支持时)
- 整流 thinking 响应块(将 thinking 内容从流中过滤或标记)
Thinking 整流器需要处理的情况:
- 客户端发送了 thinking 请求但上游不支持 → 移除 thinking 参数
- 上游返回了 thinking 内容块但客户端不期望 → 过滤或转换为普通文本
- 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(包含 id、name、input 结构)与 OpenAI 的 function_call(包含 name、arguments 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 个协议的场景,直接的两两转换更简单。
参考来源
- Anthropic Messages API 文档. https://docs.anthropic.com/en/api/messages
- OpenAI Chat Completions API 文档. https://platform.openai.com/docs/api-reference/chat
- Google Gemini API 文档. https://ai.google.dev/api/generate-content