test: verify the thinking switch against the live API (issue #5, #6)

A sixteen-row matrix over 127 real calls: disable and enable on
MiniMax-M3 in both streaming and non-streaming mode, extra_body winning
over the profile slot, qwen and deepseek still disabling correctly, a
drift sentinel that re-derives every registered capability from live
behaviour, and the assembly guard refusing the models that cannot
comply.

Two judgement criteria had to be corrected by the data they were meant
to judge. Output length cannot separate the two regimes at all -- the
disabled runs reach 46 tokens when the model narrates its working in
the visible answer, and the enabled runs drop to 13 when medium effort
barely thinks. reasoning_tokens separates them cleanly in both
directions, which is precisely what issue #6 was collected for. A
second anchor compares prompt_tokens between the two regimes: the
vendor injects a reasoning instruction when thinking is on, so the
input side grows, and comparing the two runs relatively avoids
hardcoding any vendor number.

Provider names are mapped explicitly rather than guessed from the model
string; guessing had silently skipped the qwen row behind a "source
unavailable" reason that was not true.
This commit is contained in:
2026-08-02 06:55:38 -04:00
parent 82f4ec4910
commit 4c135075b3
6 changed files with 437 additions and 5 deletions
+26
View File
@@ -1,5 +1,31 @@
# Changelog
## 未发布(issue #5 + #6)
推理开关能力建模与 `reasoning_tokens` 采集。`enable_thinking=False` 此前对 `minimax` / `openai` 两类源**完全不产生效果**——两个 profile 的 thinking 两档皆为空字典,`payload.update({})` 是空操作,而配置方以为关掉了推理。这比"不提供这个开关"更危险:不提供的话调用方会去找别的办法,提供了但静默失效,调用方就带着一个错误的前提往下走。一个下游项目正卡在这上面。
### 行为变更(**请先读这一条**)
- **MiniMax 源的 `ENABLE_THINKING` 从"无效"变为"生效"。** 经实测,MiniMax 认的开关是 `reasoning_effort` 而非 `enable_thinking` / `thinking`(后两者被静默丢弃);现在 `False` 注入 `reasoning_effort: none``True` 注入 `medium`。此前依赖"设了 false 但其实没关"这一实际行为的调用方,行为会变。
- **`MiniMax-M2.7` / `MiniMax-M2.5``ENABLE_THINKING=false` 会在装配期报错。** 这两个模型的推理**关不掉**,是模型固有属性(三种参数形态各 15 轮实测全部无效,OpenRouter 与 models.dev 两个外部注册表独立登记为强制推理)。调用方要的是"不推理"的语义保证,给不了就必须说,而不是装出一个骗人的 client。
- **`provider=openai` 的源配任何非 `None``ENABLE_THINKING` 会在装配期报错。** 该段名实践中被复用为任意 OpenAI 兼容厂商的兜底,向未知厂商下发厂商方言参数会 400。要控制推理请 `register_provider` 注册形态,或用 `SourceConfig.extra_body` 直接下发。
- **`enable_thinking` 进入缓存指纹。** 它现在真的改变请求体,不进指纹就会出现"关掉推理后重启读到开着推理时的旧响应"。**配了该项的 scope 会有一次性冷启动**;未配的 scope 指纹字面量逐字不变,不受影响。
### 新增
- **`LLMResponse` / `TransportResult` 新增 `reasoning_tokens: int | None`**(issue #6)。推理 token 已计入 `completion_tokens`,故**成本总额一直是对的**——这不是计费缺口,是归因缺口:缺了它,"这次调用花的钱里有多少花在推理上"无法区分。
- **遥测表 `llm_calls` 新增 `reasoning_tokens` 列**,`TelemetryRecorder` 端口由 21 字段扩为 22;补列纪律与 issue #3/#4 逐字相同(排末尾、先探测再 ALTER、失败只逐行降级)。
- **`ProviderProfile` 的 thinking 两档类型放宽为 `Mapping | None`**,三值语义互不重叠:`{...}` 已知注入片段 / `{}` 已知无需注入 / `None` **未知**。空字典曾同时承载后两种含义,那正是本次 bug 的根因。
- **新增 model 级能力表** `ThinkingCapability` / `DEFAULT_CAPABILITIES` / `get_capability` / `register_capability`,以及单一判定函数 `resolve_thinking`。形态(参数长什么样)按 provider 变、数年不变一次;能力(能否关闭)按 model 变、每代都变——provider 级的表在物理上表达不了同厂代际差异。每条登记都附实测证据与日期。
### 下游请读
- **`reasoning_tokens``None` 是"本次调用未上报",不是"该源不上报"**,与 `cached_prompt_tokens` 的 NULL 语义**不同**。中转网关在上游不返回 usage 时会用本地 tokenizer 补算并整体替换 usage 对象,把 `completion_tokens_details` 一并吃掉(实测同一请求 10 轮呈 6:4 双峰)。故判据须写 `in (None, 0)`;**写 `== 0` 的条件永远不成立**——实测三家供应商在未推理时都是整个 details 缺失,无人上报字面 `0`
- **不要用输出长度反推是否发生了推理。** 两档的 `completion_tokens` 分布是重叠的(实测关闭档最高 46、开启档最低 13),按阈值判两个方向都会误判。唯一可靠的判别量是 `reasoning_tokens`
- **`enable_thinking=True` 对 MiniMax 映射到 `medium` 档。** 它是五档旋钮而库给的是布尔开关,这个映射是库做的选择:`medium` 对应"厂商正常强度",与 qwen 的 `enable_thinking:true`、deepseek 的 `thinking:{enabled}` 同为"不指定预算、由模型自定"的语义。要精确控制档位用 `extra_body={"reasoning_effort": "..."}`,它的优先级高于 profile 注入。
- **未登记的模型不会被挡住**,按 provider 形态尽力注入并发一条 warning。新模型上线不该被库拦下,但也不该假装成功;实测后请用 `register_capability` 登记。
- **`pricing.py` 一行未改。** 推理 token 已含在 `completion_tokens` 内,单列计价即重复计费。
## 1.0.5(2026-07-31)
采样参数透传(issue #4)。`chat()` 此前没有任何途径设置 `temperature` / `seed` / `max_tokens`——全库检索 `temperature` 零命中,`ChatRequest.overlay` 虽会被并进请求体却只由结构化中间件填充,调用方够不着。对受控实验而言这是阻塞性的:解码温度未知且可能随供应商默认值变化,每格配置跑 5 个 seed 报出的标准差无从解释。