"""providers.py 注册表测试(M1 设计 §7;register_provider 为纯函数,无可变全局)。""" import pytest from loguru import logger from polygateway.providers import ( DEFAULT_CAPABILITIES, DEFAULT_PROFILES, ProviderProfile, ThinkingCapability, get_capability, get_provider, register_capability, register_provider, resolve_thinking, ) class TestDefaultProfiles: def test_qwen_profile(self): p = get_provider("qwen") assert p.thinking_on == {"enable_thinking": True} assert p.thinking_off == {"enable_thinking": False} assert p.strip_think_tags is True assert p.supports_native_schema is False def test_deepseek_profile(self): p = get_provider("deepseek") assert p.thinking_on == {"thinking": {"type": "enabled"}} assert p.thinking_off == {"thinking": {"type": "disabled"}} assert p.strip_think_tags is False def test_openai_slots_are_unknown_not_empty(self): """issue #5: 该段名实践中被复用为任意兼容厂商的兜底(下游把 kimi 挂在此), 故不能下发任何厂商方言参数。None = 形态未知 → 配了 enable_thinking 即报错, 而不是空字典那种"注入了个寂寞"的静默失效。 """ p = get_provider("openai") assert p.thinking_on is None and p.thinking_off is None assert p.strip_think_tags is False def test_minimax_profile_uses_reasoning_effort(self): """2026-08-02 实测: reasoning_effort 才是 MiniMax 认的开关。""" p = get_provider("minimax") assert p.thinking_off == {"reasoning_effort": "none"} assert p.thinking_on == {"reasoning_effort": "medium"} assert p.strip_think_tags is False def test_unknown_provider_fails_loudly(self): """消灭子串猜测: 未注册 provider 装配期即报错,不做模糊匹配。""" with pytest.raises(ValueError, match="glm"): get_provider("glm") with pytest.raises(ValueError): get_provider("qwen2") # 子串相似也不放行 class TestPureFunctionRegistration: def test_register_returns_new_mapping(self): glm = ProviderProfile(name="glm", thinking_on={}, thinking_off={}, 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_qwen = ProviderProfile( name="qwen", thinking_on={"x": 1}, thinking_off={}, strip_think_tags=False ) table = register_provider(custom_qwen, base=DEFAULT_PROFILES) assert get_provider("qwen", registry=table).thinking_on == {"x": 1} assert get_provider("qwen").thinking_on == {"enable_thinking": True} def test_default_profiles_mapping_is_read_only(self): with pytest.raises(TypeError): DEFAULT_PROFILES["hack"] = None # type: ignore[index] def _warnings(): """捕获库发出的 WARNING;loguru 不经标准 logging,pytest 的 caplog 抓不到。""" messages: list[str] = [] sink_id = logger.add(messages.append, level="WARNING") return messages, sink_id class TestThinkingCapability: """issue #5: 能力按 model 登记——同一 provider 内部代际差异是决定性的。""" def test_registered_models_carry_evidence(self): """登记必须附实测证据: 表会过期,没有出处就无从判断该不该信。""" for model in ("MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"): cap = get_capability(model) assert cap is not None and cap.evidence.strip() def test_m3_can_disable_but_m2x_cannot(self): assert get_capability("MiniMax-M3").can_disable is True assert get_capability("MiniMax-M2.7").can_disable is False assert get_capability("MiniMax-M2.5").can_disable is False def test_unregistered_model_is_unknown(self): assert get_capability("some-brand-new-model") is None def test_register_capability_is_pure(self): table = register_capability("x-1", ThinkingCapability(True, "实测")) assert get_capability("x-1", table=table) is not None assert get_capability("x-1") is None # 默认表未被污染 def test_default_capabilities_mapping_is_read_only(self): with pytest.raises(TypeError): DEFAULT_CAPABILITIES["hack"] = None # type: ignore[index] class TestResolveThinking: """五条判定规则(顺序即语义);设计 §5 真值表。""" def test_rule1_none_injects_nothing(self): got = resolve_thinking(get_provider("minimax"), None, None, model="MiniMax-M3") assert got == {} @pytest.mark.parametrize("enable", [True, False]) def test_rule2_unknown_shape_raises_and_points_the_way(self, enable): with pytest.raises(ValueError, match="register_provider") as exc: resolve_thinking(get_provider("openai"), None, enable, model="kimi-k3") assert "extra_body" in str(exc.value) def test_rule3_unregistered_model_warns_but_passes(self): messages, sink_id = _warnings() try: got = resolve_thinking(get_provider("minimax"), None, False, model="MiniMax-M9") finally: logger.remove(sink_id) assert got == {"reasoning_effort": "none"} assert any("MiniMax-M9" in m for m in messages) def test_rule4_cannot_disable_raises_with_the_model_name(self): cap = get_capability("MiniMax-M2.7") with pytest.raises(ValueError, match="MiniMax-M2.7"): resolve_thinking(get_provider("minimax"), cap, False, model="MiniMax-M2.7") def test_rule4_only_blocks_the_off_direction(self): """关不掉 ≠ 开不了: M2.x 默认就在推理,开的方向不该被拦。""" cap = get_capability("MiniMax-M2.7") got = resolve_thinking(get_provider("minimax"), cap, True, model="MiniMax-M2.7") assert got == {"reasoning_effort": "medium"} def test_rule5_normal_path(self): cap = get_capability("MiniMax-M3") assert resolve_thinking(get_provider("minimax"), cap, False, model="MiniMax-M3") == { "reasoning_effort": "none" } def test_unknown_shape_beats_capability_check(self): """第 2 步先于第 4 步: 形态未知时无从注入,能力如何无关紧要。""" cap = ThinkingCapability(can_disable=False, evidence="构造") with pytest.raises(ValueError, match="register_provider"): resolve_thinking(get_provider("openai"), cap, False, model="whatever")