initial
This commit is contained in:
@@ -0,0 +1,551 @@
|
||||
# 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 <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[]` 可以。
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"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 模型为基线:
|
||||
|
||||
```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": {"<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:**
|
||||
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.<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 三条实现硬约束
|
||||
|
||||
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>` 在**匹配到该 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 步是这个项目的技术核心,其余都是工程量。
|
||||
Reference in New Issue
Block a user