docs: design reasoning effort as a tier the boolean cannot express
issue #20 asks for one zhipu profile. Adding it does not fix what the issue describes: GLM-5.3 mandates thinking (three sources agree, the vendor included), so `none` is an undefined value we were sending, and `medium` — the tier our minimax profile hardcodes — does not exist on GLM, kimi or deepseek at all. So the gap is the type, not the table. Capability becomes a tier list where `none`'s presence answers "can it be turned off", and refusal carries the cheapest tier that model does support — a refusal with no way forward is what sent the caller to extra_body in the first place. Reviewed by Codex, which caught two claims that were wrong: source-level extra_body and enable_thinking already reach the cache key through the model fingerprint, and the three reference projects are not in the workspace, so "no callers" was a grep against absent directories.
This commit is contained in:
@@ -0,0 +1,291 @@
|
|||||||
|
# 推理档位一等化设计(issue #20 及其一般形式)
|
||||||
|
|
||||||
|
- **日期**: 2026-09-04
|
||||||
|
- **状态**: 待人类审批
|
||||||
|
- **触发**: issue #20 —— 智谱无 profile,下游只能手写 `extra_body`,本库为推理准备的三道机制被**静默**绕过
|
||||||
|
- **影响面**: `SourceConfig`/`ChatRequest` 公共类型、`ProviderProfile`/`ThinkingCapability` 公共类型、`resolve_thinking`/`reconcile_thinking` 公共函数、缓存 key 公式(ARCH §7.5)、遥测端口(25 → 26 字段)、`.env` 键
|
||||||
|
- **人类拍板(2026-09-04)**: 作用域取「源级默认 + 请求级覆盖」;档位不支持时「默认报错、可显式开映射」;不可关闭时「报错并给可执行替代」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 问题不是 issue #20 说的那个
|
||||||
|
|
||||||
|
issue #20 的字面诉求是补一条 `zhipu` profile。补上它**不能**解决它自己描述的失败,因为二态 bool 在新一代模型上无档可填。
|
||||||
|
|
||||||
|
2026-09-04 调研,四份独立注册表(cherry-studio 客户端注册表、OpenRouter `/models` 的 `reasoning` 字段、LiteLLM 模型元数据、我们自己的网关 new-api `relaykit/relayconvert/reasoning/`)与六家官方文档,三条结论直接推翻 issue #20 的建议:
|
||||||
|
|
||||||
|
| # | 结论 | 证据 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **GLM-5.3 官方强制推理**,`thinking.type` 只接受 `enabled`;官方档位 `low/high/max`,`none` **不是**它的档位 | 智谱官方文档;cherry `toggle:false`;OpenRouter `mandatory:true` 三源一致 |
|
||||||
|
| 2 | **`medium` 只在 GPT-5.x / Claude 5 / Gemini 3 三家存在** | 见 §8 档位表 |
|
||||||
|
| 3 | 不可关闭不是孤例: GLM-5.3 系、Gemini 3 Pro / 3.1 Pro 为 mandatory;MiniMax M2.x **接受 `disabled` 但不生效** | 官方文档;与本库 2026-08-02 实测一致 |
|
||||||
|
|
||||||
|
第 1 条意味着 issue #20 建议的 `can_disable=True` 不能登记:我们发出去的 `reasoning_effort:"none"` 是个**未定义值**,智谱按自己的方式处理(多半当最低档)。这正好解释 issue #20 自己观测到的「短提示词 rt≈1.2,5552 token 长上下文跳到 0/54/167」——低档本来就要想,只是短提示词下想得少。
|
||||||
|
|
||||||
|
第 2 条意味着现有 `minimax` profile 那条「`thinking_on` 统一取 medium」的约定,推广到 GLM/kimi/deepseek 上全部是空档。
|
||||||
|
|
||||||
|
**真实缺口**: `enable_thinking: bool | None` 这个类型表达不了现实。补数据不能修复类型。
|
||||||
|
|
||||||
|
## 2. 现状审计(旧行为逐条处置)
|
||||||
|
|
||||||
|
替换 `thinking.py` 的请求侧决策,响应侧与对账基本保留。逐条声明:
|
||||||
|
|
||||||
|
| # | 现有行为 | 处置 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `enable_thinking` 三态: None 不注入 / True 注入 on / False 注入 off | **保留**语义,降为 `reasoning_effort` 的语法糖(§4.2) |
|
||||||
|
| 2 | `ProviderProfile.thinking_on/off` 两个固定片段,`None`=形态未知 | **替换**为 `ThinkingWire`(§3.3);`None`=未知的语义**保留** |
|
||||||
|
| 3 | `ThinkingCapability.can_disable: bool` | **替换**为 `supported_efforts`;`can_disable` 成为 `'none' in supported_efforts` 的派生(§3.2) |
|
||||||
|
| 4 | `evidence: str` 强制附实测出处 | **保留**,且强化: 初始表全部标注「文档推定,待实测」 |
|
||||||
|
| 5 | `resolve_thinking` 四道关卡(不表态/形态未知/能力未登记/不可关闭) | **保留四关的顺序与语义**,判据从 bool 换成档位(§4.1) |
|
||||||
|
| 6 | 能力未登记 → warning 后尽力注入 | **保留**(新模型不该被库挡住,ARCH §5 R4) |
|
||||||
|
| 7 | `observe_thinking` 多信号裁定三态 | **保留**,不改一行 |
|
||||||
|
| 8 | `reconcile_thinking` 声明 × 观测对账,矛盾返回文案、不抛错 | **保留**,判据扩展到档位(§4.3) |
|
||||||
|
| 9 | transport 按 `(source, model, direction)` 节流告警 | **替换**: 节流键的 `direction` 换成生效档位——同一模型 low 与 max 是两个独立的矛盾 |
|
||||||
|
| 10 | `ThinkingUnsupportedError(ValueError)`,由 transport 翻译为 `RequestRejectedError` | **保留**,新增的档位错误走同一条路 |
|
||||||
|
| 11 | `_build_payload` 中 `resolve_thinking` 结果先于 `extra_body`/`overlay` | **保留**(顺序即优先级,issue #4 决策 A) |
|
||||||
|
| 12 | 缓存 key 不含任何推理参数 | **修复**(§5,现存缺口) |
|
||||||
|
| 13 | 遥测无档位列 | **新增**一列(§6) |
|
||||||
|
|
||||||
|
**有意放弃**: 无。第 3 条的 `can_disable` 是唯一的破坏性变更,迁移见 §12。
|
||||||
|
|
||||||
|
## 3. 数据模型
|
||||||
|
|
||||||
|
### 3.1 档位词汇
|
||||||
|
|
||||||
|
七档封闭枚举,取四家参考实现共同收敛的词汇(cherry / OpenRouter / LiteLLM / new-api 用的是同一套):
|
||||||
|
|
||||||
|
```python
|
||||||
|
class Effort(StrEnum):
|
||||||
|
NONE = "none"; AUTO = "auto" # 不推理 / 推理但档位由模型自定
|
||||||
|
MINIMAL = "minimal"; LOW = "low"; MEDIUM = "medium"
|
||||||
|
HIGH = "high"; XHIGH = "xhigh"; MAX = "max"
|
||||||
|
```
|
||||||
|
|
||||||
|
`none` 即「不推理」,与强度档同处一个词汇表——这是关键的表达力来源: 「能不能关」不再是独立的布尔,而是 `none` 在不在该模型的支持列表里。
|
||||||
|
|
||||||
|
`auto` 不可省(自审补): newapi 上 26 个模型里有 9 个是**纯开关型**(qwen 五个、MiniMax-M3、glm-5/5.1/4.6v),它们能开推理但没有档位名可填。没有 `auto` 就只能拿某个强度档冒充「开」,而那正是现有 `thinking_on` 硬编码 `medium` 的病根。`auto` 的 wire = `on_base` 不附 `effort_key`,恰好等于旧的 `thinking_on` 行为。四家参考实现都有这一档(cherry 的 canonical selection `'default'|'none'|'auto'|Effort`;new-api 的 `ModeAdaptive`)。
|
||||||
|
|
||||||
|
`Effort` 归 `types.py`(最内层纯值类型),与 `ThinkingObservation` 同处一处,理由相同: 它是 `SourceConfig`/`ChatRequest` 的字段类型,定义在决策模块会让 `types.py` 反向 import。
|
||||||
|
|
||||||
|
### 3.2 `ThinkingCapability`: 能力(按 model)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThinkingCapability:
|
||||||
|
supported_efforts: tuple[Effort, ...] # 顺序 = 由弱到强
|
||||||
|
evidence: str
|
||||||
|
```
|
||||||
|
|
||||||
|
**不设 `default_effort` 字段**(Codex 审查采纳): 初稿有此字段,唯一消费者是「`enable_thinking=True` 等价于哪档」;自审把该语法糖改成 `Effort.AUTO` 后它就没有消费方了——§4 不用它决策,§6 遥测在不表态时记 `NULL`(库并不观测模型内部默认档,记推定值等于把「没看见」说成「发生了」,违既有纪律)。厂商默认档是**文档知识**,写进 `evidence` 文本即可,不必升格为必须逐模型维护的 API 字段(P1 YAGNI)。
|
||||||
|
|
||||||
|
三个派生量,不单独存字段(存了就会漂移):
|
||||||
|
|
||||||
|
| 派生 | 定义 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| `can_disable` | `Effort.NONE in supported_efforts` | 兼容旧语义 |
|
||||||
|
| `cheapest_effort` | 除 `none` 外的第一档 | 不可关闭时的可执行替代(§4.1 Phase 5) |
|
||||||
|
| 是否档位型 | 除 `none`/`auto` 外仍有 ≥1 档 | 决定告警文案(纯开关型不该说「可选档位」) |
|
||||||
|
|
||||||
|
OpenRouter 与 LiteLLM 两家**独立收敛到了同一形状**(`supported_efforts`+`default_effort` / `reasoning_effort_levels`+`default_reasoning_effort`),这是「档位清单即能力」这一形状可靠的旁证。我们只取其前半——两家都是**面向展示**的目录(要在 UI 上显示默认档),本库是**执行**路径,默认档不参与任何判定,故不设该字段。
|
||||||
|
|
||||||
|
### 3.3 `ProviderProfile`: 形态(按 provider)
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThinkingWire:
|
||||||
|
off: Mapping[str, Any] | None # 关闭档的片段;None = 该 provider 无关闭形态
|
||||||
|
on_base: Mapping[str, Any] | None # 开启档的固定部分;None = 形态未知
|
||||||
|
effort_key: str | None # 档位写进哪个键;None = 该 provider 无档位概念
|
||||||
|
```
|
||||||
|
|
||||||
|
`ProviderProfile.thinking_on/thinking_off` 由 `thinking: ThinkingWire` 取代。`None` 表示「未知」这一语义原样保留(issue #5 的核心成果,不可退回)。
|
||||||
|
|
||||||
|
四个形态样例(经 new-api 中转的口径):
|
||||||
|
|
||||||
|
| provider | off | on_base | effort_key |
|
||||||
|
|---|---|---|---|
|
||||||
|
| zhipu | `{"thinking":{"type":"disabled"}}` | `{"thinking":{"type":"enabled"}}` | `reasoning_effort` |
|
||||||
|
| qwen | `{"enable_thinking": False}` | `{"enable_thinking": True}` | `None`(无档位,只有 toggle) |
|
||||||
|
| openai / anthropic / google | `{"reasoning_effort":"none"}` | `{}` | `reasoning_effort` |
|
||||||
|
| minimax | `{"reasoning_effort":"none"}` | `{}` | `reasoning_effort` |
|
||||||
|
|
||||||
|
### 3.4 为什么不需要 cherry 的 endpoint contract 与 wireDialect
|
||||||
|
|
||||||
|
cherry 有两层我们**明确不做**:
|
||||||
|
|
||||||
|
1. **endpoint-keyed 的 per-model wire 覆盖**。它需要这层,是因为同一模型在 `openai-chat` / `openai-responses` / `anthropic-messages` / `google-generate-content` 四种协议下形态不同。**本库只有一个 chat transport(`openai_compat.py`)**,所有请求都是 OpenAI 兼容形态,跨协议转换由 new-api 在服务端完成(它自己就有一层 canonical intent,见 `relaykit/relayconvert/reasoning/intent.go`)。一个协议 = 一层形态。
|
||||||
|
2. **`wireDialect` 代际方言**(Claude 4.6+ `adaptive` vs ≤4.5 `budget_tokens`;Gemini 3 `thinkingLevel` vs 2.x `thinkingBudget`)。这是**原生协议**才有的问题;我们发 OpenAI 形态的 `reasoning_effort`,代际差异由网关吸收。
|
||||||
|
|
||||||
|
同理,`glm-5.2`(有 `none` 档)与 `glm-5.3`(无 `none` 档)**共用同一份 wire**——差别落在 capability 的 `supported_efforts` 上。本库既有的「形态按 provider、能力按 model」分层,恰好容纳档位而无需新增一层。
|
||||||
|
|
||||||
|
## 4. 解析
|
||||||
|
|
||||||
|
### 4.1 `resolve_thinking`: 五道关卡
|
||||||
|
|
||||||
|
判定顺序即语义。前三关是既有的,判据从 bool 换成档位;**Phase 4「可执行替代」是新增的**,Phase 5 是既有第 4 关的档位化推广。
|
||||||
|
|
||||||
|
| Phase | 条件 | 结果 |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | 生效档位为 `None`(调用方不表态) | 返回 `{}`,不注入 |
|
||||||
|
| 2 | `wire.on_base is None`(形态未知) | `ThinkingUnsupportedError`,指路 `register_provider`/`extra_body` |
|
||||||
|
| 3 | 能力未登记 | warning 后按 wire 尽力注入,**不校验档位** |
|
||||||
|
| 4 | 请求 `none` 而该模型无 `none` 档 | `ThinkingUnsupportedError`,**给出 `cheapest_effort` 作为替代** |
|
||||||
|
| 5 | 其余档位不在 `supported_efforts` 且未开映射 | `ThinkingUnsupportedError`,列出该模型可选档 |
|
||||||
|
|
||||||
|
**4 必须先于 5**(自审补): `none` 只是 5 的一个特例,若让它落进 5 的通用分支,报错就退化成「不支持 none,可选 low/high/max」——丢掉了「这个模型根本关不掉」这个关键信息与可执行替代。
|
||||||
|
|
||||||
|
Phase 4 的文案是本设计的一个交付物,而非装饰:
|
||||||
|
|
||||||
|
> 模型 'glm-5.3' 无法关闭推理(官方 `thinking.type` 只接受 enabled);最省的档是 'low',请配 `LLM__ZHIPU__1__REASONING_EFFORT=low` 或调用时传 `reasoning_effort=Effort.LOW`。evidence: ...
|
||||||
|
|
||||||
|
理由: 该分支若只报错不给出路,下游会去找 `extra_body` 那条绕过的路——**那正是 issue #20 的成因**。报错必须带可执行替代,否则等于把用户推回起点。
|
||||||
|
|
||||||
|
映射(Phase 5 的逃生口)默认关闭,由 `SourceConfig.effort_fallback="nearest"` 显式开启,按 `supported_efforts` 的顺序取最近档并 warning。
|
||||||
|
|
||||||
|
> **Codex 审查异议(留待人类定夺)**: 本项当前无可复验的消费者——没有任何已知调用场景要求自动降/升档,引入它会带来配置项、映射算法、warning 口径与测试面。人类 2026-09-04 已明确选择「默认报错 + 可显式开映射」,故设计保留其形状;但**实现阶段可只落报错分支**,待真实需求出现再补映射,不影响 API 形状。默认关闭的理由是钱: 一次静默的 `medium→max` 在 GLM-5.3 上是数倍账单,「严禁默认值掩盖错误」(P5)在此有真金白银的含义。
|
||||||
|
|
||||||
|
### 4.2 生效档位的优先级
|
||||||
|
|
||||||
|
```
|
||||||
|
request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语法糖) > None
|
||||||
|
```
|
||||||
|
|
||||||
|
`enable_thinking` **保留不删**(它已被三项目消费,迁移兼容约束见 ARCH §5.1),降级为语法糖:
|
||||||
|
|
||||||
|
| 旧写法 | 等价于 |
|
||||||
|
|---|---|
|
||||||
|
| `enable_thinking=False` | `reasoning_effort=Effort.NONE` |
|
||||||
|
| `enable_thinking=True` | `reasoning_effort=Effort.AUTO`(注入 `on_base`,不附档位)——与旧行为逐字节等价,且不依赖能力表 |
|
||||||
|
| `enable_thinking=None` | 不表态 |
|
||||||
|
|
||||||
|
**同源同时配 `enable_thinking` 与 `reasoning_effort` 且语义矛盾**(如 `True` + `none`)→ **构造期 `ValueError`**。不做「后者赢」的静默兜底: 两个字段表达同一件事时,矛盾是配置错误,不是优先级问题。
|
||||||
|
|
||||||
|
### 4.3 `reconcile_thinking`: 对账扩展
|
||||||
|
|
||||||
|
现有对账只判「要求关闭却观测到推理」与「要求开启却未推理」。档位化后新增一类可判定的矛盾:
|
||||||
|
|
||||||
|
- 请求 `none`、模型登记 `can_disable=True`、却观测到 `OBSERVED` → 既有文案,**保留**(这正是 issue #20 第 3 条要恢复的机制)。
|
||||||
|
- 请求非 `none` 档、观测到 `ABSENT` → 既有文案,保留。
|
||||||
|
- **不做**「档位高低与 `reasoning_tokens` 多少的对账」: 档位与 token 数没有可判定的函数关系(issue #20 自己的数据里 glm-5.3-flash 的 medium 档 rt 在 8~56 之间跳),拿它报警必然是噪声。这条留给 §11 的压测,不进库。
|
||||||
|
|
||||||
|
## 5. 缓存 key
|
||||||
|
|
||||||
|
**更正一个误判(Codex 审查指出)**: 源级 `extra_body` 与 `enable_thinking` **早已进 key**——经 `build_model_fingerprint` 的 `_fingerprint_mark`(`client.py`),由 issue #4/#5 落地,ARCH §7.5 有明文。本设计**不存在**先前稿本断言的「现存毒化缺口」,那是把 `CacheMW` 只读 `request.sampling` 误当成了全部 key 来源。
|
||||||
|
|
||||||
|
真正需要处置的是两处,均因请求级档位而新增:
|
||||||
|
|
||||||
|
| 层 | 处置 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| 源级 `reasoning_effort` | 并入 `_fingerprint_mark`,与 `enable_thinking` 同规则(**仅表态时**追加) | 与既有一致;全源不表态时指纹字面量不变,存量缓存不冷启动 |
|
||||||
|
| 请求级 `reasoning_effort` | 进 `build_cache_key`,仅非 `None` 时参与 | `model_fingerprint` 是**装配期**算的集合级指纹,覆盖不到逐调用变化的值。不进 key 则同 messages 跑 low 与 max 会互相命中——issue #4「5 个 seed 全命中同一响应」的逐字翻版 |
|
||||||
|
|
||||||
|
**已知取舍原样延续**: ARCH §7.5 已记载 `model_fingerprint` 是**集合级**而非本次选中源的指纹,同 scope 各源配置不同时仍可能返回另一源的响应;要求逐源可复现应让每源独享 scope 或 namespace。加入 `reasoning_effort` 后该取舍不变,本设计不扩大战线去改它。
|
||||||
|
|
||||||
|
**冷启动代价**: 只有新配 `REASONING_EFFORT` 的源冷启动一次;存量只配 `ENABLE_THINKING` 的源字面量逐字不变。
|
||||||
|
|
||||||
|
## 6. 遥测
|
||||||
|
|
||||||
|
`llm_calls` 新增一列 `reasoning_effort TEXT`(INSERT 字段 25 → 26,物理列 26 → 27;两套口径的区分见 `telemetry/schema.py` 模块 docstring)。
|
||||||
|
|
||||||
|
记的是**本次调用生效的档位**,不是配置值——`None`(不表态)与 `'low'` 必须能区分,故可空。
|
||||||
|
|
||||||
|
不加此列则你要做的压测「不同档位是不是真有用」在数据侧无法分组: 现在 25 列里没有任何一列能回答「这一行用的是哪档」。补列走既有的 `PGW_TELEMETRY_SCHEMA_MODE` 机制,两端 DDL 与 `COLUMNS` 同源(schema.py 是单一事实源)。
|
||||||
|
|
||||||
|
## 7. 备选方案对比
|
||||||
|
|
||||||
|
| | 方案 | 改动面 | 权衡 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **A** | **最小补丁**: 只补 `zhipu` profile,`thinking_on` 填一个档,维持 bool | `providers.py` 一条 + `thinking.py` 两条 | issue #20 字面满足。但 §1 三条结论全部无解: GLM-5.3 填什么档都是错(`medium` 是空档、`none` 是未定义值);`can_disable` 只能在「让下游跑不起来」与「登记一个官方否认的能力」之间二选一。**治标** |
|
||||||
|
| **B** | **能力表档位化 + 源级/请求级双入口**(本设计) | `types.py` 加 `Effort`、两个公共类型重构、`resolve_thinking` 加两关、缓存 key、遥测加列、`.env` 加键 | 表达力对齐现实;下游不必再走 `extra_body`;压测可按档位分组。代价是公共类型破坏性变更 + 一次缓存冷启动 |
|
||||||
|
| **C** | **照抄 cherry 的完整 wire DSL**: closed operation 集合、`effortMap`、`budgetWire`、endpoint-keyed contract | B 的全部 + 一套 wire 解释器 + per-model wire 覆盖表 | 能表达 budget 型(qwen `thinking_budget`)与原生协议代际差异。但本库只有一个 OpenAI 兼容 transport(§3.4),这层复杂度当前无消费者——**违 P1 YAGNI** |
|
||||||
|
|
||||||
|
**推荐 B**。A 治不了 issue #20 描述的病;C 的两项额外能力(多协议 wire、token 预算)在本库当前没有消费者,等真出现 budget 型需求时,`ThinkingWire` 增一个 `budget_key` 字段即可增量抵达,不必现在就上解释器。
|
||||||
|
|
||||||
|
## 8. 初始能力表(全部标注「文档推定,待实测」)
|
||||||
|
|
||||||
|
来源: 官方文档 + OpenRouter + cherry-studio + LiteLLM 四方交叉。**这是待验证的假设,不是结论**——LiteLLM 里同一个 kimi-k3 在 `moonshot/` 下是三档、在 `perplexity/` 下是六档,中转会改档位有第三方证据。人类已定:能力表数据以后统一经 new-api 实测。
|
||||||
|
|
||||||
|
**落库规则**(Codex 审查补): 本表是**调研素材**,不是可直接转代码的表。只有 `supported_efforts` 能写成合法 `Effort` 元组的条目才进 `DEFAULT_CAPABILITIES`;标 `?`/`未查到`/两源打架的条目**一律不登记**——未登记走 `resolve_thinking` Phase 3(warning 后尽力注入)这条既有的正确退化路径,比登记一个猜测更诚实。`default` 列只是调研记录,按 §3.2 并入 `evidence` 文本,不进字段。
|
||||||
|
|
||||||
|
| 模型 | supported_efforts(推定) | 厂商默认(入 evidence) | 关? |
|
||||||
|
|---|---|---|---|
|
||||||
|
| glm-5.3, glm-5.3-flash | low, high, max | max | ✗ |
|
||||||
|
| glm-5.2 | none, high, max | max | ✓ |
|
||||||
|
| kimi-k3 | low, high, max | max | ?(OR 标可关,但官方档位无 `none`——**待实测**) |
|
||||||
|
| kimi-for-coding | 未查到 | — | ? |
|
||||||
|
| deepseek-v4-pro / -flash / -flash-vision-exp | none, high, max | high | ✓ |
|
||||||
|
| gpt-5.4, gpt-5.5 | none, low, medium, high, xhigh | medium | ✓ |
|
||||||
|
| claude-opus-5, claude-sonnet-5 | low, medium, high, xhigh, max(+`none` 经网关转 `thinking` 关闭) | high | ✓ |
|
||||||
|
| claude-haiku-5 | 推定同上 | — | ? |
|
||||||
|
| gemini-3.1-pro | low, medium, high | 官说 high / OR 说 medium(**打架**) | ✗ |
|
||||||
|
| gemini-3-flash | low, medium, high | — | ? |
|
||||||
|
| MiniMax-M3 | none, auto | auto | ✓ |
|
||||||
|
| MiniMax-M2.5, M2.7 | auto(**仅此一档**) | auto | ✗ |
|
||||||
|
| glm-5, glm-5.1, glm-4.6v | none, auto | auto | ✓ |
|
||||||
|
| qwen-plus-latest, qwen3.5-flash, qwen3.6-plus, qwen3.7-max, qwen3.7-plus | none, auto | auto | ✓ |
|
||||||
|
|
||||||
|
三个 embedding 模型(text-embedding-v2/v4、qwen3-vl-embedding)无推理语义,不入表。
|
||||||
|
|
||||||
|
## 9. 非功能维度
|
||||||
|
|
||||||
|
| 维度 | 回答 |
|
||||||
|
|---|---|
|
||||||
|
| **并发** | 两张表仍是 `MappingProxyType` + 纯函数查找,无共享可变状态。transport 的 `_warned_models`/`_warned_mismatches` 是实例级 `set`,读写之间无 `await`,单事件循环内原子。节流键加入生效档位后基数上升(源×模型×档位),仍为有界小集合 |
|
||||||
|
| **取消** | 档位解析全部是同步纯函数,不含 `await`,不改变 `CancelledError` 的穿透路径。既有保证不受影响 |
|
||||||
|
| **降级方向** | 推理档位属**请求正确性**而非资源闸,故一律**报错不放行**(Phase 2/4/5(下同)),与「限流/熔断后端不可用须报错」同向。能力**未登记**是唯一例外——warning 后尽力注入,理由是新模型上线不该被库挡住(既有决策,保留) |
|
||||||
|
| **幂等** | 纯函数,无副作用,同输入恒同输出。重复调用安全 |
|
||||||
|
| **持久化** | 两处一次性影响: ① 缓存 key 变化 → 已配推理参数的 namespace 冷启动一次;② 遥测补列 → 走既有 `PGW_TELEMETRY_SCHEMA_MODE`,补列语句与 DDL 同源。均无部分写入风险(补列是 DDL 原子操作,缓存 miss 不损坏数据) |
|
||||||
|
|
||||||
|
## 10. 错误处理与测试策略
|
||||||
|
|
||||||
|
**错误分类**: 全部落 `RequestRejectedError`(不重试、不换源、不计熔断)。理由: 档位不支持是确定性的配置/参数问题,重试与换源都不会让它变对。路径与既有一致——`thinking.py` 抛 `ThinkingUnsupportedError(ValueError)`,transport 在请求期翻译。
|
||||||
|
|
||||||
|
装配期 vs 运行期: 源级配置(`SourceConfig.reasoning_effort`)在**构造期**校验并报错;请求级(`ChatRequest.reasoning_effort`)只能在**运行期**校验,落 `RequestRejectedError` 上抛。
|
||||||
|
|
||||||
|
**测试策略**(先失败后通过,每条对应一个行为):
|
||||||
|
|
||||||
|
| 层 | 用例 |
|
||||||
|
|---|---|
|
||||||
|
| unit | 五道关卡各自的触发与不触发;`enable_thinking` 语法糖的三种等价;矛盾配置构造期报错;`nearest` 映射的取档方向;派生量(`can_disable`/`cheapest_effort`)与 `supported_efforts` 一致 |
|
||||||
|
| unit | Phase 4 文案**含** `cheapest_effort` 与 env 键名(这是交付物,要断言内容而非只断言抛错) |
|
||||||
|
| unit | 缓存 key: 同 messages 不同档位 → key 不同;不表态时 key 与存量形状一致(回归) |
|
||||||
|
| integration | 遥测 `reasoning_effort` 列在两端(sqlite/pg)落值正确,不表态时为 NULL |
|
||||||
|
| e2e(`slow`) | 经 new-api 对 §8 表逐模型实测,校正 `supported_efforts`;标 `slow`(成败取决于外部服务当下状态) |
|
||||||
|
|
||||||
|
## 11. 明确不做
|
||||||
|
|
||||||
|
1. **档位与 `reasoning_tokens` 的运行期对账**(§4.3): 无可判定的函数关系,拿它报警是噪声。
|
||||||
|
2. **token 预算型控制**(`thinking_budget`/`budget_tokens`): qwen 系支持,但当前无下游需求;`ThinkingWire` 可增量加 `budget_key` 抵达。
|
||||||
|
3. **原生协议 wire 与代际方言**(§3.4): 本库只有一个 OpenAI 兼容 transport。
|
||||||
|
4. **档位对采样参数的联动**: DeepSeek 思考模式不支持 `temperature`/`top_p`,Moonshot kimi-k2.5+ 固定采样参数,传别的值 400。**本设计不代下游做参数裁剪**——这是模型的约束,应由 evidence 记录并让 400 如实抛出,库替下游删参数是「默认值掩盖错误」。记入能力表 evidence,不写进代码逻辑。
|
||||||
|
5. **压测本身**: 「不同档位是不是真有用」是 `harness-eval` 范畴,依赖本设计的遥测列,不属于本设计。
|
||||||
|
|
||||||
|
## 12. 迁移与兼容
|
||||||
|
|
||||||
|
**破坏性变更一处**: `ThinkingCapability` 的构造签名(`can_disable` → `supported_efforts`)。
|
||||||
|
|
||||||
|
**先更正**(Codex 审查指出): 初稿称「已核实 `reference/` 三项目无调用点,实际影响面为零」——**该结论不成立**。`reference/` 下当前**没有** GovDoc-SaaS / Video-Tree-TRM5 / CHSAnalyzer 三个目录(工作区实际只有本次调研克隆的四个开源项目),此前的 `grep` 因目录不存在而输出空,被误读成「无匹配」。
|
||||||
|
|
||||||
|
真实的库内调用点(可复验):
|
||||||
|
|
||||||
|
| 位置 | 用法 | 处置 |
|
||||||
|
|---|---|---|
|
||||||
|
| `thinking.py:169` | 读 `capability.can_disable` | 改读派生属性,行为不变 |
|
||||||
|
| `tests/unit/test_thinking.py:128` | `ThinkingCapability(True, "实测")` **位置参数构造** | 随实现同步改——这是不可兼容的部分 |
|
||||||
|
| `tests/e2e/test_thinking_live.py:455` | 读 `can_disable` | 派生属性覆盖 |
|
||||||
|
| `__init__.py` | 包根导出 `ThinkingCapability`/`register_capability` | 符号名不变,构造形态变 |
|
||||||
|
|
||||||
|
**兼容策略**: 保留 `can_disable` 为只读派生属性(`Effort.NONE in supported_efforts`),**读侧代码一律不改**;位置参数构造无法兼容,库内三处随实现同步修改。
|
||||||
|
|
||||||
|
**下游影响面: 推断而非核实**。三项目尚未迁移接入本库(M4 才做),`ThinkingCapability` 是 2026-08-02 才加入的库内表,下游调用它的可能性低——但工作区读不到三项目源码,这条只能是推断。**须人类在审批时确认**,或在实现计划里加一步「三项目可读时复验调用点」。属公共 API 破坏性变更,走 minor 版本号(1.4.0)。
|
||||||
|
|
||||||
|
**非破坏**: `SourceConfig.enable_thinking` 保留,行为等价(§4.2);`.env` 的 `ENABLE_THINKING` 键保留;新增键 `{SCOPE}__{PROVIDER}__{N}__REASONING_EFFORT`。三项目不改配置即可继续跑,除非它们配的是「关闭一个官方不可关的模型」——那种情况**本来就是静默失效**,现在会明确报错并给出替代档。
|
||||||
|
|
||||||
|
## 13. 验收标准
|
||||||
|
|
||||||
|
1. 五道关卡各有先失败后通过的测试证据;Phase 5 文案内容被断言。
|
||||||
|
2. 同 messages 不同档位不再互相命中缓存。
|
||||||
|
3. 遥测能按档位分组(压测的前置条件)。
|
||||||
|
4. `.env` 只配 `ENABLE_THINKING` 的存量下游行为不变(回归测试)。
|
||||||
|
5. 进 `DEFAULT_CAPABILITIES` 的条目**仅限** §8 中无 `?`/无冲突者,每条 `evidence` 标注「文档推定,待实测」并附出处;其余条目留在设计文档里等实测,不登记。
|
||||||
|
6. import-linter 契约不破(`Effort` 落 `types.py`,不产生反向依赖)。
|
||||||
Reference in New Issue
Block a user