Files
2026-08-23 02:12:08 +08:00

552 lines
26 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.
# zend-token API 设计
> 状态:设计草案 v0.2 · 2026-08-12
> 依据:[openai.md](./openai.md)、[anthropic.md](./anthropic.md)、[deepseek.md](./deepseek.md)、[china-providers.md](./china-providers.md)、[cross-provider-mapping.md](./cross-provider-mapping.md)
>
> v0.2 变更:单一 IR → **双规范模型**(§3);`/v1/responses` 提升为一等入口。
>
> **后续**:本文的落地层是 [transform-spec.md](./transform-spec.md)(转写方案 v0.3),它在双规范模型之上补齐了上游 Profile、改写管线与 Kimi 实测结论。以下两处已被 v0.3 修正:§8.1 的"Claude 族上游一律 opaque 搬运 signature"只对 **Anthropic 官方真机**成立(见 transform-spec §7.1 的可信度分级);§6 的能力矩阵需按**上游身份三元组**而非 provider 划分(同一家的两个入口行为可能相反)。
本文只定义**对外 API 契约与桥接语义**,不涉及技术栈、存储、部署选型。
---
## 1. 设计原则
1. **双族对等** — OpenAI 族与 Claude 族是**同等公民**,不是"主格式 + 转换层"。
2. **同族零损** — 同族内的入口/上游组合不经过跨族转换,不付出任何有损代价。
3. **有损集中** — 所有跨族有损只发生在**一处**(§5 跨族桥),集中实现、集中测试、集中记录。
4. **降级必须可见** — 上游能力不足导致的任何语义变化都要可观测。**静默忽略是各家的通病,我们不重复它**(§7)。
5. **上游怪癖不外泄** — MiniMax 的 `base_resp`、阿里的全量流、智谱的 `sensitive` 都在网关内消化。
6. **不发明第三种方言** — 扩展一律走命名空间(`ztoken` 对象 / `X-Ztoken-*` header),不污染标准字段。
---
## 2. 端点总览
| 端点 | 族 | 说明 |
| --- | --- | --- |
| `POST /v1/chat/completions` | OpenAI | 生态最广 |
| `POST /v1/responses` | OpenAI | 新接口,reasoning 能力完整 |
| `POST /v1/messages` | Claude | Claude Code 等客户端 |
| `POST /v1/messages/count_tokens` | Claude | token 预估 |
| `GET /v1/models` | 共用 | 模型列表 |
| `GET /v1/models/{model}` | 共用 | 含**能力矩阵**(本项目扩展,§6) |
认证同时接受两种 header,互为别名:
```
Authorization: Bearer <ztoken-key> # OpenAI 风格
x-api-key: <ztoken-key> # Claude 风格
```
`anthropic-version` 接受但不校验(我们只有一个版本)。
---
## 3. 双规范模型(核心)
### 3.1 为什么不用单一 IR
单一 IR 看起来更"优雅",但有个真实缺陷:**它强迫每条路径都付出一次有损转换**。
实际流量分布是高度不均的——OpenAI 入口 → OpenAI 兼容上游(DeepSeek、智谱、Kimi、豆包、混元、千帆 v2、星火、阿里兼容模式)会是绝大多数。这条路径上,源和目标本来就是同一种方言,却要绕经 block 模型往返,白白丢失顺序信息又白白重建。
改为按族划分后:
```
Chat ─┐ ┌─ DeepSeek / 智谱 / Kimi / 豆包 /
├─→ 【OaiIR】 ──┬── 同族直出 ────────┤ 混元 / 千帆v2 / 星火 / 阿里兼容
Responses ─┘ │ └─ MiniMax / DashScope 原生
╔════╧════╗
║ 跨族桥 ║ ← 唯一的有损点
╚════╤════╝
Messages ──→ 【AnthIR】┴────────────────────── Anthropic / DeepSeek-anthropic /
阿里 Anthropic 兼容
```
**同族路径零转换成本;跨族只有一处,集中攻。** 这也让"Claude 入口 → Claude 上游"这条最脆弱的路径(thinking signature 必须原样保住)天然安全——它根本不进桥。
### 3.2 OaiIR — OpenAI 族规范模型
**以 Responses 的 item 模型为基线**,因为 `Responses item ⊃ Chat message`Chat 把文本放 `content`、工具放 `tool_calls`,无法表达 reasoning 与 text 的交错顺序,Responses 的异构 `output[]` 可以。
```jsonc
{
"model": "<逻辑模型名>",
"instructions": "...", // 顶层系统指令(Chat 的 system/developer 归一到这)
"items": [ // 异构数组,顺序即语义
{"type":"message", "role":"user"|"assistant", "content":[Part]},
{"type":"function_call", "call_id":"...", "name":"...", "arguments":"<json string>"},
{"type":"function_call_output", "call_id":"...", "output":"..."},
{"type":"reasoning", "id":"...", "summary":[...], "encrypted_content":"...?"}
],
"tools": [{"type":"function","name":...,"parameters":{...},"strict":bool}],
"tool_choice": {...},
"sampling": {"max_output_tokens":int, "temperature":0..2, "top_p":..., "stop":[...],
"frequency_penalty":?, "presence_penalty":?, "seed":?, "n":1},
"reasoning": {"effort":"none|minimal|low|medium|high|xhigh|max", "summary":"auto|none"},
"text": {"format": {"type":"text"|"json_object"|"json_schema", "schema":?, "strict":?}},
"stream": bool,
"passthrough": {"<provider>": {...}}
}
```
`Part` 类型:`input_text` / `output_text` / `input_image` / `input_file` / `refusal`
**注意 `arguments` 是 JSON 字符串**而非对象——这是 OpenAI 族的原生形态,保留它可以让同族直出时零解析。
### 3.3 AnthIR — Claude 族规范模型
以 Messages 的 block 模型为基线:
```jsonc
{
"model": "<逻辑模型名>",
"system": [{"type":"text","text":"...","cache_control":?}],
"messages": [
{"role":"user"|"assistant", "content":[Block]}
],
"tools": [{"name":..., "description":..., "input_schema":{...}, "strict":?}],
"tool_choice": {"type":"auto"|"any"|"tool"|"none", "disable_parallel_tool_use":?},
"sampling": {"max_tokens":int, "temperature":0..1, "top_p":?, "top_k":?, "stop_sequences":[...]},
"thinking": {"mode":"auto"|"on"|"off", "effort":?, "budget_tokens":?, "display":"summarized"|"omitted"},
"output_config": {"format":?, "effort":?},
"stream": bool,
"passthrough": {"<provider>": {...}}
}
```
`Block` 类型:`text` / `image` / `document` / `tool_use` / `tool_result` / `thinking` / `redacted_thinking` / `server_tool_use` / 各类 `*_tool_result`
**`tool_use.input` 是对象**(不是字符串)——与 OaiIR 的 `arguments` 字符串形成明确对比,跨族时必须做 parse/stringify。这是个容易漏的点:Anthropic 流式下发的 `input_json_delta` 是部分 JSON 字符串,但终态是对象。
### 3.4 两族各自的不变量
族内归一在入口层立即完成,避免畸形约束泄漏进下游逻辑。
**OaiIR**
1. `system` / `developer` 消息全部提升到 `instructions`,按原顺序用 `\n` 拼接。
2. Chat 的 `tool` role 消息转成 `function_call_output` item。
3. Chat 的 `assistant.tool_calls[]` 拆成独立的 `function_call` item,排在该 message item 之后。
**AnthIR**
1. `role` 只有 `user` / `assistant`
2. `content` 恒为数组(裸字符串包装成 `[{type:"text"}]`)。
3. **连续同 role 消息自动合并**为一条。这一条直接消化百度 v1 的"必须严格交替"约束。
**两族共同:**
- `max_output_tokens` / `max_tokens` 缺省时由能力矩阵填默认值,并标注 `X-Ztoken-Defaulted`。理由:Anthropic 侧必填,取严格的一方可以让每个 adapter 不必各写一份兜底。
### 3.5 路由决策
```
入口族 == 上游族 → 同族直出(零转换)
入口族 != 上游族 → 过跨族桥(§5)
```
`GET /v1/models/{model}` 暴露上游族,客户端可以据此选择同族入口以避开有损。
---
## 4. 入口方言层
三个入口各自负责"方言 → 族规范模型"的归一,不做跨族的事。
### 4.1 `/v1/chat/completions` → OaiIR
按 §3.4 归一。以下字段接受但按能力矩阵处理(可能降级,§7):
`n``logprobs``top_logprobs``logit_bias``seed``frequency_penalty``presence_penalty``response_format``parallel_tool_calls``tools[].function.strict`
响应回投影为 Chat 结构时:
- `function_call` item → `message.tool_calls[]``call_id``id`
- `reasoning` item → `message.reasoning_content`(摘要文本)
- **`logprobs` 字段恒存在**(值可为 `null`)——OpenAI spec 列为 required,省略会打破严格客户端。
### 4.2 `/v1/responses` → OaiIR
近乎恒等映射(OaiIR 本就以它为基线)。需处理的:
| 字段 | 处理 |
| --- | --- |
| `input` 为 string | 包装成单个 user message item |
| `previous_response_id` | **v0.1 不支持**,返回 400 `unsupported_parameter`(我们无状态,见 §12 开放问题 1) |
| `conversation` | 同上 |
| `store` | 恒为 `false`,请求 `true` 时 L1 降级 |
| `background` | 不支持,L3 |
| `include` | 按能力矩阵过滤 |
| `context_management` | L1 丢弃(网关不做自动压缩) |
### 4.3 `/v1/messages` → AnthIR
按 §3.4 归一。`thinking` 参数接受三种历史写法并统一:
| 客户端传入 | AnthIR |
| --- | --- |
| `{"type":"enabled","budget_tokens":N}` | `mode:"on"`, `budget_tokens:N` |
| `{"type":"adaptive","display":D}` | `mode:"auto"`, `display:D` |
| `{"type":"disabled"}` | `mode:"off"` |
**这吃掉了 Anthropic 的代际地雷** —— 客户端不会再踩到"4.5 只认 enabled、4.7 只认 adaptive、Fable 5 两个都拒、Opus 5 的 disabled 还和 effort 冲突"(见 anthropic.md §5 的模型矩阵)。这是相对直连 Anthropic 的明确增值点。
`stop_reason: pause_turn` **不对外暴露**:网关内部完成服务端工具续跑循环后才返回终态。理由是 OpenAI 族无法表达它,两个族的行为必须一致。
### 4.4 扩展字段约定
三个入口统一,请求体顶层接受 `ztoken` 对象(未知子字段忽略不报错):
```jsonc
{
"ztoken": {
"thinking": {"mode":"on","effort":"high"}, // 跨族统一的思考控制
"strict_capability": false, // true = 能力不足报错而非降级
"allow_degradation": ["vision","n"], // 显式放行 L3
"passthrough": {"dashscope": {"enable_search": true}}
}
}
```
用嵌套对象而非顶层字段,是为了避免和未来的官方字段撞名。OpenAI SDK 的 `extra_body` 可直接塞它,客户端零改造。
---
## 5. 跨族桥(难点集中区)
这一节是整个设计里唯一有损的地方,所以要写得最细。
### 5.1 结构差异总览
| 维度 | OaiIR | AnthIR | 桥接难度 |
| --- | --- | --- | --- |
| 容器 | 扁平 `items[]`,工具调用是**独立 item** | `messages[].content[]`,工具调用是**消息内的 block** | 中 |
| 工具入参 | `arguments`: **JSON 字符串** | `input`: **对象** | 低(parse/stringify |
| 工具结果 | 独立 `function_call_output` item | user 消息内的 `tool_result` block | 中 |
| 工具 ID | `call_id` | `tool_use.id` / `tool_result.tool_use_id` | 低(需映射表) |
| 系统指令 | `instructions` 字符串 | `system` block 数组 | 低 |
| 推理 | `reasoning` item + `encrypted_content` | `thinking` block + `signature` | **高**(§8 |
| 温度 | 0~2 | 0~1 | 低(clamp |
| 工具错误 | 靠 output 文本表达 | `tool_result.is_error` 布尔 | 中(有损) |
### 5.2 OaiIR → AnthIR
```
instructions → system: [{type:"text", text}]
message(role=user) → messages[].role=user, content=[blocks]
message(role=assistant) → messages[].role=assistant, content=[blocks]
function_call → 并入前一条 assistant 消息的 content:
{type:"tool_use", id:call_id, name, input:JSON.parse(arguments)}
function_call_output → 新建/并入 user 消息:
{type:"tool_result", tool_use_id:call_id, content:[{type:"text"}]}
reasoning → {type:"thinking", thinking:summary, signature:<信封>}
input_text/output_text → {type:"text"}
input_image → {type:"image", source:{...}}
input_file → {type:"document", source:{...}}
refusal part → 丢弃(L2),原文记入 degraded 说明
```
**关键动作**`function_call` item 必须**回填**进它前面那条 assistant message 的 content 数组,而不是新建消息——否则会产生连续两条 assistant 消息,违反 AnthIR 不变量 3(虽然会被自动合并,但顺序可能错)。
**`arguments` 解析失败**(模型吐出非法 JSON,流式截断时常见):不要抛异常。降级为 `input: {}` 并标记 L2,把原始字符串放进 `ztoken` 诊断字段。
### 5.3 AnthIR → OaiIR
```
system[] → instructions(用 \n 拼接)
content[].text → message part(output_text / input_text)
content[].tool_use → 独立 function_call item:
{call_id:id, name, arguments:JSON.stringify(input)}
content[].tool_result → 独立 function_call_output item
content[].thinking → reasoning item{summary:[thinking], encrypted_content:<信封>}
content[].redacted_thinking → reasoning itemsummary 为空,data 进信封)
content[].image → input_image part
content[].document → input_file partPDF 可直传的上游)/ L3(不可传的)
tool_result.is_error=true → output 文本前缀 "Error: "**有损**,见 5.4
server_tool_use / *_tool_result → L3OpenAI 族无对应概念
citations → annotations(尽力而为,L2
```
### 5.4 已知有损清单
跨族时必然丢失的信息,全部记入 `X-Ztoken-Degraded`
| 丢失项 | 方向 | 级别 | 说明 |
| --- | --- | --- | --- |
| `tool_result.is_error` 语义 | A→O | L2 | OpenAI 无此字段,只能退化成文本前缀 |
| `thinking``text` 的交错顺序 | A→O(Chat) | L2 | Chat 的 message 结构表达不了;**走 Responses 入口则无损** |
| `cache_control` 断点 | O→A / A→非 Anthropic | L1 | 上游若非 Anthropic 则无处安放 |
| `top_k` | O→A 时反向缺失 | L1 | OpenAI 族无此参数 |
| `n > 1` | O→A | L3 | Anthropic 恒为 1 |
| `logprobs` | O→A | L2 | Anthropic 不支持,返回 null |
| server toolsweb_search 等) | A→O | L3 | 概念不存在 |
| `citations` 精确位置 | A→O | L2 | annotations 表达力更弱 |
| **`signature` 真实性** | A→O→A 往返 | **L3** | 见 §8.4,往返后无法发回 Anthropic 上游 |
### 5.5 禁止跨族的情形
以下组合**直接返回 400**,不做勉强转换:
1. **Claude 入口 + 请求含 `thinking` + 非 Claude 族上游 + 后续要回传** — 见 §8.4。
2. **`n > 1` 且上游为 Claude 族** — 除非 `allow_degradation``n`
3. **含 `server_tool_use` 历史的会话切到 OpenAI 族上游** — 历史无法表达,续接必然错乱。
---
## 6. 模型路由与能力矩阵
### 6.1 逻辑模型名
```jsonc
{
"id": "qwen-max",
"upstream": {
"family": "openai", // openai | anthropic —— 决定是否过桥
"provider": "dashscope",
"model": "qwen-max-latest",
"endpoint_ref": null // 火山方舟填 "ep-2024xxxx"
},
"capabilities": { ... }
}
```
**火山方舟的 Endpoint ID 是账号级资源**china-providers.md §4),所以路由表是 `(tenant, logical_model) → upstream`,全局表仅作 fallback。
模型别名支持"伪装":把 `claude-sonnet-4-6` 指向任意上游,让硬编码模型名的客户端直接可用。但**不静默兜底未知模型名**——未匹配返回 404,避免拼写错误被掩盖(DeepSeek 的 Anthropic 兼容层就是反面教材)。
### 6.2 能力矩阵
```jsonc
"capabilities": {
"family": "openai",
"streaming": true,
"streaming_incremental": true, // false = 上游返回全量累积,网关需差分(阿里)
"tools": true,
"tool_strict": false,
"parallel_tools": true,
"vision": true,
"documents": false,
"json_mode": true,
"json_schema": false,
"thinking": "always_on" | "optional" | "none",
"thinking_opaque": true, // 思考内容必须原样回传(Anthropic=true
"prefill": false, // 助手前缀续写(DeepSeek prefix / Kimi partial
"n_max": 1,
"temperature_range": [0.0, 1.0],
"max_output_tokens": 8192,
"cache": "explicit" | "automatic" | "none"
}
```
---
## 7. 能力降级协议
各家的通病是静默忽略。我们保留静默以维持生态兼容,但**强制可观测**。
### 7.1 三级降级
| 级别 | 定义 | 默认行为 |
| --- | --- | --- |
| **L1 无害** | 丢弃不改变输出语义 | 静默丢弃,仅记 header |
| **L2 降质** | 输出仍可用但质量/精度变化 | header + 结构化 warning |
| **L3 变义** | 客户端拿到的东西与请求语义不符 | **报 400**,除非显式放行 |
| 参数/能力 | 级别 |
| --- | --- |
| `seed``logit_bias``metadata``user``store` | L1 |
| `temperature` clamp、`top_k` 丢弃、penalty 丢弃、`logprobs` 返回 null | L2 |
| **`tools[].strict` 失效** | **L3** |
| **图片/文档输入被丢弃** | **L3** |
| **`n > 1` 退化为 1** | **L3** |
| **`json_schema` 降级为 `json_object`** | **L3** |
### 7.2 可观测通道
```
X-Ztoken-Upstream: dashscope/qwen-max-latest (family=openai, bridged=false)
X-Ztoken-Degraded: seed=dropped(L1), temperature=clamped(L2,2.0->1.0), strict=ignored(L3)
X-Ztoken-Defaulted: max_output_tokens=4096
```
`bridged=true/false` 让调用方知道自己是否付了跨族代价——这是选择入口的直接依据。
**严格模式**`ztoken.strict_capability: true` 时,任何 L2 及以上降级直接 400,错误体列出全部冲突项。
L3 默认报错是本设计与所有现有兼容层的最大分歧。宁可让调用方显式声明"我接受图片被丢弃",也不要让他拿到一个看起来正常、实际模型根本没看到图的响应。放行方式:
```jsonc
"ztoken": {"allow_degradation": ["vision", "n", "strict"]}
```
---
## 8. thinking / reasoning 桥接
依据 anthropic.md §5Anthropic 的 `thinking` 文本只是**摘要**,真实推理加密在 `signature` 里,**改一个字节就 400**。所以桥接必须搬运不透明数据,不是翻译文本。
### 8.1 四象限
| 入口族 | 上游族 | 策略 |
| --- | --- | --- |
| Claude | Claude | **原样透传**,不解包不重打包 —— 唯一能保住真 signature 的路径 |
| OpenAI | OpenAI | 原样透传 `encrypted_content` / `reasoning_content` |
| OpenAI | Claude | 摘要 → `reasoning_content`;真 signature 装进信封放 `encrypted_content` |
| Claude | OpenAI | 上游 `reasoning_content``thinking.thinking``signature` 填我们的信封 |
**两条同族路径都不进桥**,这正是双规范模型的最大收益。
### 8.2 信封格式
```
ztk1.<base64url(AES-GCM(payload))>
```
`payload` = 上游原始 block/item 数组 + 元数据(provider、model、family、生成时间)。客户端视其为**完全不透明**。`ztk1.` 前缀用于版本演进;解析不了的信封当作"无思考上下文"降级,不报错。
### 8.3 无状态优先
默认下发信封由客户端原样回传,网关不存储。与 OpenAI 的 `encrypted_content`、Anthropic 的 `signature` 同一思路,行业已验证。
风险是客户端裁剪未知字段。缓解:信封放在**该族的原生位置**(OpenAI 族 → `reasoning.encrypted_content`Claude 族 → `thinking.signature`),这两个位置是各自 SDK 会保留的标准字段,比自定义字段安全得多。
> 有状态 handle 模式作为可选项保留,不进 v0.2 契约。
### 8.4 A→O→A 往返的硬限制
**这是必须写进对外文档的一条。**
Claude 上游的思考经 OpenAI 族客户端往返后,回来的 `signature` 是**我们的信封**,不是 Anthropic 的原始 signature。网关可以解开信封还原原始 block —— 但前提是**信封没被裁剪**。
若客户端丢弃了 `encrypted_content`,原始 signature 就永久丢失,此时:
- 该会话**不能再发回 Anthropic 上游**(会 400 或丢失推理连续性)。
- 网关的行为:剥离全部 thinking block,标记 `X-Ztoken-Degraded: thinking=lost(L3)`,并按 §7 规则报错或放行。
因此 §5.5 规定:Claude 入口 + thinking + 非 Claude 上游 + 需回传的组合,默认拒绝。
### 8.5 三条实现硬约束
1. **切换上游模型时必须剥离 thinking block** —— 其他模型静默忽略但**照样计费**。故障转移路径尤其注意。
2. **信封只走 body,绝不走 header** —— Claude 4+ 的 signature 显著变长,header 有截断风险,截断的 signature 下一轮直接 400。
3. **不得按 `type == "thinking"` 过滤** —— 会漏掉 `redacted_thinking`,直接破坏协议。
---
## 9. 流式契约
### 9.1 对外保证
无论上游行为如何:
1. **增量语义** — 每个事件只含新增内容。上游返回全量累积时(阿里 `incremental_output=false`)网关做差分,由能力矩阵 `streaming_incremental: false` 驱动。
2. **事件顺序合法** — Claude 族保证 `message_start` → block 三段式 → `message_delta``message_stop`OpenAI 族保证首 chunk 带 role、末尾 `data: [DONE]`
3. **usage 位置固定** — Chat 下需 `stream_options.include_usage` 才发末尾 usage chunk`choices` 为空数组);Messages 下 `message_delta.usage` 为**累计值**。
4. **中途错误可表达** — §10.3。
### 9.2 跨族流式的状态机
跨族时流式比非流式难得多,因为要**在流上重建结构**:
**O→A**OpenAI chunk 无"块结束"信号,只能靠 `tool_calls[].index` 变化和 `finish_reason` 推断边界。网关需维护 block index 状态机,在文本↔工具切换时补发 `content_block_stop` / `content_block_start`
**A→O**
- `content_block_start(text)` → 吞掉(无对应)
- `content_block_start(tool_use)` → 发一个带 `id`/`name``arguments:""` 的 tool_calls chunk
- `input_json_delta.partial_json` → 追加到 `arguments`
- `thinking_delta``delta.reasoning_content`
- **`signature_delta` → 无处安放**,必须缓冲到 block 结束后装信封,随末尾 chunk 下发
- `message_delta.usage` 累计值 → 取末值,不累加
- `ping` → 丢弃或转 SSE 注释行保活
### 9.3 上游怪癖消化清单
| 上游怪癖 | 处理 |
| --- | --- |
| 阿里全量累积流 | 差分成增量 |
| 阿里需 `X-DashScope-SSE: enable` | adapter 自动附加 |
| Anthropic `fallback` block 无 delta | 解析器不假设每 block 至少一个 delta |
| Anthropic 200 后发 `error` 事件 | 转成客户端族的错误表达 |
| OpenAI `[DONE]` 非 JSON | 单独识别,不进 JSON 解析器 |
| MiniMax 200 内嵌错误 | 流首帧即检测 `base_resp`,转真实状态码 |
### 9.4 provider 透传
`ztoken.passthrough.<provider>` 在**匹配到该 provider 时**原样并入上游请求体,不匹配则丢弃。用于阿里 `enable_search``repetition_penalty` 这类无法归一的自有参数。透传字段不参与能力校验,风险由调用方承担。
---
## 10. 错误模型
### 10.1 双族错误格式
按入口族返回对应格式,**HTTP 状态码两族一致**:
```jsonc
// OpenAI 族
{"error": {"message":"...", "type":"invalid_request_error", "param":"tools[0].strict", "code":"capability_unsupported"}}
// Claude 族
{"type":"error", "error":{"type":"invalid_request_error","message":"..."}, "request_id":"req_..."}
```
两族都返回 `X-Ztoken-Request-Id`
### 10.2 上游错误归一
| 上游 | 对外 |
| --- | --- |
| MiniMax 200 + `base_resp.status_code != 0` | **翻译成真实状态**1002→429、1004→401、1008→402、1039→400 |
| DeepSeek 402 余额不足 | 429 + `insufficient_quota` |
| Anthropic 529 overloaded | 503 |
| DeepSeek 422 | 400 |
| 智谱 `finish_reason:"sensitive"` | 非错误 → 映射 `content_filter`,原值放 `ztoken.upstream_finish_reason` |
**MiniMax 这条是网关的核心价值之一**:上游用 200 表达限流会让所有标准重试/熔断中间件失效,还原成 429 后通用组件即可正常工作。
### 10.3 流式中途错误
已发 200 后无法改状态码:
- **Claude 族**:发 `event: error`(与官方一致)。
- **OpenAI 族**:发含 `error` 字段的 data chunk,随后 `data: [DONE]`。OpenAI 无官方规范,此为我们的事实约定,需写进对外文档。
---
## 11. usage 归一
两族 usage 互转按 cross-provider-mapping.md §5。**最易错的一条**
```
OpenAI prompt_tokens = Anthropic (input_tokens + cache_read_input_tokens + cache_creation_input_tokens)
```
直接用 `input_tokens` 在缓存命中时会低报到 1%。
网关自身计费**以上游原始 usage 为准**,不依赖任何投影结果。
---
## 12. 开放问题
1. **`previous_response_id` / `conversation` 要不要支持?** 支持就意味着网关有状态(要存会话)。当前设计是 400 拒绝。若要支持,建议只做"信封化"——把历史塞进加密信封由客户端持有,保持无状态。
2. **L3 默认报错**是否可接受?会让"能跑就行"的客户端在我们这里失败、在别家静默成功。我认为值得,但这是产品取向。
3. **信封是否需对客户端可解密?** 目前完全不透明(网关持密钥)。若要客户端可自查,需改成签名而非加密。
4. **`cache_control` 对非 Anthropic 上游**:接受并 L1 丢弃,还是直接拒绝?
5. **多模态是否代下载转码?** 图片 base64/url 支持度各家不一,代下载会引入出网请求和体积限制。
---
## 13. 实现顺序
1. **两个族规范模型 + 三个入口方言层**(不接上游,用 echo adapter 打通)
2. **能力矩阵 + 降级引擎**(§7,纯逻辑,可完整单测)
3. **第一个上游 adapterDeepSeek**(OpenAI 兼容,同族直出,最简单)
4. **跨族桥的非流式路径**(§5)—— 用双向往返测试保证:`O→A→O``A→O→A` 的可逆部分必须恒等
5. **流式规范化层**(§9,含阿里全量差分)—— 最易出 bug,值得先写对照测试
6. **跨族桥的流式路径**(§9.2)—— 全项目最难的一块,状态机需单独设计
7. **Anthropic adapter**thinking 原样透传路径)
8. **国产 adapter 批量接入**
第 4 步和第 6 步是这个项目的技术核心,其余都是工程量。