docs: document reasoning ownership and explicit cache migration
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 推理档位一等化设计(issue #20 及其一般形式)
|
||||
|
||||
> **替代指针(2026-09-09)**:§3–6/8/12 的 AUTO 无条件放行、MiniMax 内置 medium、受管 raw 覆盖与缓存迁移/遥测总括语义,以[1.3.4 已批准设计](2026-09-09-134-thinking-contracts-design.md) §4–8 为准。历史调研与实验事实保留,不倒改为新语义已验证。
|
||||
|
||||
- **日期**: 2026-09-04
|
||||
- **状态**: **2026-09-04 人类已批准**(经 Claude 自审 → Codex 独立审 → 人类审批门)
|
||||
- **触发**: issue #20 —— 智谱无 profile,下游只能手写 `extra_body`,本库为推理准备的三道机制被**静默**绕过
|
||||
@@ -16,7 +18,7 @@ issue #20 的字面诉求是补一条 `zhipu` profile。补上它**不能**解
|
||||
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 实测一致 |
|
||||
@@ -32,7 +34,7 @@ issue #20 的字面诉求是补一条 `zhipu` profile。补上它**不能**解
|
||||
替换 `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) |
|
||||
@@ -82,7 +84,7 @@ class ThinkingCapability:
|
||||
三个派生量,不单独存字段(存了就会漂移):
|
||||
|
||||
| 派生 | 定义 | 用途 |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `can_disable` | `Effort.NONE in supported_efforts` | 兼容旧语义 |
|
||||
| `cheapest_effort` | 除 `none` 外的第一档 | 不可关闭时的可执行替代(§4.1 Phase 5) |
|
||||
| 是否档位型 | 除 `none`/`auto` 外仍有 ≥1 档 | 决定告警文案(纯开关型不该说「可选档位」) |
|
||||
@@ -104,7 +106,7 @@ class ThinkingWire:
|
||||
四个形态样例(经 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` |
|
||||
@@ -126,7 +128,7 @@ cherry 有两层我们**明确不做**:
|
||||
判定顺序即语义。前三关是既有的,判据从 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 尽力注入,**不校验档位** |
|
||||
@@ -154,14 +156,14 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
`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}` 等 | 同左 | **逐字节等价** |
|
||||
| 靠档位表达「开」(openai/anthropic/google) | `{"reasoning_effort": "medium"}` | `{}`(不注入) | **行为变更** |
|
||||
| 同上但**默认不推理**(minimax) | `{"reasoning_effort": "medium"}` | 同左(2026-09-05 回退) | **逐字节等价** |
|
||||
@@ -189,7 +191,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
已知三条入口,缺一即漏:
|
||||
|
||||
| # | 入口 | 归一点 |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| 1 | `.env` / `from_env()` / `from_settings()` | `config._cast` 委托 `coerce_effort` |
|
||||
| 2 | 构造函数全量注入 `SourceConfig(...)` 与 `chat(reasoning_effort=...)` | `SourceConfig.__post_init__` / `chat()` 入口 |
|
||||
| 3 | **缓存命中回放** `LLMResponse.applied_effort` | `CacheMW._coerce_applied_effort` |
|
||||
@@ -211,7 +213,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
真正需要处置的是两处,均因请求级档位而新增:
|
||||
|
||||
| 层 | 处置 | 理由 |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| 源级 `reasoning_effort` | 并入 `_fingerprint_mark`,与 `enable_thinking` 同规则(**仅表态时**追加) | 与既有一致;全源不表态时指纹字面量不变,存量缓存不冷启动 |
|
||||
| 请求级 `reasoning_effort` | 进 `build_cache_key`,仅非 `None` 时参与 | `model_fingerprint` 是**装配期**算的集合级指纹,覆盖不到逐调用变化的值。不进 key 则同 messages 跑 low 与 max 会互相命中——issue #4「5 个 seed 全命中同一响应」的逐字翻版 |
|
||||
|
||||
@@ -240,7 +242,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
## 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** |
|
||||
@@ -254,7 +256,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
**落库规则**(Codex 审查补): 本表是**调研素材**,不是可直接转代码的表。只有 `supported_efforts` 能写成合法 `Effort` 元组的条目才进 `DEFAULT_CAPABILITIES`。分三档处置:
|
||||
|
||||
| 情形 | 处置 |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| 档位清单与「能否关闭」皆无冲突 | 直接登记 |
|
||||
| **档位清单三源一致,仅「能否关闭」存疑**(如 kimi-k3: 官方档位无 `none`,OpenRouter 却标 `mandatory:false`) | 按**保守方向**登记(不含 `none`),evidence 注明存疑点。理由: 不登记会退回 Phase 3 的「尽力注入」,下游配 `none` 时静默失效——**那正是 issue #20 的病**;保守登记则报错并给出最低档,明确且有出路 |
|
||||
| 档位清单本身无该型号直接证据(`未查到`,或仅由**同系**推定如 `推定同上`) | 不登记,走 Phase 3 |
|
||||
@@ -262,7 +264,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
第二档与第三档的分界是**有没有该型号自己的档位证据**,不是「关不关得掉存不存疑」: `kimi-k3` 进第二档,因为月之暗面官方文档直接写明它的三档是 `low/high/max`,只有「能否关」两源分歧;而 `gemini-3-flash`、`claude-haiku-5` 的档位清单是从同系型号(3.1-pro / opus-5)推来的,**没有该型号自己的文档**,故进第三档。Phase 3 并非静默——它会 warning 指路「实测后用 `register_capability` 登记」,且未登记模型的运行期对账文案也专门写了这一句;登记一个纯推定值反而会让下游以为库确认过。T10 实测时这三个型号优先补。`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`——**待实测**) |
|
||||
@@ -283,7 +285,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
## 9. 非功能维度
|
||||
|
||||
| 维度 | 回答 |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| **并发** | 两张表仍是 `MappingProxyType` + 纯函数查找,无共享可变状态。transport 的 `_warned_models`/`_warned_mismatches` 是实例级 `set`,读写之间无 `await`,单事件循环内原子。节流键加入生效档位后基数上升(源×模型×档位),仍为有界小集合 |
|
||||
| **取消** | 档位解析全部是同步纯函数,不含 `await`,不改变 `CancelledError` 的穿透路径。既有保证不受影响 |
|
||||
| **降级方向** | 推理档位属**请求正确性**而非资源闸,故一律**报错不放行**(Phase 2/4/5(下同)),与「限流/熔断后端不可用须报错」同向。能力**未登记**是唯一例外——warning 后尽力注入,理由是新模型上线不该被库挡住(既有决策,保留) |
|
||||
@@ -299,7 +301,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
**测试策略**(先失败后通过,每条对应一个行为):
|
||||
|
||||
| 层 | 用例 |
|
||||
|---|---|
|
||||
| --- | --- |
|
||||
| unit | 五道关卡各自的触发与不触发;`enable_thinking` 语法糖的三种等价;矛盾配置构造期报错;`nearest` 映射的取档方向;派生量(`can_disable`/`cheapest_effort`)与 `supported_efforts` 一致 |
|
||||
| unit | Phase 4 文案**含** `cheapest_effort` 与 env 键名(这是交付物,要断言内容而非只断言抛错) |
|
||||
| unit | 缓存 key: 同 messages 不同档位 → key 不同;不表态时 key 与存量形状一致(回归) |
|
||||
@@ -319,7 +321,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
**破坏性变更五处**(初稿只列了第 1 条,其余四条为 2026-09-05 独立验证实测补全——照初稿写 CHANGELOG 会让下游撞上没有预告的 `TypeError`):
|
||||
|
||||
| # | 位置 | 变更 | 谁会断 |
|
||||
|---|---|---|---|
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `ThinkingCapability` | 构造签名 `can_disable` → `supported_efforts` | 自建能力表的调用方 |
|
||||
| 2 | `ports.Transport.complete()` | 新增**无默认值**参数 `reasoning_effort` | 任何自建 transport 实现 |
|
||||
| 3 | `ports.TelemetryRecorder.record_llm_call()` | 新增无默认值参数 `reasoning_effort` | 任何自建 recorder 实现 |
|
||||
@@ -333,7 +335,7 @@ request.reasoning_effort > source.reasoning_effort > source.enable_thinking(语
|
||||
真实的库内调用点(可复验):
|
||||
|
||||
| 位置 | 用法 | 处置 |
|
||||
|---|---|---|
|
||||
| --- | --- | --- |
|
||||
| `thinking.py:169` | 读 `capability.can_disable` | 改读派生属性,行为不变 |
|
||||
| `tests/unit/test_thinking.py:128` | `ThinkingCapability(True, "实测")` **位置参数构造** | 随实现同步改——这是不可兼容的部分 |
|
||||
| `tests/e2e/test_thinking_live.py:455` | 读 `can_disable` | 派生属性覆盖 |
|
||||
|
||||
Reference in New Issue
Block a user