feat: let one call ask for a different tier than its source defaults to
The three-layer priority (call > source > enable_thinking sugar > silence) now lives in one pure function, thinking.effective_effort(). The assembly guard and the request hot path used to each carry their own inline copy of the sugar conversion; two copies of the same judgement drift into the worst shape there is - passes at assembly, raises at runtime. The guard now also honours effort_fallback, so a source that opted into nearest is no longer sentenced at assembly for a tier it could have mapped.
This commit is contained in:
@@ -198,6 +198,57 @@ class TestSamplingOverlay:
|
||||
assert [c["seed"] for c in captured] == [1, 2]
|
||||
|
||||
|
||||
class TestReasoningEffortPriority:
|
||||
"""三层优先级: 请求级 > 源级 > `enable_thinking` 语法糖 > 不表态(设计 §4.2)。
|
||||
|
||||
一律抓**真实请求体**而非只查 `ChatRequest` 字段: 档位的价值全在发出去的那几个
|
||||
字节上,只断言中间态会让"字段填了但一路没人读"这种缺口继续通过测试——issue #20
|
||||
的 `extra_body` 绕行正是这么长出来的。
|
||||
"""
|
||||
|
||||
def _capturing_client(self, captured, **overrides):
|
||||
def handler(request):
|
||||
captured.append(json.loads(request.content))
|
||||
return _sse()
|
||||
|
||||
return _client(handler=handler, **overrides)
|
||||
|
||||
def _zhipu(self, **overrides):
|
||||
# glm-5.3 的档位是 low/high/max(能力表已登记),zhipu 的 wire 三样俱全,
|
||||
# 是唯一能同时看清"开启形态"与"档位键"的组合
|
||||
overrides.setdefault("model", "glm-5.3")
|
||||
return _source(provider="zhipu", **overrides)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("provider", "model", "fragment"),
|
||||
[
|
||||
("qwen", "qwen-max", {"enable_thinking": True}),
|
||||
("deepseek", "deepseek-v4-pro", {"thinking": {"type": "enabled"}}),
|
||||
("zhipu", "glm-5.3", {"thinking": {"type": "enabled"}}),
|
||||
("moonshot", "kimi-k3", {"thinking": {"type": "enabled"}}),
|
||||
],
|
||||
)
|
||||
async def test_legacy_on_tier_matches_old_fragment(self, provider, model, fragment):
|
||||
"""存量 `ENABLE_THINKING=true` 的回归门: 发出去的字节逐字不变。
|
||||
|
||||
**只覆盖 `on_base` 自己就说全了"开"的四段**。minimax/openai/anthropic/google
|
||||
的开档旧版硬编码 `{"reasoning_effort": "medium"}`,新版不注入任何档位——那是
|
||||
设计 §4.2 声明过的**有意变更**(medium 在 GLM/kimi/deepseek 的档位表里根本
|
||||
不存在,是库替下游做的档位判断),不是本门要守的不变量。
|
||||
|
||||
qwen/deepseek 两条字面量逐字取自升级前的 `ProviderProfile.thinking_on`;
|
||||
zhipu/moonshot 升级前没有对应段,断言的是它们 2026-09-04 登记的形态。
|
||||
"""
|
||||
captured = []
|
||||
source = _source(provider=provider, model=model, enable_thinking=True)
|
||||
async with self._capturing_client(captured, sources=[source]) as client:
|
||||
await client.chat([{"role": "user", "content": "hi"}])
|
||||
body = captured[0]
|
||||
assert {k: body[k] for k in fragment} == fragment
|
||||
# `auto` = 开启但不指定强度: 语法糖不得替调用方挑一个档
|
||||
assert "reasoning_effort" not in body
|
||||
|
||||
|
||||
class _MemoryRecorder:
|
||||
"""收下遥测行原样存起来;断言"哪些行被写了"必须能看到零行的情形。"""
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from polygateway.thinking import (
|
||||
DEFAULT_CAPABILITIES,
|
||||
ThinkingCapability,
|
||||
ThinkingUnsupportedError,
|
||||
effective_effort,
|
||||
get_capability,
|
||||
observe_thinking,
|
||||
reconcile_thinking,
|
||||
@@ -616,3 +617,67 @@ class TestCapabilityTierList:
|
||||
assert get_capability("MiniMax-M2.7").can_disable is False
|
||||
assert get_capability("MiniMax-M2.5").can_disable is False
|
||||
assert get_capability("MiniMax-M3").can_disable is True
|
||||
|
||||
|
||||
class TestEffectiveEffort:
|
||||
"""三层优先级的**唯一**判定处(设计 §4.2): 请求级 > 源级 > 语法糖 > 不表态。
|
||||
|
||||
收口成一个纯函数,是因为它此前在装配守卫与 transport 里各写了一份就地转换:
|
||||
两份各自演化的判定,迟早会在"装配期放行、运行期报错"这种最难查的形态上分叉。
|
||||
"""
|
||||
|
||||
def test_request_beats_source(self):
|
||||
assert (
|
||||
effective_effort(
|
||||
request_effort=Effort.MAX, source_effort=Effort.LOW, enable_thinking=None
|
||||
)
|
||||
is Effort.MAX
|
||||
)
|
||||
|
||||
def test_source_beats_sugar(self):
|
||||
assert (
|
||||
effective_effort(request_effort=None, source_effort=Effort.HIGH, enable_thinking=None)
|
||||
is Effort.HIGH
|
||||
)
|
||||
|
||||
def test_none_request_does_not_clear_source(self):
|
||||
"""请求级"没表态"绝不能被读成"要求关闭"——那会静默改掉源级的默认档。"""
|
||||
assert (
|
||||
effective_effort(request_effort=None, source_effort=Effort.LOW, enable_thinking=None)
|
||||
is Effort.LOW
|
||||
)
|
||||
|
||||
def test_request_none_tier_is_an_opinion(self):
|
||||
"""`Effort.NONE` 是一次明确的表态,必须压过源级档位而不是被当成缺省。"""
|
||||
assert (
|
||||
effective_effort(
|
||||
request_effort=Effort.NONE, source_effort=Effort.MAX, enable_thinking=None
|
||||
)
|
||||
is Effort.NONE
|
||||
)
|
||||
|
||||
def test_enable_thinking_true_is_auto(self):
|
||||
"""`True` → `auto`(开启但不指定强度),而**不是**旧版硬编码的 medium。"""
|
||||
assert (
|
||||
effective_effort(request_effort=None, source_effort=None, enable_thinking=True)
|
||||
is Effort.AUTO
|
||||
)
|
||||
|
||||
def test_enable_thinking_false_is_the_none_tier(self):
|
||||
assert (
|
||||
effective_effort(request_effort=None, source_effort=None, enable_thinking=False)
|
||||
is Effort.NONE
|
||||
)
|
||||
|
||||
def test_sugar_is_the_last_word_only(self):
|
||||
"""语法糖排在最末: 显式配了档位就以档位为准(矛盾组合已被构造期挡下)。"""
|
||||
assert (
|
||||
effective_effort(request_effort=None, source_effort=Effort.LOW, enable_thinking=True)
|
||||
is Effort.LOW
|
||||
)
|
||||
|
||||
def test_all_absent_is_no_opinion(self):
|
||||
"""三层都不表态 → None(随模型默认),与 `Effort.NONE` 严格区分。"""
|
||||
assert (
|
||||
effective_effort(request_effort=None, source_effort=None, enable_thinking=None) is None
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user