# 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 # OpenAI 风格 x-api-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":""}, {"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": {"": {...}} } ``` `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": {"": {...}} } ``` `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 item(summary 为空,data 进信封) content[].image → input_image part content[].document → input_file part(PDF 可直传的上游)/ L3(不可传的) tool_result.is_error=true → output 文本前缀 "Error: "(**有损**,见 5.4) server_tool_use / *_tool_result → L3:OpenAI 族无对应概念 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 tools(web_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 §5:Anthropic 的 `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. ``` `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 时**原样并入上游请求体,不匹配则丢弃。用于阿里 `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. **第一个上游 adapter:DeepSeek**(OpenAI 兼容,同族直出,最简单) 4. **跨族桥的非流式路径**(§5)—— 用双向往返测试保证:`O→A→O` 与 `A→O→A` 的可逆部分必须恒等 5. **流式规范化层**(§9,含阿里全量差分)—— 最易出 bug,值得先写对照测试 6. **跨族桥的流式路径**(§9.2)—— 全项目最难的一块,状态机需单独设计 7. **Anthropic adapter**(thinking 原样透传路径) 8. **国产 adapter 批量接入** 第 4 步和第 6 步是这个项目的技术核心,其余都是工程量。