This commit is contained in:
2026-08-23 02:12:08 +08:00
commit 5987b2a1f2
19 changed files with 4174 additions and 0 deletions
+181
View File
@@ -0,0 +1,181 @@
# 跨 Provider 映射表
本文件把三家的字段与语义放在一起对照,标注**哪些能无损映射、哪些必然有损**。数据来源见 [openai.md](./openai.md)、[anthropic.md](./anthropic.md)、[deepseek.md](./deepseek.md) 各自的来源列表。
---
## 1. 端点与鉴权
| | OpenAI Chat | OpenAI Responses | Anthropic Messages | DeepSeek |
| --- | --- | --- | --- | --- |
| 路径 | `/v1/chat/completions` | `/v1/responses` | `/v1/messages` | `/chat/completions` |
| 鉴权 | `Authorization: Bearer` | 同左 | **`x-api-key`** | `Authorization: Bearer` |
| 版本 header | 无 | 无 | **`anthropic-version: 2023-06-01` 必需** | 无 |
---
## 2. 请求参数
| 语义 | OpenAI Chat | OpenAI Responses | Anthropic | DeepSeek |
| --- | --- | --- | --- | --- |
| 消息列表 | `messages` | `input` | `messages` | `messages` |
| 系统提示 | `messages[role=system\|developer]`(可多条、可在任意位置) | `instructions`(顶层) | **`system`(顶层,只有一条)** | `messages[role=system]` |
| 输出上限 | `max_completion_tokens``max_tokens` 已废弃) | `max_output_tokens` | **`max_tokens`(必填)** | `max_tokens` |
| 温度 | `temperature` 0~2 | 同左 | **`temperature` 0~1** | `temperature` 0~2 |
| top-p | `top_p` | `top_p` | `top_p` | `top_p` |
| top-k | ✗ | ✗ | `top_k` | ✗ |
| 停止序列 | `stop`(≤4 | — | `stop_sequences` | `stop`(≤16 |
| 频率惩罚 | `frequency_penalty` | — | ✗ | **已废弃** |
| 存在惩罚 | `presence_penalty` | — | ✗ | **已废弃** |
| 多候选 | `n`1~128 | ✗ | ✗ | ✗ |
| JSON 模式 | `response_format` | `text.format` | `output_config.format` | `response_format`(仅 `json_object` |
| 工具 | `tools[].function.parameters` | `tools[].parameters` | **`tools[].input_schema`** | 同 OpenAI Chat |
| 工具选择 | `tool_choice`: `auto`/`required`/`none`/具体 | 同左 | `tool_choice`: `auto`/**`any`**/`none`/`tool` | 同 OpenAI Chat |
| 禁并行工具 | `parallel_tool_calls: false` | 同左 | `tool_choice.disable_parallel_tool_use: true` | `parallel_tool_calls` |
| 推理开关 | `reasoning_effort` | `reasoning.{effort,summary}` | `thinking` + `output_config.effort` | `thinking.{type,reasoning_effort}` |
| 缓存控制 | `prompt_cache_key` / `prompt_cache_options.ttl` | 同左 | `cache_control` 断点 | **无(全自动)** |
| 用户标识 | `safety_identifier``user` 已废弃) | 同左 | `metadata.user_id` | `user_id`(≤512,限字符集) |
### 必然有损的转换
- **多条 system 消息 → Anthropic**:只能合并。Anthropic 官方兼容层的做法是把所有 system/developer 消息用**单个换行 `\n`** 连接成一条,放在最前。
- **`n > 1` → 任何非 OpenAI 上游**:无法实现,只能拒绝或退化为 `n=1`
- **`frequency_penalty` / `presence_penalty` / `logit_bias` / `seed` → Anthropic 或 DeepSeek**:无对应,只能忽略。
- **`temperature` OpenAI(0~2) → Anthropic(0~1)**:需 clamp。官方兼容层的选择是 **>1 一律截为 1**(而不是线性缩放)。
- **`top_k` → OpenAI**:无对应,只能丢弃。
---
## 3. 消息与内容块
| 语义 | OpenAI Chat | Anthropic |
| --- | --- | --- |
| 纯文本 | `content: "..."``[{type:"text", text}]` | `content: "..."``[{type:"text", text}]` |
| 图片 | `{type:"image_url", image_url:{url, detail}}` | `{type:"image", source:{type:"base64"\|"url", media_type, data\|url}}` |
| 图片 detail | `detail: low/high/auto` | **无对应,丢弃** |
| PDF/文档 | `{type:"file"}` | `{type:"document", source:{...}, citations}` |
| 音频输入 | `{type:"input_audio"}` | **不支持** |
| 助手工具调用 | `assistant.tool_calls[]` | `content` 里的 `tool_use` block |
| 工具结果 | **独立 `{role:"tool", tool_call_id, content}` 消息** | **user 消息 content 里的 `tool_result` block** |
| 工具错误 | 靠 content 文本表达 | `tool_result.is_error: true` |
| 推理内容 | `reasoning` itemResponses| `thinking` / `redacted_thinking` block |
| 引用 | `annotations` | `citations`5 种 location 类型) |
### 工具 ID 三套命名
| | 调用侧 | 结果侧 |
| --- | --- | --- |
| OpenAI Chat | `tool_calls[].id` | `tool.tool_call_id` |
| OpenAI Responses | `function_call.call_id` | `function_call_output.call_id` |
| Anthropic | `tool_use.id``toolu_` 前缀) | `tool_result.tool_use_id` |
中转站需要维护 ID 映射表,不能假设上游会接受下游生成的 ID 格式。
---
## 4. 停止原因
| 语义 | OpenAI `finish_reason` | Anthropic `stop_reason` | DeepSeek `finish_reason` |
| --- | --- | --- | --- |
| 自然结束 | `stop` | `end_turn` | `stop` |
| 达到上限 | `length` | `max_tokens` | `length` |
| 命中停止序列 | `stop` | `stop_sequence` | `stop` |
| 调用工具 | `tool_calls` | `tool_use` | `tool_calls` |
| 内容过滤 | `content_filter` | `refusal`+ `stop_details.category` | `content_filter` |
| 上下文溢出 | 无(表现为 400) | `model_context_window_exceeded` | 无 |
| 服务端工具暂停 | 无 | **`pause_turn`** | 无 |
| 资源不足 | 无 | 无 | **`insufficient_system_resource`** |
**注意**`stop``stop_sequence` 在 OpenAI 侧合并成一个值,Anthropic → OpenAI 方向可无损降级;反向则丢失信息。`pause_turn` 在 OpenAI 格式下**无法表达**,中转站必须在网关内部自行完成续跑循环,不能透传给客户端。
---
## 5. Usage 统计
| 语义 | OpenAI | Anthropic | DeepSeek |
| --- | --- | --- | --- |
| 输入 token | `usage.prompt_tokens` | `usage.input_tokens` **(仅未缓存部分)** | `usage.prompt_tokens`(总数) |
| 输出 token | `usage.completion_tokens` | `usage.output_tokens` | `usage.completion_tokens` |
| 总计 | `usage.total_tokens` | 需自行相加 | `usage.total_tokens` |
| 缓存命中 | `prompt_tokens_details.cached_tokens` | `cache_read_input_tokens` | `prompt_cache_hit_tokens` |
| 缓存写入 | 无单独计数 | `cache_creation_input_tokens` | 无(`prompt_cache_miss_tokens` 是未命中数) |
| 推理 token | `completion_tokens_details.reasoning_tokens` | `output_tokens_details.thinking_tokens` | `completion_tokens_details.reasoning_tokens` |
**关键陷阱**Anthropic → OpenAI 时,
```
prompt_tokens = input_tokens + cache_read_input_tokens + cache_creation_input_tokens
```
直接用 `input_tokens` 会严重低报(缓存命中时可能低报 99%)。
---
## 6. 流式协议映射
| | OpenAI | Anthropic |
| --- | --- | --- |
| 结构 | 无状态 chunk 序列 | **有状态**的 block 事件机 |
| 事件名 | 无(都是匿名 `data:` | 有(`event: message_start` 等) |
| 文本增量 | `choices[0].delta.content` | `content_block_delta` + `text_delta` |
| 工具增量 | `delta.tool_calls[].function.arguments`(按 `index` 累加) | `content_block_delta` + `input_json_delta.partial_json` |
| 推理增量 | Responses 有专门事件 | `thinking_delta` + `signature_delta` |
| usage | 最后一个 chunk(需 `include_usage`),`choices` 为空数组 | `message_delta.usage`**累计值** |
| 结束标记 | `data: [DONE]` | `message_stop` |
| 保活 | 无标准(实践中发注释行) | `ping` 事件 |
| 中途错误 | 无标准 | `error` 事件(200 之后仍可能发) |
### Anthropic → OpenAI 流式转换要点
1. `message_start` → 发一个只含 `role: "assistant"` 的首 chunk。
2. `content_block_start(text)` → 无对应,吞掉。
3. `text_delta``delta.content`
4. `content_block_start(tool_use)` → 发一个带 `id`/`name``arguments: ""``tool_calls[index]` chunk。
5. `input_json_delta``tool_calls[index].function.arguments` 追加。
6. `thinking_delta` → 无标准落点;常见做法是映射到 `delta.reasoning_content`(DeepSeek 风格,事实标准)。**`signature_delta` 无处可放,会丢失**——这意味着经过 OpenAI 格式往返后的对话**无法再发回 Anthropic 上游**。
7. `message_delta.stop_reason` → 最后一个 chunk 的 `finish_reason`
8. `message_stop``data: [DONE]`
9. `ping` → 丢弃或转成 SSE 注释行保活。
### OpenAI → Anthropic 流式转换要点
必须自行维护 block index 状态机:文本与工具调用要分配不同的 `index`,并在切换时补发 `content_block_stop` / `content_block_start`。OpenAI 的 chunk 里没有"块结束"信号,只能靠 `tool_calls[].index` 变化和 `finish_reason` 推断。
---
## 7. 推理内容三方对照(最不可互通的部分)
| | OpenAI Responses | Anthropic | DeepSeek |
| --- | --- | --- | --- |
| 载体 | `reasoning` item | `thinking` / `redacted_thinking` block | `reasoning_content` 字符串 |
| 可见文本是什么 | `summary`(需显式开启) | **摘要,永远不是原始 CoT** | 思考内容字符串 |
| 真实推理载体 | `encrypted_content` | **`signature`(完整思考的密文)** | 无,就是明文字段 |
| 密文是否必需 | ZDR / `store:false` 时必需 | **始终存在**,服务端解密它重建思考 | 不适用 |
| 可否修改 | 加密内容需原样回传 | **不可修改,改了直接 400** | 可自由处理 |
| 多轮是否必回传 | 有函数调用时必须 | **工具轮次内必须**;其他情况可省略但不可改 | 无工具调用时**不必**;有工具调用时**必须** |
| 关闭方式 | `reasoning.effort: "none"` / `"minimal"` | 部分模型**根本无法关闭** | `thinking.type: "disabled"` |
| 跨模型可移植 | — | **否**,绑定生成模型,换模型必须剥离 | 是 |
| 跨平台可移植 | — | **是**API/Bedrock/Vertex 三方通用 | — |
### 为什么 Anthropic 侧无法靠字段翻译打通
Anthropic 的 `thinking` 文本字段是**摘要**,真正的推理内容加密在 `signature` 里,服务端回传时**解密 `signature` 来重建思考,并不读你传的文本**(详见 [anthropic.md](./anthropic.md) §5)。
所以这条常见的转换路径是**死路**:
```
Anthropic thinking block → reasoning_content 字符串 → 存下来 → 转回 thinking block
```
转回去的东西只有摘要文本、没有 signatureAnthropic 侧要么 400,要么丢失全部推理连续性。
### 可行做法
**把 signature / encrypted_content / redacted_thinking.data 当成 opaque blob 原样搬运。** 三家的密文字段都明确声明为不可解析,中转站不需要理解它们:
1. 网关按会话 ID 存储上游返回的**原始 content block 数组**(含密文字段)。
2. 下游客户端只看到脱敏后的摘要文本(映射到 `reasoning_content` 或 Responses 的 reasoning item)。
3. 下一轮请求时,用存储的原始块重建上游请求,而不是从下游回传的文本反向构造。
需要额外注意的两点:
- **模型路由会破坏它**Anthropic thinking block 绑定生成它的模型,换模型时必须剥离。其他模型**静默忽略而不报错,但照样计输入 token**——做故障转移时不清理,用户会为无用内容付费且毫无提示。
- **signature 很长**Claude 4+ 显著变长)。存储层的列宽、日志截断、header 大小限制都可能悄悄截断它,而截断后的 signature 会在下一轮触发 400。