fix: normalise a bare tier string at the fourth door

`resolve_thinking` is exported, and its third argument turned from `bool`
into `Effort` this cycle — so the most natural downstream call now passes
the `"low"` it read out of JSON or config. Every gate inside compares by
identity, and `"none" is Effort.NONE` is always false: the tier was not
rejected, it was silently misjudged. Phase 2 read the on-form for a
request that wanted the off-form, Phase 4 was skipped entirely on a model
that cannot be disabled, and the only visible symptom arrived much later
as an AttributeError from `.value` — an exception that is neither
documented nor one of the four error classes.

Design 4.4 already lists this as the fourth entry a tier can come in
through; this makes the code agree with it.
This commit is contained in:
2026-09-05 06:49:34 -04:00
parent 32b92a8894
commit 4866e6b858
2 changed files with 57 additions and 2 deletions
+12 -2
View File
@@ -15,7 +15,7 @@ from typing import Any
from loguru import logger
from polygateway.providers import ProviderProfile, ThinkingWire
from polygateway.types import EFFORT_ORDER, Effort, ThinkingObservation
from polygateway.types import EFFORT_ORDER, Effort, ThinkingObservation, coerce_effort
def observe_thinking(*, thinking: str, reasoning_tokens: int | None) -> ThinkingObservation:
@@ -344,7 +344,7 @@ def effective_effort(
def resolve_thinking(
profile: ProviderProfile,
capability: ThinkingCapability | None,
effort: Effort | None,
effort: Effort | str | None,
*,
model: str,
fallback: str = "error",
@@ -385,7 +385,17 @@ def resolve_thinking(
`warn_unregistered=False` 供请求热路径去重用: 装配期已经喊过一次,逐次调用
再喊只会刷屏。判定结果不受此参数影响。
**裸字符串也收**(设计 §4.4 第 4 条入口): 本函数在 `__all__` 里,下游直调时
传的天然是从 JSON/配置读出来的 `"low"`,而第三参数本次由 `bool` 换成 `Effort`
正是这条入口冒出来的时机。签名照实写 `Effort | str`——下面每一关的判据都是
`is Effort.X` 的身份比较,`"none" is Effort.NONE` 恒假,不归一的后果不是报错
而是**静默判否**: Phase 2 按开启方向取字段、Phase 4 整条被绕过,最后在拼错误
文案时才以 `AttributeError` 现形(一个未文档化、也不属四分类的异常)。
"""
# Phase 0: 归一 —— 判据全是身份比较,入口不归一则后面每一关都在拿裸串比枚举
if effort is not None:
effort = coerce_effort(effort, origin=f"resolve_thinking(model={model!r})")
# Phase 1: 调用方不表态 —— 与 Effort.NONE 严格区分,用模型自己的默认档
if effort is None:
return ThinkingResolution({}, None)
+45
View File
@@ -443,6 +443,51 @@ class TestResolveThinking:
resolve_thinking(profile, cap, Effort.NONE, model="x-1")
assert "register_provider" not in str(exc.value), "形态已知,不该指向注册"
# —— 归一化: 本函数是档位进入库内的第四条入口(设计 §4.4) ——
def test_a_bare_string_tier_is_normalised_at_the_door(self):
"""`resolve_thinking` 在 `__all__` 里,下游直调时传的天然是裸串。
第三参数本次由 `bool` 换成 `Effort`,而下游最自然的写法是从 JSON/配置读出来
的 `"low"`。不在入口归一,`_inject` 撞 `.value` 抛的是 `AttributeError`——
一个未文档化、也不属错误四分类的异常(2026-09-05 独立验证查出)。
"""
got = resolve_thinking(
get_provider("zhipu"), get_capability("glm-5.3"), "low", model="glm-5.3"
)
assert got.payload == {"thinking": {"type": "enabled"}, "reasoning_effort": "low"}
assert got.applied_effort is Effort.LOW
def test_a_bare_none_string_still_means_the_off_tier(self):
"""裸 `"none"` 必须走到关闭形态,而不是被当成某个开启档。
身份比较 `"none" is Effort.NONE` 恒假,漏归一的后果是**静默判否**:
`_wire_unknown_for` 的 `effort is not Effort.NONE` 恒真,于是关闭请求会去看
`on_base`——正是设计 §2 处置表第 2 条点名要避免的误判方向。
"""
got = resolve_thinking(
get_provider("zhipu"), get_capability("glm-5.2"), "none", model="glm-5.2"
)
assert got.payload == {"thinking": {"type": "disabled"}}
assert got.applied_effort is Effort.NONE
def test_a_bare_none_string_reaches_phase4_on_a_model_that_cannot_disable(self):
"""漏归一时 Phase 4 整条被绕过: 关不掉的模型会被静默放行成"开启""""
with pytest.raises(ThinkingUnsupportedError, match="无法关闭推理") as exc:
resolve_thinking(
get_provider("zhipu"), get_capability("glm-5.3"), "none", model="glm-5.3"
)
assert "'low'" in str(exc.value), "Phase 4 的可执行替代不能丢"
def test_an_illegal_tier_string_names_this_function_as_the_origin(self):
"""非法档位报 `ValueError` 并指回**是哪一处**填错——档位有四条入口,不说清
就得让人自己去翻。"""
with pytest.raises(ValueError, match="resolve_thinking") as exc:
resolve_thinking(
get_provider("zhipu"), get_capability("glm-5.3"), "lowest", model="glm-5.3"
)
assert "非法推理档位" in str(exc.value)
class TestReconcileThinking:
"""声明 × 观测对账(设计 §4.3): 矛盾出文案,不表态出 None。