initial
This commit is contained in:
@@ -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` item(Responses)| `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
|
||||
```
|
||||
|
||||
转回去的东西只有摘要文本、没有 signature,Anthropic 侧要么 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。
|
||||
Reference in New Issue
Block a user