Files
PolyGateway/tests/unit/test_providers.py
T

131 lines
5.5 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_does_not_select_a_tier(self):
"""形态不代替模型能力,也不替调用者选择付费档位。"""
w = get_provider("minimax").thinking
assert w.on_base == {}
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]