feat: give zhipu, moonshot, anthropic and google a wire of their own
Eight segments now, and each one holds a ThinkingWire instead of two fixed fragments: off, on_base, and the key a tier gets written to. The two fragments could not say "on, at this depth", which is what every current generation model wants. Two deliberate behaviour changes fall out of it. The openai segment stops reporting its shape as unknown — reasoning_effort is OpenAI's own field, not a vendor dialect, so a compatible endpoint behind the gateway takes it. And minimax's on-tier stops carrying a hardcoded medium: that was the library picking a price for the caller, and medium is not even a tier GLM, kimi or deepseek serve. The issue #5 guards stay; their sample moves from "the openai segment" to an explicitly registered unknown one, which is what they always meant to test.
This commit is contained in:
@@ -8,6 +8,7 @@ from loguru import logger
|
||||
|
||||
from polygateway.client import GatewayClient
|
||||
from polygateway.config import EmbeddingSettings, GatewaySettings, OcrSettings
|
||||
from polygateway.providers import ProviderProfile, ThinkingWire, register_provider
|
||||
|
||||
_BASE_ENV = {
|
||||
"LLM__QWEN__1__BASE_URL": "https://gw-a.example/v1",
|
||||
@@ -637,10 +638,23 @@ class TestCrossFieldInvariants:
|
||||
GatewayClient.from_settings(settings)
|
||||
|
||||
def test_unknown_thinking_shape_fails_at_assembly(self):
|
||||
"""provider=openai 是任意兼容厂商的兜底段名,形态未知即报错并指路。"""
|
||||
settings = self._thinking_sources("openai", "kimi-k3", False)
|
||||
# 形态未知即报错并指路。2026-09-04 起默认表 8 段全部有形态(openai 段改发
|
||||
# OpenAI 标准的 reasoning_effort),故样本改为显式注册一个未知段——守卫测的
|
||||
# 是机制,不是某个段当时的配置
|
||||
mystery = ProviderProfile(
|
||||
name="mystery",
|
||||
thinking=ThinkingWire(off=None, on_base=None, effort_key=None),
|
||||
strip_think_tags=False,
|
||||
)
|
||||
settings = self._thinking_sources("mystery", "kimi-k3", False)
|
||||
with pytest.raises(ValueError, match="register_provider"):
|
||||
GatewayClient.from_settings(settings)
|
||||
GatewayClient.from_settings(settings, registry=register_provider(mystery))
|
||||
|
||||
def test_openai_segment_now_assembles_with_the_standard_field(self):
|
||||
# 行为变更(2026-09-04): reasoning_effort 是 OpenAI 官方字段而非厂商方言,
|
||||
# 经网关的兼容端点收得下,故兜底段不再把"关闭"判为形态未知
|
||||
settings = self._thinking_sources("openai", "gpt-5.5", False)
|
||||
assert GatewayClient.from_settings(settings) is not None
|
||||
|
||||
def test_supported_combination_assembles(self):
|
||||
settings = self._thinking_sources("minimax", "MiniMax-M3", False)
|
||||
|
||||
@@ -16,6 +16,7 @@ from polygateway.errors import (
|
||||
)
|
||||
from polygateway.middleware.telemetry import TelemetryEmitter
|
||||
from polygateway.pricing import ModelPrice, PricingTable
|
||||
from polygateway.providers import ProviderProfile, ThinkingWire, register_provider
|
||||
from polygateway.transports._http_errors import summarize_body
|
||||
from polygateway.transports.openai_compat import (
|
||||
OpenAICompatTransport,
|
||||
@@ -60,10 +61,11 @@ def _sse_stream(*frames, done=True):
|
||||
return httpx.Response(200, content=text.encode(), headers={"content-type": "text/event-stream"})
|
||||
|
||||
|
||||
def _transport_for(handler):
|
||||
def _transport_for(handler, *, registry=None):
|
||||
mock = httpx.MockTransport(handler)
|
||||
return OpenAICompatTransport(
|
||||
client_factory=lambda src: httpx.AsyncClient(base_url=src.base_url, transport=mock)
|
||||
client_factory=lambda src: httpx.AsyncClient(base_url=src.base_url, transport=mock),
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
|
||||
@@ -666,7 +668,9 @@ class TestRequestShaping:
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("enable_thinking", "expected"),
|
||||
[(True, "medium"), (False, "none")],
|
||||
# 开档不再附 medium(2026-09-04): 那是替下游做的档位判断,且 medium 不在
|
||||
# GLM/kimi/deepseek 的档位表里。MiniMax 开启档本就无需参数,要强度请配档位
|
||||
[(True, None), (False, "none")],
|
||||
)
|
||||
async def test_minimax_injects_reasoning_effort(self, enable_thinking, expected):
|
||||
"""issue #5: MiniMax 认的是 reasoning_effort,不是 enable_thinking。"""
|
||||
@@ -680,7 +684,10 @@ class TestRequestShaping:
|
||||
name="mm", provider="minimax", model="MiniMax-M3", enable_thinking=enable_thinking
|
||||
)
|
||||
await _complete(_transport_for(handler), source)
|
||||
assert seen["reasoning_effort"] == expected
|
||||
if expected is None:
|
||||
assert "reasoning_effort" not in seen
|
||||
else:
|
||||
assert seen["reasoning_effort"] == expected
|
||||
assert "enable_thinking" not in seen # 旧形态实测被静默丢弃,不再下发
|
||||
|
||||
async def test_extra_body_overrides_the_profile_slot(self):
|
||||
@@ -758,9 +765,15 @@ class TestRequestShaping:
|
||||
def handler(request): # pragma: no cover - 不该走到发请求
|
||||
raise AssertionError("请求不该发出")
|
||||
|
||||
source = _source(name="k3", provider="openai", model="kimi-k3", enable_thinking=False)
|
||||
# 2026-09-04 起默认表 8 段全部有形态,守卫样本改为显式注册的未知段
|
||||
mystery = ProviderProfile(
|
||||
name="mystery",
|
||||
thinking=ThinkingWire(off=None, on_base=None, effort_key=None),
|
||||
strip_think_tags=False,
|
||||
)
|
||||
source = _source(name="k3", provider="mystery", model="kimi-k3", enable_thinking=False)
|
||||
with pytest.raises(RequestRejectedError, match="register_provider"):
|
||||
await _complete(_transport_for(handler), source)
|
||||
await _complete(_transport_for(handler, registry=register_provider(mystery)), source)
|
||||
|
||||
async def test_overlay_merged_into_payload(self):
|
||||
seen = {}
|
||||
|
||||
@@ -1,57 +1,99 @@
|
||||
"""providers.py 注册表测试(M1 设计 §7;register_provider 为纯函数,无可变全局)。"""
|
||||
"""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_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_all_eight_profiles_registered(self):
|
||||
"""issue #20: 智谱缺段,下游只能把 GLM 挂在 openai 兜底段下再手写 extra_body。"""
|
||||
assert set(DEFAULT_PROFILES) == _EXPECTED_SEGMENTS
|
||||
|
||||
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_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_openai_slots_are_unknown_not_empty(self):
|
||||
"""issue #5: 该段名实践中被复用为任意兼容厂商的兜底(下游把 kimi 挂在此),
|
||||
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"
|
||||
|
||||
故不能下发任何厂商方言参数。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_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_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_openai_family_sends_the_standard_field_only(self):
|
||||
"""gpt/claude/gemini 经网关都吃 OpenAI 标准的 reasoning_effort,不下发厂商方言。"""
|
||||
for name in ("openai", "anthropic", "google", "minimax"):
|
||||
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_unknown_provider_fails_loudly(self):
|
||||
"""消灭子串猜测: 未注册 provider 装配期即报错,不做模糊匹配。"""
|
||||
with pytest.raises(ValueError, match="glm"):
|
||||
get_provider("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` = 不知道怎么表达。"""
|
||||
w = get_provider("minimax").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_on={}, thinking_off={}, strip_think_tags=False)
|
||||
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
|
||||
# 默认表未被污染(无可变全局状态铁律)
|
||||
@@ -59,12 +101,14 @@ class TestPureFunctionRegistration:
|
||||
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
|
||||
custom = ProviderProfile(
|
||||
name="qwen",
|
||||
thinking=ThinkingWire(off={}, on_base={"x": 1}, effort_key=None),
|
||||
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}
|
||||
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):
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import pytest
|
||||
from loguru import logger
|
||||
|
||||
from polygateway.providers import get_provider
|
||||
from polygateway.providers import ProviderProfile, ThinkingWire, get_provider
|
||||
from polygateway.thinking import (
|
||||
DEFAULT_CAPABILITIES,
|
||||
ThinkingCapability,
|
||||
@@ -20,6 +20,16 @@ from polygateway.thinking import (
|
||||
)
|
||||
from polygateway.types import Effort, ThinkingObservation
|
||||
|
||||
_MYSTERY = ProviderProfile(
|
||||
name="mystery",
|
||||
thinking=ThinkingWire(off=None, on_base=None, effort_key=None),
|
||||
strip_think_tags=False,
|
||||
)
|
||||
"""形态完全未知的 provider(issue #5 的守卫对象)。
|
||||
|
||||
2026-09-04 起默认表 8 段全部有形态,故未知样本改为显式构造——测的是**机制**
|
||||
(不知道怎么表达就报错并指路),不是某个段当时的配置。"""
|
||||
|
||||
|
||||
def _warnings():
|
||||
"""捕获库发出的 WARNING;loguru 不经标准 logging,pytest 的 caplog 抓不到。"""
|
||||
@@ -144,7 +154,7 @@ class TestResolveThinking:
|
||||
@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")
|
||||
resolve_thinking(_MYSTERY, None, enable, model="kimi-k3")
|
||||
assert "extra_body" in str(exc.value)
|
||||
|
||||
def test_rule3_unregistered_model_warns_but_passes(self):
|
||||
@@ -165,7 +175,9 @@ class TestResolveThinking:
|
||||
"""关不掉 ≠ 开不了: 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"}
|
||||
# 2026-09-04: "开"不再硬编码 medium——那是替下游做的档位判断,且 medium 在
|
||||
# GLM/kimi/deepseek 的档位表里根本不存在。MiniMax 开启档本就无需参数
|
||||
assert got == {}
|
||||
|
||||
def test_rule5_normal_path(self):
|
||||
cap = get_capability("MiniMax-M3")
|
||||
@@ -177,7 +189,7 @@ class TestResolveThinking:
|
||||
"""第 2 步先于第 4 步: 形态未知时无从注入,能力如何无关紧要。"""
|
||||
cap = ThinkingCapability((Effort.AUTO,), "构造")
|
||||
with pytest.raises(ValueError, match="register_provider"):
|
||||
resolve_thinking(get_provider("openai"), cap, False, model="whatever")
|
||||
resolve_thinking(_MYSTERY, cap, False, model="whatever")
|
||||
|
||||
|
||||
class TestReconcileThinking:
|
||||
|
||||
Reference in New Issue
Block a user