Files
iomgaa a194f4326e fix: put the tier back where "on" by itself is not on
The minimax wire lost its tier value on the assumption that these models
reason by default, so injecting nothing still reads as "on". T10 measured
the real gateway and the assumption does not hold: MiniMax-M3 with no
reasoning parameter did not reason in 5 of 5 rounds, while all six
strength values worked. Existing downstreams on ENABLE_THINKING=true
went from reasoning to silently not reasoning, and the capability table
cannot catch it because phase 5 lets auto through unconditionally.

Restore on_base to the old {"reasoning_effort": "medium"} verbatim. This
is a stopgap - it hands the tier choice back to the library, which this
work set out to remove. The real fix is to constrain auto by the
capability table, a public behaviour change tracked as issue #21.

The assertions that said "minimax injects no tier on the on-tier" go
back with it; each carries a note on why it moved twice.
2026-09-05 10:48:08 -04:00

135 lines
5.7 KiB
Python

"""providers.py 注册表测试(M1 设计 §7;register_provider 为纯函数,无可变全局)。
2026-09-04 起 profile 存的是 `ThinkingWire`(off / on_base / effort_key)而非两个
固定片段——档位型模型(GLM-5.3、kimi-k3、deepseek-v4…)的"开"档需要附一个档位值,
两个固定片段表达不了。
"""
import pytest
from polygateway.providers import (
DEFAULT_PROFILES,
ProviderProfile,
ThinkingWire,
get_provider,
register_provider,
)
_EXPECTED_SEGMENTS = frozenset(
{"qwen", "deepseek", "zhipu", "moonshot", "minimax", "openai", "anthropic", "google"}
)
class TestDefaultProfiles:
def test_all_eight_profiles_registered(self):
"""issue #20: 智谱缺段,下游只能把 GLM 挂在 openai 兜底段下再手写 extra_body。"""
assert set(DEFAULT_PROFILES) == _EXPECTED_SEGMENTS
def test_qwen_is_a_switch_with_no_tiers(self):
w = get_provider("qwen").thinking
assert w.on_base == {"enable_thinking": True}
assert w.off == {"enable_thinking": False}
assert w.effort_key is None # 百炼靠 thinking_budget 调深度,不是档位
assert get_provider("qwen").strip_think_tags is True
def test_deepseek_carries_both_switch_and_tier(self):
w = get_provider("deepseek").thinking
assert w.on_base == {"thinking": {"type": "enabled"}}
assert w.off == {"thinking": {"type": "disabled"}}
assert w.effort_key == "reasoning_effort"
def test_zhipu_matches_the_vendor_migration_note(self):
"""智谱官方: thinking.type=enabled + reasoning_effort 才是 GLM-5.3 的正确形态。"""
w = get_provider("zhipu").thinking
assert w.on_base == {"thinking": {"type": "enabled"}}
assert w.off == {"thinking": {"type": "disabled"}}
assert w.effort_key == "reasoning_effort"
def test_openai_family_sends_the_standard_field_only(self):
"""gpt/claude/gemini 经网关都吃 OpenAI 标准的 reasoning_effort,不下发厂商方言。
minimax 2026-09-05 起不在本组: 它的形态相同,但"开"这一档被迫带上了一个
档位值(见 `test_minimax_on_tier_carries_a_tier_value`)。
"""
for name in ("openai", "anthropic", "google"):
w = get_provider(name).thinking
assert w.on_base == {}, name
assert w.off == {"reasoning_effort": "none"}, name
assert w.effort_key == "reasoning_effort", name
def test_minimax_on_tier_carries_a_tier_value(self):
"""issue #21 的权宜之计: minimax 的"开"必须真写一个档位值,不能是空片段。
断言反复过一次: T2 按"这些模型默认就推理"的推定把它改成 `{}`,T10 真实
网关实测推翻推定(M3 不发推理参数时 5/5 轮不推理),故逐字恢复旧版的 medium。
"""
w = get_provider("minimax").thinking
assert w.on_base == {"reasoning_effort": "medium"}
assert w.off == {"reasoning_effort": "none"}
assert w.effort_key == "reasoning_effort"
def test_unknown_provider_fails_loudly(self):
"""消灭子串猜测: 未注册 provider 装配期即报错,不做模糊匹配。"""
with pytest.raises(ValueError, match="glm"):
get_provider("glm") # 段名是 zhipu,不是 glm
with pytest.raises(ValueError):
get_provider("qwen2") # 子串相似也不放行
def test_error_lists_every_registered_segment(self):
with pytest.raises(ValueError, match="zhipu"):
get_provider("nope")
class TestWireNoneSemantics:
"""三个 `None` 语义互不重叠(issue #5 的成果,不可退回成"注入了个寂寞")。"""
def test_on_base_none_means_shape_unknown(self):
w = ThinkingWire(off=None, on_base=None, effort_key=None)
assert w.on_base is None
def test_off_none_means_no_off_shape(self):
"""有开启形态但没有关闭形态,与"整个形态未知"是两回事。"""
w = ThinkingWire(off=None, on_base={"x": 1}, effort_key=None)
assert w.on_base is not None and w.off is None
def test_effort_key_none_means_switch_only(self):
"""qwen 是这一档: 能开能关,但没有档位可谈。"""
assert get_provider("qwen").thinking.effort_key is None
def test_empty_on_base_is_not_none(self):
"""`{}` = 已知无需注入任何参数即处于该档;`None` = 不知道怎么表达。
样本 2026-09-05 由 minimax 换成 openai: minimax 的 `on_base` 因 issue #21
改回带值,不再是空片段;openai 段是现存 `{}` 语义的代表。
"""
w = get_provider("openai").thinking
assert w.on_base == {} and w.on_base is not None
class TestPureFunctionRegistration:
def test_register_returns_new_mapping(self):
glm = ProviderProfile(
name="glm",
thinking=ThinkingWire(off={}, on_base={}, effort_key=None),
strip_think_tags=False,
)
table = register_provider(glm)
assert get_provider("glm", registry=table) is glm
# 默认表未被污染(无可变全局状态铁律)
with pytest.raises(ValueError):
get_provider("glm")
def test_register_on_custom_base_and_override(self):
custom = ProviderProfile(
name="qwen",
thinking=ThinkingWire(off={}, on_base={"x": 1}, effort_key=None),
strip_think_tags=False,
)
table = register_provider(custom, base=DEFAULT_PROFILES)
assert get_provider("qwen", registry=table).thinking.on_base == {"x": 1}
assert get_provider("qwen").thinking.on_base == {"enable_thinking": True}
def test_default_profiles_mapping_is_read_only(self):
with pytest.raises(TypeError):
DEFAULT_PROFILES["hack"] = None # type: ignore[index]