26 KiB
zend-token API 设计
状态:设计草案 v0.2 · 2026-08-12 依据:openai.md、anthropic.md、deepseek.md、china-providers.md、cross-provider-mapping.md
v0.2 变更:单一 IR → 双规范模型(§3);
/v1/responses提升为一等入口。后续:本文的落地层是 transform-spec.md(转写方案 v0.3),它在双规范模型之上补齐了上游 Profile、改写管线与 Kimi 实测结论。以下两处已被 v0.3 修正:§8.1 的"Claude 族上游一律 opaque 搬运 signature"只对 Anthropic 官方真机成立(见 transform-spec §7.1 的可信度分级);§6 的能力矩阵需按上游身份三元组而非 provider 划分(同一家的两个入口行为可能相反)。
本文只定义对外 API 契约与桥接语义,不涉及技术栈、存储、部署选型。
1. 设计原则
- 双族对等 — OpenAI 族与 Claude 族是同等公民,不是"主格式 + 转换层"。
- 同族零损 — 同族内的入口/上游组合不经过跨族转换,不付出任何有损代价。
- 有损集中 — 所有跨族有损只发生在一处(§5 跨族桥),集中实现、集中测试、集中记录。
- 降级必须可见 — 上游能力不足导致的任何语义变化都要可观测。静默忽略是各家的通病,我们不重复它(§7)。
- 上游怪癖不外泄 — MiniMax 的
base_resp、阿里的全量流、智谱的sensitive都在网关内消化。 - 不发明第三种方言 — 扩展一律走命名空间(
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[] 可以。
{
"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 模型为基线:
{
"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:
system/developer消息全部提升到instructions,按原顺序用\n拼接。- Chat 的
toolrole 消息转成function_call_outputitem。 - Chat 的
assistant.tool_calls[]拆成独立的function_callitem,排在该 message item 之后。
AnthIR:
role只有user/assistant。content恒为数组(裸字符串包装成[{type:"text"}])。- 连续同 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_callitem →message.tool_calls[](call_id→id)reasoningitem →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 对象(未知子字段忽略不报错):
{
"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,不做勉强转换:
- Claude 入口 + 请求含
thinking+ 非 Claude 族上游 + 后续要回传 — 见 §8.4。 n > 1且上游为 Claude 族 — 除非allow_degradation含n。- 含
server_tool_use历史的会话切到 OpenAI 族上游 — 历史无法表达,续接必然错乱。
6. 模型路由与能力矩阵
6.1 逻辑模型名
{
"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 能力矩阵
"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 默认报错是本设计与所有现有兼容层的最大分歧。宁可让调用方显式声明"我接受图片被丢弃",也不要让他拿到一个看起来正常、实际模型根本没看到图的响应。放行方式:
"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.<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 三条实现硬约束
- 切换上游模型时必须剥离 thinking block —— 其他模型静默忽略但照样计费。故障转移路径尤其注意。
- 信封只走 body,绝不走 header —— Claude 4+ 的 signature 显著变长,header 有截断风险,截断的 signature 下一轮直接 400。
- 不得按
type == "thinking"过滤 —— 会漏掉redacted_thinking,直接破坏协议。
9. 流式契约
9.1 对外保证
无论上游行为如何:
- 增量语义 — 每个事件只含新增内容。上游返回全量累积时(阿里
incremental_output=false)网关做差分,由能力矩阵streaming_incremental: false驱动。 - 事件顺序合法 — Claude 族保证
message_start→ block 三段式 →message_delta→message_stop;OpenAI 族保证首 chunk 带 role、末尾data: [DONE]。 - usage 位置固定 — Chat 下需
stream_options.include_usage才发末尾 usage chunk(choices为空数组);Messages 下message_delta.usage为累计值。 - 中途错误可表达 — §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 chunkinput_json_delta.partial_json→ 追加到argumentsthinking_delta→delta.reasoning_contentsignature_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 状态码两族一致:
// 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. 开放问题
previous_response_id/conversation要不要支持? 支持就意味着网关有状态(要存会话)。当前设计是 400 拒绝。若要支持,建议只做"信封化"——把历史塞进加密信封由客户端持有,保持无状态。- L3 默认报错是否可接受?会让"能跑就行"的客户端在我们这里失败、在别家静默成功。我认为值得,但这是产品取向。
- 信封是否需对客户端可解密? 目前完全不透明(网关持密钥)。若要客户端可自查,需改成签名而非加密。
cache_control对非 Anthropic 上游:接受并 L1 丢弃,还是直接拒绝?- 多模态是否代下载转码? 图片 base64/url 支持度各家不一,代下载会引入出网请求和体积限制。
13. 实现顺序
- 两个族规范模型 + 三个入口方言层(不接上游,用 echo adapter 打通)
- 能力矩阵 + 降级引擎(§7,纯逻辑,可完整单测)
- 第一个上游 adapter:DeepSeek(OpenAI 兼容,同族直出,最简单)
- 跨族桥的非流式路径(§5)—— 用双向往返测试保证:
O→A→O与A→O→A的可逆部分必须恒等 - 流式规范化层(§9,含阿里全量差分)—— 最易出 bug,值得先写对照测试
- 跨族桥的流式路径(§9.2)—— 全项目最难的一块,状态机需单独设计
- Anthropic adapter(thinking 原样透传路径)
- 国产 adapter 批量接入
第 4 步和第 6 步是这个项目的技术核心,其余都是工程量。