38 KiB
zend-token 转写方案(Transform Spec)
状态:设计 v0.3 · 2026-08-13 范围:Claude / OpenAI / DeepSeek / Kimi 四家的双向转写实现规范。 与 api-design.md 的关系:那份定义对外契约(端点、IR、降级协议),本份定义转写引擎本身——上游画像、改写管线、逐字段规则、测试口径。v0.2 的双规范模型(OaiIR / AnthIR)在此保留不变,本文是它的落地层。 依据:anthropic.md、openai.md、deepseek.md、kimi.md(含实测)、cross-provider-mapping.md、compatibility-layers.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) |
三条贯穿全文的硬规则(违反任意一条,转写在生产上一定出事):
- 上游身份是三元组
(provider, entry_family, model),不是 provider。 同一家的两个入口对同一参数行为相反(Kimitemperature:OpenAI 入口 400 / Anthropic 入口静默忽略;DeepSeektool_choice: any:Anthropic 入口静默降级 / OpenAI 入口 400)。任何"按 provider 分支"的代码都是错的。 - 最小合法请求体。 只发 Profile 白名单里的字段,绝不为未传参数补默认值(Kimi K3 补
temperature=0.7直接 400,moonshot-v1 补默认值会改变原有行为)。 - 上游说什么都不算数。 模型名、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 消息归一的两条新约束
role: "developer"必须降级为system——DeepSeek 硬 400unknown variant 'developer'。归一发生在入口层(OaiIR 本就把 system/developer 提升到instructions),所以只要 §3.4 的不变量被执行,这条自动满足。但 passthrough 路径要单独检查。- 消息级扩展字段不得裁剪。 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
{
"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 域 |
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 模型解析
- 查网关白名单(
(tenant, logical_model) → 上游身份 + 上游模型名)。未命中 404,绝不兜底。 - 按
models.reject_patterns拒绝kimi-k3[1m]这类客户端侧后缀(Kimi 两个入口都 404,coding 侧更离谱:返回 401invalid_authentication_error)。 - 记录
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.<base64url(AES-GCM(payload))>,只走 body 绝不走 header(Claude 4+ 的 signature 显著变长,header 截断后下一轮直接 400)。
7.4 三条实现硬约束
- 切换上游模型时必须剥离 thinking block——Anthropic thinking 与生成它的模型绑定,其他模型静默忽略但照样计费。故障转移路径尤其容易漏。
- 不得按
type == "thinking"过滤——会漏掉redacted_thinking,直接破坏协议。 - 思考的隐藏成本要显式记账——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:
{
"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 对外保证
- 增量语义(上游全量累积时网关差分)。
- 事件顺序合法(Claude 族三段式;OpenAI 族首 chunk 带 role、末尾
[DONE])。 - usage 位置固定。
- 中途错误可表达。
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
fallbackblock:只有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 上游错误体的三种形状(都要能解析)
{"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. 测试口径
转写引擎的正确性无法靠"跑通一次"证明,需要三类测试:
- 黄金样本(golden files):每个上游身份 × 每种消息形态(纯文本 / 多模态 / 工具轮 / 思考轮 / 流式),存请求-响应对。管线改动后做快照 diff。四家的实测原始响应体已在 docs 里,直接作为首批样本。
- 往返恒等:
O→A→O与A→O→A的可逆部分必须恒等。不可逆部分(is_error、thinking 交错顺序、server tools)需在测试里显式列举断言其丢失——防止有人"顺手修好"后反而破坏了降级标记。 - 上游契约探针(定期跑,不进 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. 开放问题
previous_response_id/conversation:当前 400 拒绝(网关无状态)。若要支持,建议信封化由客户端持有。- L3 默认报错是产品取向——会让"能跑就行"的客户端在我们这里失败、在别家静默成功。我认为值得(DeepSeek 的
[Unsupported Image]就是反面教材),但需要产品确认。 - 多模态代下载转码:Kimi 不收 http 图片 URL。代下载会让网关变成任意 URL 的抓取代理(SSRF 风险),建议默认报错 + 可选开关。
- Kimi coding 的用量核算:该上游没有任何余额/用量端点,只能自行累加 usage,无官方口径可对账。多租户计费需接受这个误差。
- thinking 可信度 T1 的对外表达:我们知道 Kimi coding 的 signature 是假的,是否应该在响应里向下游标注?标注会泄漏上游身份,不标注则下游可能误信。