A provider that registered a disable form but no enable form was told its shape was unknown and pointed at register_provider -- work it had already done -- for a request that only ever needed the disable form. The old bool code took the slot by direction; the tiered rewrite lost that. Take the relevant field again, and keep "shape unknown" for the case where both halves are missing, so the "cannot disable" wording still owns the half-missing case.
27 KiB
推理档位一等化设计(issue #20 及其一般形式)
- 日期: 2026-09-04
- 状态: 2026-09-04 人类已批准(经 Claude 自审 → Codex 独立审 → 人类审批门)
- 触发: issue #20 —— 智谱无 profile,下游只能手写
extra_body,本库为推理准备的三道机制被静默绕过 - 影响面:
SourceConfig/ChatRequest公共类型、ProviderProfile/ThinkingCapability公共类型、resolve_thinking/reconcile_thinking公共函数、缓存 key 公式(ARCH §7.5)、遥测端口(25 → 26 字段)、.env键 - 人类拍板(2026-09-04): 作用域取「源级默认 + 请求级覆盖」;档位不支持时「默认报错、可显式开映射」;不可关闭时「报错并给可执行替代」
- 人类复核(2026-09-04,针对 Codex 异议):
effort_fallback的最近档映射要实现,不因当前无已知消费者而推迟;方案选择标准 = 架构可维护性/清晰度 > 代码简洁 > 鲁棒性
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=未知的语义保留。判据须按请求档位取相关字段(旧版 slot = thinking_on if enable_thinking else thinking_off 即如此)——初稿 §4.1 Phase 2 写成「只看 on_base」是错的: 那会让「关闭形态已知、开启形态未知」的自定义 provider 在请求 none 时被误拒,且指路指向它已经做过的 register_provider,比不指更糟(2026-09-05 独立验证查出) |
| 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 用的是同一套):
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)
@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)
@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 有两层我们明确不做:
- 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)。一个协议 = 一层形态。 wireDialect代际方言(Claude 4.6+adaptivevs ≤4.5budget_tokens;Gemini 3thinkingLevelvs 2.xthinkingBudget)。这是原生协议才有的问题;我们发 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 | 该请求档所需的形态未知(请求 none 看 wire.off,其余档看 wire.on_base;none 方向须 off 与 on_base 皆为 None 才算「整体形态未知」——单 off is None 是「该 provider 关不掉」,归 _inject 说清缺的是哪半边,2026-09-05 实现时补正) |
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 审查异议与人类复核: Codex 指出本项当前无可复验的消费者,引入它要带来配置项、映射算法、warning 口径与测试面。人类 2026-09-04 复核后确认实现——理由是这条逃生口的价值不取决于今天有没有人用它:换模型是常态,而「换完就跑不起来」与「换完静默涨价」之间需要一个下游可以显式选择的中间档。故本项随本期一并实现,含映射方向、warning 口径与测试。默认关闭的理由是钱: 一次静默的
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,不附档位),不依赖能力表 |
True 的等价性分两种(2026-09-04 实现时发现,更正初稿「与旧行为逐字节等价」的说法):
| provider 类型 | 旧 thinking_on |
新 AUTO 注入 |
是否等价 |
|---|---|---|---|
on_base 完整表达「开」(qwen/deepseek/zhipu/moonshot) |
{"enable_thinking": True} 等 |
同左 | 逐字节等价 |
| 靠档位表达「开」(minimax/openai/anthropic/google) | {"reasoning_effort": "medium"} |
{}(不注入) |
行为变更 |
后者是有意的: 旧版那个 medium 是库替下游做的档位判断(profile 注释自己承认「取 medium 是因为它是五档里语义最接近厂商正常强度的一档」),而 medium 在 GLM/kimi/deepseek 的档位表里根本不存在——正是本设计要消灭的东西。语义仍是「开」(这些模型默认即推理),只是不再强制一个档;要指定强度请显式配 REASONING_EFFORT。须进 CHANGELOG 的行为变更条目。
| 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 的源字面量逐字不变。
key 用请求档,不用 nearest 映射后的生效档(T6 实现时定,理由在此补正): 决定性的原因是
CacheMW 位于洋葱中比 transport 更外的一层,查缓存时 resolve_thinking 尚未执行,生效档
根本拿不到。副作用是被映射到同一档的两个请求(minimal 与 low 都映射到 low)各占一个
缓存槽,存两份相同响应——浪费但不毒化,可接受。
由此带来一条已知边界(与 ARCH §7.5 既有两条并列,不在本设计处理): 能力表更新导致映射结果
变化时(如某模型新增 minimal 档),请求档 minimal 算出的 key 不变而实际发出的字节变了,
会命中按旧映射存下的响应。能力表版本不进 model_fingerprint 是既有取舍的延续(provider 表
与能力表都不在指纹里),要求严格隔离的调用方应换 cache_namespace 或 cache_salt。
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。分三档处置:
| 情形 | 处置 |
|---|---|
| 档位清单与「能否关闭」皆无冲突 | 直接登记 |
档位清单三源一致,仅「能否关闭」存疑(如 kimi-k3: 官方档位无 none,OpenRouter 却标 mandatory:false) |
按保守方向登记(不含 none),evidence 注明存疑点。理由: 不登记会退回 Phase 3 的「尽力注入」,下游配 none 时静默失效——那正是 issue #20 的病;保守登记则报错并给出最低档,明确且有出路 |
档位清单本身未知(未查到/推定同上) |
不登记,走 Phase 3 |
| 模型 | 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. 明确不做
- 档位与
reasoning_tokens的运行期对账(§4.3): 无可判定的函数关系,拿它报警是噪声。 - token 预算型控制(
thinking_budget/budget_tokens): qwen 系支持,但当前无下游需求;ThinkingWire可增量加budget_key抵达。 - 原生协议 wire 与代际方言(§3.4): 本库只有一个 OpenAI 兼容 transport。
- 档位对采样参数的联动: DeepSeek 思考模式不支持
temperature/top_p,Moonshot kimi-k2.5+ 固定采样参数,传别的值 400。本设计不代下游做参数裁剪——这是模型的约束,应由 evidence 记录并让 400 如实抛出,库替下游删参数是「默认值掩盖错误」。记入能力表 evidence,不写进代码逻辑。 - 压测本身: 「不同档位是不是真有用」是
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. 验收标准
- 五道关卡各有先失败后通过的测试证据;Phase 5 文案内容被断言。
- 同 messages 不同档位不再互相命中缓存。
- 遥测能按档位分组(压测的前置条件)。
.env只配ENABLE_THINKING的存量下游行为不变(回归测试)。- 进
DEFAULT_CAPABILITIES的条目仅限 §8 中无?/无冲突者,每条evidence标注「文档推定,待实测」并附出处;其余条目留在设计文档里等实测,不登记。 - import-linter 契约不破(
Effort落types.py,不产生反向依赖)。