# zend-token 转写方案(Transform Spec) > 状态:设计 v0.3 · 2026-08-13 > 范围:**Claude / OpenAI / DeepSeek / Kimi 四家**的双向转写实现规范。 > 与 [api-design.md](./api-design.md) 的关系:那份定义**对外契约**(端点、IR、降级协议),本份定义**转写引擎本身**——上游画像、改写管线、逐字段规则、测试口径。v0.2 的双规范模型(OaiIR / AnthIR)在此**保留不变**,本文是它的落地层。 > 依据:[anthropic.md](./anthropic.md)、[openai.md](./openai.md)、[deepseek.md](./deepseek.md)、[kimi.md](./kimi.md)(含实测)、[cross-provider-mapping.md](./cross-provider-mapping.md)、[compatibility-layers.md](./compatibility-layers.md)、[errors-and-limits.md](./errors-and-limits.md)。 --- ## 0. 一页结论 四家做同一件事(多轮对话 + 工具调用 + 推理),但**它们的差异不在一个层次上**,混在一起处理必然写成一堆 if-else。本方案把差异**分成四层**,每层用不同机制消化: | 层 | 差异性质 | 举例 | 消化机制 | | --- | --- | --- | --- | | **L-A 方言** | 同一语义、不同字段名/形状 | `input_schema` vs `parameters`、`prefix` vs `partial` | **双 IR + 桥**(§4) | | **L-B 能力** | 上游根本没有这个能力 | Anthropic 无 `n>1`、Kimi 不收 http 图片 URL | **能力矩阵 + 降级协议**(api-design §6/§7) | | **L-C 怪癖** | 有能力但要求特定写法,否则 400 或静默失效 | Kimi K3 `temperature≠1` 硬 400、DeepSeek 关思考三入口三种写法 | **上游 Profile 驱动的改写管线**(§5、§6)← 本文核心新增 | | **L-D 诚信度** | 上游返回的东西**不可信** | Kimi coding 模型名回显、假 signature、200+空 content | **可信度分级 + 网关自持真相**(§7、§8、§9) | **三条贯穿全文的硬规则**(违反任意一条,转写在生产上一定出事): 1. **上游身份是三元组 `(provider, entry_family, model)`,不是 provider。** 同一家的两个入口对同一参数行为相反(Kimi `temperature`:OpenAI 入口 400 / Anthropic 入口静默忽略;DeepSeek `tool_choice: any`:Anthropic 入口静默降级 / OpenAI 入口 400)。任何"按 provider 分支"的代码都是错的。 2. **最小合法请求体。** 只发 Profile 白名单里的字段,**绝不为未传参数补默认值**(Kimi K3 补 `temperature=0.7` 直接 400,moonshot-v1 补默认值会改变原有行为)。 3. **上游说什么都不算数。** 模型名、usage、错误状态码、signature 全都可能是装饰品。网关必须自持一份真相(白名单、账单口径、错误分类),只在能交叉验证时才采信上游。 --- ## 1. 求同:四家真正共享的内核 剥掉方言后,四家**语义完全一致**的部分只有下面这些。IR 的核心字段集应当**恰好等于**这张表——多一个字段就意味着某一族要造轮子,少一个就意味着有损。 | 内核语义 | 四家一致的部分 | 落在 IR 的字段 | | --- | --- | --- | | 会话是 user/assistant 交替的消息序列 | ✅ 全一致 | `messages[]` / `items[]` | | 系统指令独立于对话 | ✅ 语义一致(位置不同) | `instructions` / `system` | | 内容是多模态块序列(文本 / 图片 / 文档) | ✅ 结构一致(字段名不同) | `Part` / `Block` | | 工具 = JSON Schema 描述的函数 | ✅ 全一致(嵌套层数不同) | `tools[]` | | 模型发起调用 → 客户端执行 → 结果回灌 | ✅ 全一致(载体不同) | `tool_use` / `function_call` | | 输出上限、温度、top_p、停止序列 | ✅ 语义一致(取值域不同) | `sampling` | | 停止原因(自然 / 截断 / 工具 / 过滤) | ✅ 四个语义一致 | `stop_reason` | | 流式增量下发 | ✅ 都是 SSE | 流式规范层 | | 用量计数(输入 / 输出 / 缓存 / 推理) | ✅ 概念一致(口径不同) | `usage` | | 前缀缓存 | ✅ 四家都有 | 无字段(见 §11) | **共识只到这里。** 下面每一项在四家之间都无法直接对齐:推理内容的载体与回传规则、结构化输出的 schema 方言、内置工具、前缀续写(prefill)、`n>1`、logprobs、`is_error`。 > 设计含义:**IR 只保内核 + 各族原生扩展**,不试图为"推理""内置工具"发明统一抽象。它们走 §7 的可信度分级和 opaque 透传。 --- ## 2. 存异:一张必须先看的上游身份表 我们要接的**不是 4 个上游,是 8 个入口**。逐个列出,因为每一行的合法请求体都不同: | # | 上游身份 | Base URL | 族 | 备注 | | --- | --- | --- | --- | --- | | 1 | `anthropic / messages` | `api.anthropic.com/v1` | Claude | 唯一有真 signature 的上游 | | 2 | `openai / chat` | `api.openai.com/v1` | OpenAI | 生态基准 | | 3 | `openai / responses` | 同上 | OpenAI | reasoning 完整能力 | | 4 | `deepseek / chat` | `api.deepseek.com` | OpenAI | `image_url` 硬 400 | | 5 | `deepseek / anthropic` | `api.deepseek.com/anthropic` | Claude | 错误体是 OpenAI 格式;signature = 响应 id | | 6 | `deepseek / responses` | `api.deepseek.com/v1` | OpenAI | 图片被换成 `[Unsupported Image]` | | 7 | `kimi-platform / chat` + `/anthropic` | `api.moonshot.cn/v1`、`/anthropic/v1` | 两族 | 采样参数两入口行为**相反** | | 8 | `kimi-coding / chat` + `/messages` | `api.kimi.com/coding/v1` | 两族 | **另一套账号体系**,key 前缀 `sk-kimi-` | 外加 DeepSeek 的 `/beta`(prefix、FIM、strict)——**同一 provider 的第四个入口**,主 base URL 上传 `prefix` 是硬 400。 ### 2.1 四家的"性格"速判 这决定了转写引擎对它们的默认信任度: | | Anthropic | OpenAI | DeepSeek | Kimi platform | Kimi coding | | --- | --- | --- | --- | --- | --- | | 未知参数 | 静默忽略 | 静默忽略 | 静默忽略 | 静默忽略 | 静默忽略 | | **已知参数越界** | clamp/忽略 | 400 | **400** | **400(且是"只允许固定值")** | **400** | | 未知模型名 | 404 | 404 | 400(但旧名静默别名) | 404 | **200 + 静默 fallback** | | 不支持的模态 | — | — | **静默替换占位文本**(2 个入口) | 400 | 400 | | 错误体格式 | 规范 | 规范 | Anthropic 入口用 OpenAI 格式 | **三种格式并存** | 统一 | | signature | **真密文** | `encrypted_content` 真密文 | **伪造(= 响应 id)** | 无该字段 | **假的,不校验** | | 文档可信度 | 高 | 高 | 中(3 处实测推翻) | **低(10 条实测推翻)** | 低 | > **最危险的两格**是 DeepSeek 的"静默替换占位文本"和 Kimi coding 的"未知模型名 200"——它们产生 **HTTP 200 + 语义错误的响应**,下游无从察觉。网关必须在**请求侧**拦掉这两类情况,而不是指望上游报错。 --- ## 3. 架构总图 ``` ┌──────────── 入口方言层 (§4) ────────────┐ POST /v1/chat/completions ─┐ POST /v1/responses ────────┼──→ 【OaiIR】────┐ POST /v1/messages ─────────┼──→ 【AnthIR】───┤ POST /v1/messages/count_tokens │ └────────────────────┼─────────────────┘ │ ┌───────────▼───────────┐ │ 路由 + 族决策 (§6.2) │ └───────┬───────┬───────┘ 同族直出 │ │ 跨族 │ ┌────▼─────┐ │ │ 跨族桥 │ ← 唯一有损点 │ │ (§4.3) │ │ └────┬─────┘ ┌───────▼───────▼───────┐ │ 请求改写管线 (§6) │ ← Profile 驱动 │ R1…R9 顺序固定 │ └───────────┬───────────┘ │ ┌─────────────────▼─────────────────┐ │ 上游 8 个入口(§2) │ └─────────────────┬─────────────────┘ │ ┌───────────▼───────────┐ │ 响应改写管线 (§8) │ │ S1…S9 顺序固定 │ └───────────┬───────────┘ │ 流式规范层 (§9) ``` **关键点**:改写管线是**数据驱动**的——同一份代码跑所有上游,行为差异全部来自 Profile(§5)。新增一家 provider 的正常代价应该是"写一份 Profile + 若干黄金样本",只有真正的新协议形态(比如 MiniMax 的 `base_resp`)才需要新增代码路径。 --- ## 4. 方言层与跨族桥(L-A) 沿用 api-design §3–§5,此处只补**四家实测暴露出的、v0.2 未覆盖的转写规则**。 ### 4.1 消息归一的两条新约束 1. **`role: "developer"` 必须降级为 `system`**——DeepSeek 硬 400 `unknown variant 'developer'`。归一发生在入口层(OaiIR 本就把 system/developer 提升到 `instructions`),所以只要 §3.4 的不变量被执行,这条自动满足。**但 passthrough 路径要单独检查**。 2. **消息级扩展字段不得裁剪。** Kimi K3 的动态工具加载是一条 `{"role":"system","tools":[...]}` 且**没有 `content`** 的消息——OpenAI 协议里不存在这种形状。IR 的消息对象必须带 `ext` 字典承载它,schema 校验不能要求 `content` 必填。 ### 4.2 工具调用 ID:一律重写为网关 ID 四家的 ID 形态: | 上游 | 形态 | 问题 | | --- | --- | --- | | OpenAI Chat / Responses | `call_abc…` / `call_id` | 无 | | Anthropic | `toolu_…` | 无 | | DeepSeek | OpenAI 风格 | 非流式响应**也带 `index`**(OpenAI 不带) | | **Kimi platform** | **`{函数名}_{序号}`**,如 `get_weather_0` | **可预测、且多轮会重复** | | Kimi platform builtin | `t-web_search-{hash}`,`type` 为 `builtin_function` | 打破 OpenAI 枚举 | | Kimi coding | `tool_{随机串}` | 无 | **规则**:网关对外一律下发自己的不透明 ID(`ztk_tc_`),维护 `(session, gateway_id) → upstream_id` 映射,回程还原。理由不是洁癖——Kimi platform 的重复 ID 会让"按 ID 做去重/匹配"的逻辑在多轮里静默错配。 同时:**`tool_calls[].type` 的解析必须容忍 `builtin_function`**,不能按 OpenAI 枚举拒绝。 ### 4.3 跨族桥的补充规则 在 api-design §5.2/§5.3 之上追加: | 场景 | 规则 | | --- | --- | | `arguments` 解析失败 | 降级 `input: {}` + L2,原文进诊断字段(**不抛异常**)——流式截断时常见 | | A→O 的 `tool_result.is_error` | 文本前缀 `Error: ` + L2 标记(有损,无解) | | O→A 的 `function_call` | **必须回填进前一条 assistant 消息**,不新建消息 | | Kimi 动态工具 system 消息 | **禁止跨族**——AnthIR 无处安放,直接 L3 报错 | | `prefill` 语义 | IR 单一 `prefill` 标志 → DeepSeek `prefix:true`(且**必须切 `/beta`**)/ Kimi `partial:true` / Anthropic 4.5 及更早的末条 assistant;**Claude 4.6+ 与 Mythos Preview 一律 L3 拒绝** | | Kimi `partial` 的 `name` | Kimi 语义是"人设锚点",不是 OpenAI 的说话人区分——**转发时不要丢弃** | --- ## 5. 上游 Profile:把怪癖变成数据(L-C 的核心) 一份 Profile 描述一个**上游身份**(§2 表中的一行)。管线只读 Profile,不认 provider 名字。 ### 5.1 Schema ```jsonc { "id": "kimi-platform/openai", "family": "openai", // openai | anthropic —— 决定是否过桥 "base_url": "https://api.moonshot.cn/v1", "auth": {"scheme": "bearer", "key_prefix": "sk-", "reject_prefix": ["sk-kimi-"]}, "params": { "policy": "whitelist", // 白名单:未列出的字段一律不发 "allow": ["model","messages","tools","tool_choice","stream","stop", "max_tokens","max_completion_tokens","response_format"], "clamp": {"temperature": [0, 1]}, "force": {"stream_options.include_usage": true}, // 仅当 stream=true "never_default": ["temperature","top_p","n","presence_penalty","frequency_penalty"], "couplings": [ {"when": "n > 1", "require": "temperature > 0", "else": "reject:400"} ] }, "model_overrides": { // 参数合法性按模型分裂 "kimi-k3": {"drop": ["temperature","top_p","n","presence_penalty","frequency_penalty"]}, "kimi-k2.7-code": {"drop": ["temperature","top_p","n","presence_penalty","frequency_penalty"], "thinking": {"toggle": "forbidden"}}, "moonshot-v1-*": {"clamp": {"temperature": [0,1], "n": [1,5]}} }, "thinking": { "carrier": "reasoning_content", // reasoning_content | thinking_block | reasoning_item "trust": "plaintext", // plaintext | fake_sig | opaque (§7.1) "toggle": {"param": "thinking.type", "off": "disabled", "on": "enabled"}, "effort": {"param": "reasoning_effort", "values": ["low","high","max"], "upstream_validates": false}, // false = 网关自己校验枚举 "replay": "optional", // required_on_tool | optional | forbidden "hidden_prompt_tokens": 67 // 思考开启时上游注入的隐藏 system prompt }, "models": { "policy": "gateway_whitelist", // 永不依赖上游校验 "reject_patterns": ["\\[.*\\]$"], // kimi-k3[1m] 之类 "echo_trustworthy": false // 响应 model 字段能否用于记账 }, "response": { "envelope": "none | code_data | base_resp", "strip_headers": ["msh-org-id","msh-uid","msh-project-id"], "scrub_error_message": ["org-[a-z0-9]+]+>"], "usage_dialect": "openai | anthropic | dual", "stream_usage_position": ["top_level","choices0","last_chunk"] // 全部探测 }, "errors": { "shapes": ["openai","anthropic","gateway_cn"], // 解析时全部尝试 "retry_class": {"engine_overloaded_error":"backoff", "rate_limit_reached_error":"throttle", "exceeded_current_quota_error":"stop"} }, "limits": {"timeout_s": 900, "rpm": 3, "concurrency": 1, "tier_probe_header": "msh-gid", "sdk_retry": "off"} } ``` ### 5.2 四家 Profile 关键值对照 这张表是转写引擎的**实际配置内容**,逐格都有实测或文档依据: | Profile 字段 | anthropic | openai/chat | deepseek/chat | deepseek/anthropic | kimi-platform/openai | kimi-platform/anthropic | kimi-coding/* | | --- | --- | --- | --- | --- | --- | --- | --- | | `temperature` 域 | 0–1 | 0–2 | 0–2(越界 400) | **0–2**(比官方宽) | K3 系列**必须省略** | 静默忽略 | 同 platform | | 越界处理 | clamp | 400 | 400 | 400 | **400** | 忽略 | 400 | | `n>1` | 不支持 | 1–128 | 400 | — | 400 | — | 400(文案不同) | | 关思考写法 | `thinking.type` 按代际三选一 | `reasoning_effort` | `thinking.type` **或** 顶层 `reasoning_effort:none` | `thinking.type`(**文档说的 `reasoning.effort` 无效**) | `thinking.type:disabled`(**文档说不可关,实测可关**) | 同左 | 同左 | | effort 校验 | 枚举严格 | 按模型 | **不校验**(非法值静默跑默认) | 不校验 | **不校验** | 不校验 | 默认档 `high` | | `tool_choice` 强制 | `any` / `tool` | `required` / 指定 | **思考开启时 400** | `any` **静默降级 auto**;指定 400 | 思考开启:`required` ✅ / 指定 400 | 指定 400 | 同 platform | | 图片输入 | base64 / url | url / base64 | **400** | **静默换占位文本** | base64 / `ms://`(**http URL 400**) | 同左 | 同左 | | `strict` | 支持 | 支持 | **beta,静默失效** | — | MFJS 自造方言 | — | — | | 流式 usage 位置 | `message_delta`(**累计值**) | 独立 chunk(需 `include_usage`) | **挂在最后一个正常 chunk** | 规范 | **由 `include_usage` 决定两种位置** | — | 同 platform | | 缓存 | 显式断点 ×4 | `prompt_cache_key` | 全自动,**64 token 粒度** | 同左 | 全自动(`cache_control` 空操作) | 同左 | 同左 | | 思考隐藏 prompt | — | — | **~79 token** | ~79 | **~67 token**(K3;coding 侧 79) | 同左 | `kimi-for-coding` **0** | | 超时 | 10 min(SDK 校验) | — | — | — | **900 s** | 900 s | 900 s | --- ## 6. 请求改写管线(R1–R9) **顺序固定**。每一步的输入输出都是 IR,便于单测与快照对比。 ### R1 入口归一 方言 → 族 IR,执行 api-design §3.4 的不变量。此处唯一的新增是 §4.1 的两条。 ### R2 模型解析 1. 查网关白名单(`(tenant, logical_model) → 上游身份 + 上游模型名`)。**未命中 404**,绝不兜底。 2. 按 `models.reject_patterns` 拒绝 `kimi-k3[1m]` 这类客户端侧后缀(Kimi 两个入口都 404,coding 侧更离谱:**返回 401 `invalid_authentication_error`**)。 3. 记录 `requested_model`(下游传的)与 `routed_model`(我们实际发的)——两者都要进账单(§8.5)。 > 为什么必须自建白名单:Kimi coding 对任意模型名返回 200,未知名**静默 fallback 到 `kimi-for-coding`(256K)**,用户以为在用 K3(1M),长上下文任务会在毫无提示的情况下超限失败。DeepSeek 把 `deepseek-chat` 静默映射到 `deepseek-v4-flash`。这两类静默替换只有网关能拦。 ### R3 族决策 `入口族 == Profile.family` → 直出;否则过桥。桥的产物仍是 IR。 ### R4 能力校验与降级 按 api-design §7 的 L1/L2/L3 三级判定,产出 `degradation[]`。**L3 默认 400**,除非 `ztoken.allow_degradation` 放行。 四家新增的 L3 项: | L3 项 | 触发上游 | 理由 | | --- | --- | --- | | 图片/文档输入 | deepseek 全入口、kimi(http URL) | DeepSeek 会静默替换成 `[Unsupported Image]`,产生 200 + 错误答案 | | 思考开启 + 强制指定函数 | kimi 全入口、deepseek | **不得靠关思考绕过**:Kimi K3 关思考 + 指定函数 → 200 但**模型幻觉出假数据**,比 400 危险得多 | | `prefill` → Claude 4.6+ | anthropic | 上游明确禁止 | | 动态工具 system 消息跨族 | 非 kimi 上游 | 无法表达 | ### R5 参数消毒(本管线最重要的一步) 按 `params.policy = whitelist` 执行,顺序: ``` 1. 丢弃不在 allow ∪ model_overrides.allow 中的字段 2. 应用 model_overrides.drop ← Kimi K3 剥离全部采样参数 3. clamp 到合法域 ← O→A 的 temperature >1 截为 1(不做线性缩放) 4. 校验 couplings ← n>1 必须 temperature>0(否则 Kimi 报的错无人能懂) 5. 应用 force ← 流式一律补 stream_options.include_usage=true 6. never_default 检查:确认没有任何一步给未传的参数补了默认值 ``` > 第 2 步是本项目 ROI 最高的一条规则:**对 Kimi 新模型剥离 `temperature/top_p/n/presence_penalty/frequency_penalty`**,一次同时解决"OpenAI 客户端默认 `temperature=0.7` 开箱 400"和"两个入口行为相反"两个问题——剥离后两侧行为一致。 > > 第 6 步是**断言**,不是转换。它防止后续维护者出于"对齐 OpenAI 默认值"的好意加回默认值——那会在 Kimi 上 400、在 moonshot-v1 上悄悄改变输出。 ### R6 thinking 编排 见 §7。 ### R7 结构化输出与工具 schema - `json_schema` **不做跨家 schema 翻译**。Profile 声明支持则透传,否则按能力矩阵降级(`json_object`,L3)。 - DeepSeek 的 `json_object` **要求 prompt 里出现 "json" 字面量**,否则硬 400。网关的选择:**检测到缺失时报一个自解释的 400**(`"DeepSeek 要求 prompt 含 'json' 字样"`),不要自作主张往用户 prompt 里注入 —— 注入会破坏前缀缓存(§11),且改变了用户的输入。 - Kimi 的 `strict` 走自造的 MFJS 方言,OpenAI 的 strict 有硬性 schema 子集要求(`additionalProperties:false` + 全字段 `required`)。**跨家时 `strict` 一律标 L3**。 ### R8 缓存前缀保护 在请求出网前做一次断言:本次请求的 `(tools, system, messages[0..k])` 序列化结果,与同会话上一次请求的对应前缀**逐字节相同**。不同则记 metric `cache_prefix_broken`。理由见 §11。 ### R9 上游怪癖注入 Profile 里的最后一批动作:header(`anthropic-version`、`X-DashScope-SSE`)、endpoint 选择(DeepSeek `prefix`/`FIM`/`strict` → `/beta`)、鉴权(key 前缀与 base URL **必须成对校验**,`sk-` 与 `sk-kimi-` 互换会得到两种不同文案的 401)。 --- ## 7. 推理内容转写(L-D 的第一战场) 四家都有"推理",但**它们不是同一种东西**。转写规则由**可信度等级**驱动,而不是 provider。 ### 7.1 三级可信度 | 级别 | 含义 | 上游 | 网关可以做什么 | | --- | --- | --- | --- | | **T0 明文** | 就是一段文本,无校验 | DeepSeek `reasoning_content`、Kimi platform `thinking` block(**无 signature 字段**) | 自由读、改、截断、丢弃 | | **T1 假签名** | 有 `signature` 字段但**服务端不校验** | DeepSeek Anthropic 入口(signature = 响应 id)、**Kimi coding**(12,946 字符的签名,实测替换成 `XXXX`、删除、伪造 thinking 文本**四种情况全部 200**) | 同 T0;**但要保留字段形状**,否则下游按 Anthropic 规范校验会失败 | | **T2 真密文** | 密文即推理本体,改一字节即 400 | **Anthropic 官方**(`signature`)、**OpenAI**(`encrypted_content`) | **只能 opaque 原样搬运** | > 这个分级是本方案相对 v0.2 的关键修正。v0.2 假定"所有 Anthropic 族上游的 signature 都要当 opaque blob 搬运"——实测表明**只有 Anthropic 官方真机如此**。对 Kimi coding 和 DeepSeek,把它当 opaque 搬运会白白付出存储与信封成本。 > > **反向的安全提示**:Kimi coding 的 signature 只是"长得像密码学签名"。网关**不得**把它当作"上游已验证推理内容"的证据,也**不得**在自己的对外 API 里模仿这种假签名——下游若据此信任内容,就是被我们误导。 ### 7.2 回传要求矩阵 | 上游 | 工具轮次内 | 普通多轮 | 不满足时 | | --- | --- | --- | --- | | Anthropic | **必须原样**(不可重排/编辑/部分丢弃,含 `redacted_thinking`) | 建议全传 | 400 `blocks ... cannot be modified` | | OpenAI | **必须带回 reasoning item**(`store:false`/ZDR 时含 `encrypted_content`) | 建议 | 推理链断裂 | | DeepSeek | **必须**(缺失直接 400) | 可省 | 400;**传空串 `""` 可过校验** ← 跨族丢失内容时的降级手段 | | Kimi | 可省(文档说必须,**实测 200 正常**) | 可省 | — | ### 7.3 四象限策略(保留 v0.2) | 入口族 | 上游族 | 策略 | | --- | --- | --- | | Claude | Claude | **原样透传**,不解包不重打包 | | OpenAI | OpenAI | 原样透传 `encrypted_content` / `reasoning_content` | | OpenAI | Claude | 摘要 → `reasoning_content`;T2 时真 signature 装信封放 `encrypted_content`,T0/T1 时**不装信封** | | Claude | OpenAI | `reasoning_content` → `thinking.thinking`;T2 需要信封,T0/T1 直接留空 | 信封格式 `ztk1.`,只走 body **绝不走 header**(Claude 4+ 的 signature 显著变长,header 截断后下一轮直接 400)。 ### 7.4 三条实现硬约束 1. **切换上游模型时必须剥离 thinking block**——Anthropic thinking 与生成它的模型绑定,其他模型**静默忽略但照样计费**。故障转移路径尤其容易漏。 2. **不得按 `type == "thinking"` 过滤**——会漏掉 `redacted_thinking`,直接破坏协议。 3. **思考的隐藏成本要显式记账**——DeepSeek 思考模式注入 ~79 token、Kimi K3 ~67 token 的隐藏 system prompt,按输入价计费。Kimi K3 一句"说:好"开思考烧 310 completion tokens、关思考 11 个,**成本差 28 倍**。网关应把"思考开关"作为一等路由参数暴露给租户,并在账单里分列。 --- ## 8. 响应改写管线(S1–S9) ### S1 信封剥离 - Kimi 的 `/v1/users/me/balance`、`/v1/tokenizers/estimate-token-count` 返回 `{"code":0,"data":{…},"scode":"0x0","status":true}`;而 `/v1/chat/completions` 和 `/v1/models` **没有**这层包装。**同一 API 两种信封并存**,解析器不能一刀切。 - 预留 `base_resp` 通道(MiniMax 式 200 内嵌错误),Profile 里用 `response.envelope` 选择。 ### S2 双通道错误判定 HTTP 状态码 **+** body 业务码,两者任一为错即为错。见 §10。 ### S3 隐私剥离(**上线前必须完成**) | 泄漏点 | 动作 | | --- | --- | | `msh-org-id` / `msh-uid` / `msh-project-id` 响应头 | **白名单式剥离**(只放行网关自己合成的头 + `msh-request-id` 改名) | | Kimi 429 错误 message 里的 `org-xxx` | 正则擦除后重写文案(**这是账号 + key 标识泄漏**,且错误 message 通常被原样转发,比响应头更危险) | | Kimi 网关错误回显的 `ua` / `url` | 丢弃 | | Kimi coding `/v1/me`(手机号、昵称、登录时间) | **绝不透传**;用作健康检查时只保留 `status` | > 附带的运维结论:**Kimi coding 的 key 不可作为共享凭据下发给第三方**——持有 key 即可读到账号主人的部分身份信息。 ### S4 ID 重写 工具调用 ID → 网关不透明 ID(§4.2)。 ### S5 usage 归一 统一到 canonical usage: ```jsonc { "input_total": int, // 完整输入,含缓存读与缓存写 "input_cache_read": int, "input_cache_write": int, "output_total": int, // 含 reasoning tokens "output_reasoning": int } ``` 四家的坑: | 陷阱 | 规则 | | --- | --- | | Anthropic `input_tokens` **不含缓存** | `input_total = input_tokens + cache_read + cache_creation`(直接用 `input_tokens` 缓存命中时可低报 99%) | | **Kimi Anthropic 入口双套字段并存**且语义不同(`input_tokens:0` vs `prompt_tokens:88`) | Profile 标 `usage_dialect: dual`,**固定取 Anthropic 语义那套再补齐**,两套混用会差一个缓存量 | | Kimi `$web_search` 的 token **不在响应 usage 里** | 需从 `tool_calls[].function.arguments` 内嵌的 `usage.total_tokens` 解析(首轮 usage 93,实际 6674) | | DeepSeek 非思考模式**整个 `completion_tokens_details` 不出现** | 解析容忍缺失,不要当 0 之外的异常 | | Kimi 首次请求**无缓存字段**(不是 0,是字段不存在) | 同上 | **网关计费以上游原始 usage 为准,不依赖任何投影结果。** ### S6 模型三元记账 记录 `requested_model` / `routed_model` / `echoed_model` 三个值。当 `echo_trustworthy: false`(Kimi coding、`moonshot-v1-auto`)时,**账单与对外响应都用 `routed_model`**,`echoed_model` 只进日志。 ### S7 finish_reason / stop_reason 映射 按 cross-provider-mapping §4。补充: - `pause_turn` **不对外暴露**,网关内部完成服务端工具续跑循环后再返回终态。 - DeepSeek 的 `insufficient_system_resource` → OpenAI 侧无对应,映射 `stop` + L2 warning,原值进 `ztoken.upstream_finish_reason`。 - 预留 `sensitive`(智谱)→ `content_filter` 的映射通道。 ### S8 族投影 IR → 入口方言。两条易漏: - Chat 响应的 `choices[].logprobs` **必须存在**(值可为 `null`)——OpenAI spec 列为 required。 - Kimi OpenAI 入口错误体没有 `code`/`param`,对外补 `null` 以对齐严格客户端。 ### S9 可观测头注入 ``` X-Ztoken-Upstream: kimi-platform/openai:kimi-k3 (family=openai, bridged=false) X-Ztoken-Degraded: temperature=dropped(L2,model-fixed), strict=ignored(L3,allowed) X-Ztoken-Defaulted: max_tokens=4096 X-Ztoken-Request-Id: … ``` --- ## 9. 流式转写 ### 9.1 对外保证 1. 增量语义(上游全量累积时网关差分)。 2. 事件顺序合法(Claude 族三段式;OpenAI 族首 chunk 带 role、末尾 `[DONE]`)。 3. usage 位置固定。 4. 中途错误可表达。 ### 9.2 usage 位置探测(四家四种) | 上游 | 位置 | | --- | --- | | OpenAI | 独立 chunk,`choices: []`(需 `include_usage`) | | DeepSeek | **挂在带 `finish_reason` 的最后一个正常 chunk 上** ← 按 OpenAI 语义等独立 chunk 会永远拿不到 | | Kimi | 不传 `include_usage` → `choices[0].usage`;传了 → 顶层 + `choices:[]`(**且只有传了才带 `cached_tokens`**) | | Anthropic | `message_delta.usage`,**累计值**(取末值,不累加) | **统一策略**:R5 强制补 `include_usage=true`;解析时**三个位置全部探测**,取最后一次非空。 ### 9.3 跨族流式状态机 - **O→A**:OpenAI chunk 无"块结束"信号,靠 `tool_calls[].index` 变化与 `finish_reason` 推断边界,在文本↔工具切换时补发 `content_block_stop` / `content_block_start`。 - **A→O**:`content_block_start(text)` 吞掉;`input_json_delta.partial_json` 追加到 `arguments`;`thinking_delta` → `delta.reasoning_content`;**`signature_delta` 缓冲到 block 结束**,T2 时装信封随末尾 chunk 下发,T0/T1 时直接丢弃。 ### 9.4 解析器必须容忍的形态 - Anthropic `fallback` block:只有 `content_block_start` + `content_block_stop`,**中间没有任何 delta**。 - Anthropic `display:"omitted"`:只有一个 `signature_delta`,没有 `thinking_delta`。 - 未知事件类型:**优雅忽略**(Anthropic versioning policy 明确要求)。 - DeepSeek delta 中未用字段是显式 `null` 而非省略。 - Kimi 每个 chunk 带 `system_fingerprint`,非流式响应则没有。 - Kimi coding 的 SSE 事件名**没有空格**(`event:message_start`)——分隔符解析不能假设有空格。 - `data: [DONE]` 非 JSON,单独识别。 ### 9.5 中途错误 已发 200 后无法改状态码:Claude 族发 `event: error`;OpenAI 族发含 `error` 字段的 data chunk 再 `[DONE]`(无官方规范,属我们的事实约定,需写进对外文档)。 --- ## 10. 错误与重试归一 ### 10.1 上游错误体的三种形状(都要能解析) ```jsonc {"error":{"message":"…","type":"…"}} // OpenAI 风格 {"type":"error","error":{"type":"…","message":"…"},"request_id":"…"} // Anthropic 风格 {"code":5,"error":"url.not_found","message":"没找到对象","ua":"…"} // Kimi 网关风格 ``` 第三种里 **`error` 从 object 变成了 string**——假设 `error` 恒为 object 的解析器会抛类型异常。413 也可能返回**非 JSON**(Anthropic 侧由 Cloudflare 在到达 API 前返回)。 ### 10.2 状态码归一 | 上游 | 对外 | | --- | --- | | DeepSeek 402 余额不足 | 429 + `insufficient_quota` | | DeepSeek 422 | 400 | | Anthropic 529 overloaded | 503 | | **Kimi coding 模型不存在返回的 401** | **404 `model_not_found`** ← 绝不能触发凭据刷新逻辑 | | MiniMax 200 + `base_resp≠0` | 真实状态码 | ### 10.3 429 必须按 type 分流(三种处置完全相反) | type | 处置 | | --- | --- | | `engine_overloaded_error` | 退避重试 | | `rate_limit_reached_error` | 降并发 | | `exceeded_current_quota_error` | **停止**(欠费,重试纯属打爆上游) | ### 10.4 不可重试清单(会加倍烧钱或放大故障) - **Kimi "200 OK + 空 content + `finish_reason:length`"**——这是思考烧光 token 的正常返回,重试只会再烧一遍。 - DeepSeek `json_object` 偶发空 content——可重试,但要限次并记 metric。 - 任何 `exceeded_current_quota_error` / 402。 - **关闭上游 SDK 的自动重试**——它会把 1 次请求放大成 2–3 次真实调用,直接吃掉 RPM 配额(Kimi Tier 0 只有 3 RPM)。 ### 10.5 限流治理 - Kimi **没有 `x-ratelimit-*` 头**,配额只能本地计数 + 429 反馈,做不了提前避让;`retry-after` 是标准头,直接用。 - 用 `msh-gid` 响应头**自动探测账号档位**并设置本地并发上限(Tier 0 = 3 RPM / 1 并发,实测严格)。 - 上游超时 **≥900 s**(Kimi 服务端 900 s 才返回 504,网关先断会白烧已消耗的 token)。 - 对外**合成自己的 `x-ratelimit-*`**,不要透传上游的——上游额度和下游租户额度不是一回事。 --- ## 11. 缓存稳定性:一条被低估的转写约束 Kimi K3 的输入价差是 **10 倍**(命中 ¥2 / 未命中 ¥20 每 1M)。DeepSeek 缓存粒度 64 token、即时生效。Anthropic 是显式断点且**改动某层会使该层及之后所有层失效**(顺序 `tools → system → messages`)。 **对转写引擎的直接约束**: | 禁止的操作 | 原因 | | --- | --- | | 往 prompt 里注入内容(时间戳、system 提示、"json" 字样) | 前缀变化 → 全量 miss | | 规范化空白 / 重排消息 / 合并相邻文本块 | 同上 | | 把 `content` 数组压成字符串 | 前缀变化,且 Kimi 多模态下明确禁止 | | 动态改写 tools 定义顺序 | tools 是缓存第一层,失效面最大 | **允许的**:合并连续同 role 消息(AnthIR 不变量 3)——但**必须在会话首轮就确定策略并保持一致**,不能有的轮次合有的轮次不合。 > 落地建议:R8 的前缀断言接一个 metric,任何一次 `cache_prefix_broken` 都应当能定位到是管线哪一步改写导致的。这是转写引擎唯一直接与钱挂钩的可观测指标。 --- ## 12. 测试口径 转写引擎的正确性无法靠"跑通一次"证明,需要三类测试: 1. **黄金样本(golden files)**:每个上游身份 × 每种消息形态(纯文本 / 多模态 / 工具轮 / 思考轮 / 流式),存请求-响应对。管线改动后做快照 diff。**四家的实测原始响应体已在 docs 里,直接作为首批样本。** 2. **往返恒等**:`O→A→O` 与 `A→O→A` 的**可逆部分必须恒等**。不可逆部分(`is_error`、thinking 交错顺序、server tools)需在测试里**显式列举断言其丢失**——防止有人"顺手修好"后反而破坏了降级标记。 3. **上游契约探针**(定期跑,不进 CI 主流程):把 §5.2 表里的每一格变成一个真实请求断言。**四家的文档共有 13+ 条被实测推翻**(Kimi 10 条、DeepSeek 3 条),且两个方向的错都有——文档说不能实际能、文档说能实际不能。Profile 是照实测写的,探针就是它的回归网。 --- ## 13. 实现顺序 | # | 阶段 | 产出 | 备注 | | --- | --- | --- | --- | | 1 | 双 IR + 三入口方言层 | echo adapter 打通 | 纯逻辑,可完整单测 | | 2 | **Profile 装载 + R5 参数消毒** | 白名单/clamp/coupling 引擎 | **优先级高于跨族桥**——它单独就能让 Kimi/DeepSeek 从"开箱 400"变成可用 | | 3 | 能力矩阵 + 降级引擎(L1/L2/L3) | `X-Ztoken-Degraded` | 纯逻辑 | | 4 | 第一个上游:`deepseek/chat` | 同族直出 | 最简单,验证管线骨架 | | 5 | 跨族桥(非流式) | O↔A | 用往返恒等测试守住 | | 6 | 流式规范层 | usage 三处探测 + 事件容错 | 最易出 bug,先写对照测试 | | 7 | 跨族桥(流式) | 状态机 | **全项目最难**,单独设计 | | 8 | `anthropic` adapter | thinking T2 原样透传 | 唯一真 signature 路径 | | 9 | `kimi-platform` + `kimi-coding` | 两份独立 Profile | 含 §8 S3 隐私剥离,**上线前必须完成** | 第 5、7 步是技术核心;第 2、9 步是"能不能对外提供服务"的门槛。 **建议的 Java 模块划分**(对应当前空项目 `com.bytenya.ztoken`): ``` ztoken.ir — OaiIR / AnthIR 数据模型(不可变对象 + Jackson) ztoken.entry — chat / responses / messages 三个方言层 ztoken.bridge — 跨族桥(非流式 + 流式状态机) ztoken.profile — Profile 装载与校验(YAML/JSON 声明) ztoken.pipeline — R1–R9 / S1–S9,每步一个可单测的 Stage ztoken.upstream — HTTP 客户端、鉴权、超时、重试分类 ztoken.stream — SSE 编解码与规范化 ztoken.observe — 降级记录、usage 归一、计费事件 ``` --- ## 14. 开放问题 1. **`previous_response_id` / `conversation`**:当前 400 拒绝(网关无状态)。若要支持,建议信封化由客户端持有。 2. **L3 默认报错**是产品取向——会让"能跑就行"的客户端在我们这里失败、在别家静默成功。我认为值得(DeepSeek 的 `[Unsupported Image]` 就是反面教材),但需要产品确认。 3. **多模态代下载转码**:Kimi 不收 http 图片 URL。代下载会让网关变成任意 URL 的抓取代理(SSRF 风险),建议默认报错 + 可选开关。 4. **Kimi coding 的用量核算**:该上游**没有任何余额/用量端点**,只能自行累加 usage,无官方口径可对账。多租户计费需接受这个误差。 5. **thinking 可信度 T1 的对外表达**:我们知道 Kimi coding 的 signature 是假的,是否应该在响应里向下游标注?标注会泄漏上游身份,不标注则下游可能误信。