This commit is contained in:
2026-08-23 02:12:08 +08:00
commit 5987b2a1f2
19 changed files with 4174 additions and 0 deletions
+575
View File
@@ -0,0 +1,575 @@
# 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` 直接 400moonshot-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_<random>`),维护 `(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]+<ak-[^>]+>"],
"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` 域 | 01 | 02 | 02(越界 400 | **02**(比官方宽) | K3 系列**必须省略** | 静默忽略 | 同 platform |
| 越界处理 | clamp | 400 | 400 | 400 | **400** | 忽略 | 400 |
| `n>1` | 不支持 | 1128 | 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**K3coding 侧 79 | 同左 | `kimi-for-coding` **0** |
| 超时 | 10 minSDK 校验) | — | — | — | **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 全入口、kimihttp 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.<base64url(AES-GCM(payload))>`,只走 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<ak-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 — R1R9 / S1S9,每步一个可单测的 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 是假的,是否应该在响应里向下游标注?标注会泄漏上游身份,不标注则下游可能误信。