"""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]