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:
@@ -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)
|
||||
|
||||
@@ -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。
|
||||
|
||||
Reference in New Issue
Block a user