From 315142ceb1dadbb81a4f93cb9c93a6786e725d0d Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 20 Jul 2026 06:40:55 -0400 Subject: [PATCH] feat: add explicit provider profile registry --- src/polygateway/providers.py | 76 ++++++++++++++++++++++++++++++++++++ tests/unit/test_providers.py | 59 ++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/polygateway/providers.py create mode 100644 tests/unit/test_providers.py diff --git a/src/polygateway/providers.py b/src/polygateway/providers.py new file mode 100644 index 0000000..e137f98 --- /dev/null +++ b/src/polygateway/providers.py @@ -0,0 +1,76 @@ +"""provider 注册表(D11): 消灭 `"qwen" in provider` 式字符串猜测。 + +每个 provider 显式声明 thinking 参数注入形态与响应处理差异;查找按名字 +**精确匹配**,未注册即装配期报错。注册是纯函数——返回新表,不修改共享 +状态(纯 asyncio 中立铁律);client 经 `registry` 参数持有自己的表。 +""" + +from collections.abc import Mapping +from dataclasses import dataclass +from types import MappingProxyType +from typing import Any + + +@dataclass(frozen=True) +class ProviderProfile: + """单个 provider 的能力与差异声明。 + + thinking_on/thinking_off 分别是 `SourceConfig.enable_thinking` 为 + True/False 时并入请求体的参数片段(None 时二者都不注入,用模型默认); + strip_think_tags 声明响应 content 需剥离 ```` 标签(qwen 系); + supports_native_schema 供 D14 阶梯选择原生 response_format 策略。 + """ + + name: str + thinking_on: dict[str, Any] + thinking_off: dict[str, Any] + strip_think_tags: bool + supports_native_schema: bool = False + + +DEFAULT_PROFILES: Mapping[str, ProviderProfile] = MappingProxyType( + { + # 注入片段出处: VT llm.py:130-144(开启形态)与 CHS invokers.py:230-238(关闭形态) + "qwen": ProviderProfile( + name="qwen", + thinking_on={"enable_thinking": True}, + thinking_off={"enable_thinking": False}, + strip_think_tags=True, + ), + "deepseek": ProviderProfile( + name="deepseek", + thinking_on={"thinking": {"type": "enabled"}}, + thinking_off={"thinking": {"type": "disabled"}}, + strip_think_tags=False, + ), + "openai": ProviderProfile( + name="openai", + thinking_on={}, + thinking_off={}, + strip_think_tags=False, + ), + } +) + + +def get_provider( + name: str, *, registry: Mapping[str, ProviderProfile] | None = None +) -> ProviderProfile: + """按名字精确查找 profile;未注册直接报错(严禁默认值掩盖配置错误)。""" + table = DEFAULT_PROFILES if registry is None else registry + profile = table.get(name) + if profile is None: + raise ValueError( + f"未注册的 provider: {name!r}(已注册: {sorted(table)});" + f"新 provider 用 register_provider(ProviderProfile(...)) 注册后经 registry 参数传入" + ) + return profile + + +def register_provider( + profile: ProviderProfile, *, base: Mapping[str, ProviderProfile] | None = None +) -> dict[str, ProviderProfile]: + """纯函数注册: 返回 base(缺省 DEFAULT_PROFILES)+ 新条目的新表,同名覆盖。""" + table = dict(DEFAULT_PROFILES if base is None else base) + table[profile.name] = profile + return table diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py new file mode 100644 index 0000000..64f446d --- /dev/null +++ b/tests/unit/test_providers.py @@ -0,0 +1,59 @@ +"""providers.py 注册表测试(M1 设计 §7;register_provider 为纯函数,无可变全局)。""" + +import pytest + +from polygateway.providers import ( + DEFAULT_PROFILES, + ProviderProfile, + get_provider, + register_provider, +) + + +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_baseline_profile(self): + p = get_provider("openai") + assert p.thinking_on == {} and p.thinking_off == {} + 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]