Files
zend-token/docs/cross-provider-mapping.md
2026-08-23 02:12:08 +08:00

182 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 跨 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。