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:
@@ -78,6 +78,7 @@ __all__ = [
|
||||
"ThinkingCapability",
|
||||
"ThinkingObservation",
|
||||
"ThinkingUnsupportedError",
|
||||
"ThinkingWire",
|
||||
"TransientError",
|
||||
"__version__",
|
||||
"gather_bounded",
|
||||
|
||||
+114
-42
@@ -14,75 +14,147 @@ from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThinkingWire:
|
||||
"""一个 provider 表达"开/关/多深"的请求体形态(设计 §3.3)。
|
||||
|
||||
三个字段各自的 `None` **语义互不重叠**,混淆任意两个都会退回 issue #5 修掉的
|
||||
那种静默失效:
|
||||
|
||||
============== ==========================================================
|
||||
``on_base=None`` **形态未知**: 本库不知道该 provider 如何表达"开",配了开关
|
||||
即装配期报错并指路 `register_provider`/`extra_body`
|
||||
``off=None`` 已知开启形态,但**没有关闭形态**(该 provider 关不掉)
|
||||
``effort_key`` ``None`` = 该 provider 只有开关、没有档位(qwen 系靠
|
||||
``=None`` ``thinking_budget`` 调深度,不是档位)
|
||||
============== ==========================================================
|
||||
|
||||
`on_base={}` 与 `on_base=None` 同样不可混: 前者是"已知无需注入任何参数即处于
|
||||
开启档"(经网关的 OpenAI 兼容路径正是如此——档位由 `effort_key` 单独附加),
|
||||
后者是"不知道怎么表达"。
|
||||
|
||||
**为什么不是 cherry-studio 那套 wire DSL**: 它要支持 openai-chat /
|
||||
openai-responses / anthropic-messages / google-generate-content 四种端点协议,
|
||||
故需要 closed operation 集合与 `budgetWire` 代际变体。本库只有一个 OpenAI 兼容
|
||||
transport,跨协议转换由 new-api 在服务端完成(它自己就有一层 canonical intent),
|
||||
一个协议一层形态即够(P1 YAGNI)。
|
||||
"""
|
||||
|
||||
off: Mapping[str, Any] | None
|
||||
on_base: Mapping[str, Any] | None
|
||||
effort_key: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderProfile:
|
||||
"""单个 provider 的能力与差异声明。
|
||||
|
||||
thinking_on/thinking_off 分别是 `SourceConfig.enable_thinking` 为
|
||||
True/False 时并入请求体的参数片段(`enable_thinking` 为 None 时二者都不
|
||||
注入,用模型默认);strip_think_tags 声明响应 content 需剥离 ``<think>``
|
||||
标签(qwen 系);supports_native_schema 供 D14 阶梯选择原生 response_format。
|
||||
`thinking` 声明推理参数的**形态**(按 provider 变,数年不变一次);
|
||||
`strip_think_tags` 声明响应 content 需剥离 ``<think>`` 标签(qwen 系);
|
||||
`supports_native_schema` 供 D14 阶梯选择原生 response_format。
|
||||
|
||||
两档各有三种取值,**语义互不重叠**(issue #5):
|
||||
|
||||
========== ==========================================================
|
||||
``{...}`` 已知的注入片段
|
||||
``{}`` 已知**无需注入**任何参数即处于该档
|
||||
``None`` **未知**: 本库不知道该 provider 如何表达这一档
|
||||
========== ==========================================================
|
||||
|
||||
`None` 与 `{}` 必须分开: 二者曾同为空字典,导致 `enable_thinking=False`
|
||||
对 minimax/openai 源静默失效——调用方以为关掉了推理,实际什么都没发生。
|
||||
现在 `None` 会在装配期显式报错并指路 `register_provider` / `extra_body`。
|
||||
|
||||
注: 本类只声明**形态**(参数长什么样,按 provider 变);某个具体模型能否
|
||||
关闭推理属**能力**(按 model 变),见 `ThinkingCapability`。
|
||||
注: 本类只声明**形态**(参数长什么样,按 provider 变);某个具体模型支持哪些
|
||||
档位属**能力**(按 model 变),见 `thinking.ThinkingCapability`。二者合一在
|
||||
provider 级表达不了代际差异——glm-5.2 能关而 glm-5.3 不能,形态却完全相同。
|
||||
"""
|
||||
|
||||
name: str
|
||||
thinking_on: Mapping[str, Any] | None
|
||||
thinking_off: Mapping[str, Any] | None
|
||||
thinking: ThinkingWire
|
||||
strip_think_tags: bool
|
||||
supports_native_schema: bool = False
|
||||
|
||||
|
||||
# 经 new-api 中转的口径。四家参考实现(cherry-studio / OpenRouter / LiteLLM /
|
||||
# new-api 自身)一致的结论: OpenAI 兼容端点上,档位一律走标准的 `reasoning_effort`,
|
||||
# 跨协议转换(→ Claude 的 thinking、Gemini 的 thinkingConfig)由网关服务端完成。
|
||||
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},
|
||||
thinking=ThinkingWire(
|
||||
off={"enable_thinking": False},
|
||||
on_base={"enable_thinking": True},
|
||||
# 百炼的深度控制是 `thinking_budget`(token 预算)而非档位;
|
||||
# 预算型控制本库当前不支持(设计 §11 明确不做)
|
||||
effort_key=None,
|
||||
),
|
||||
strip_think_tags=True,
|
||||
),
|
||||
# 官方 thinking_mode 文档: thinking:{type} 是开关,reasoning_effort 是深度,
|
||||
# V4 一代两者并用(deepseek-v4-* 的档位见能力表)
|
||||
"deepseek": ProviderProfile(
|
||||
name="deepseek",
|
||||
thinking_on={"thinking": {"type": "enabled"}},
|
||||
thinking_off={"thinking": {"type": "disabled"}},
|
||||
thinking=ThinkingWire(
|
||||
off={"thinking": {"type": "disabled"}},
|
||||
on_base={"thinking": {"type": "enabled"}},
|
||||
effort_key="reasoning_effort",
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
# OpenAI 兼容基线段名: 实践中被复用为**任意**兼容厂商的兜底(下游把
|
||||
# kimi-k3 挂在 provider=openai 下),故不能下发任何厂商方言参数——发给
|
||||
# 不认识它的厂商会 400。两档标 None(未知): 配了 enable_thinking 即在
|
||||
# 装配期报错并指路,真 OpenAI 推理模型的用户走 register_provider
|
||||
"openai": ProviderProfile(
|
||||
name="openai",
|
||||
thinking_on=None,
|
||||
thinking_off=None,
|
||||
# issue #20。智谱官方迁移建议原文: 原先用 {"type":"disabled"} 的应改为
|
||||
# {"type":"enabled"} + reasoning_effort="low"——GLM-5.3 起 thinking.type
|
||||
# 不再接受 disabled,故"关"这一档由能力表按型号裁定(5.2 能关,5.3 不能)
|
||||
"zhipu": ProviderProfile(
|
||||
name="zhipu",
|
||||
thinking=ThinkingWire(
|
||||
off={"thinking": {"type": "disabled"}},
|
||||
on_base={"thinking": {"type": "enabled"}},
|
||||
effort_key="reasoning_effort",
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
# 注入形态出处: 2026-08-02 经自建 new-api 中转实测(findings §2),
|
||||
# 2026-08-25 复测结论不变(findings 2026-08-25 §5);**直连官方端点未验证**。
|
||||
# 实测 enable_thinking / thinking 两种写法均被静默丢弃(prompt_tokens
|
||||
# 恒定等于基线 194),reasoning_effort 才是真开关——本片段的选型据此成立。
|
||||
# "开"取 medium: qwen 的 enable_thinking:true 与 deepseek 的
|
||||
# thinking:{enabled} 都不指定预算、由模型自定,medium 是五档里语义最接近
|
||||
# "厂商正常强度"的一档;取 high 等于替下游做"加钱换质量"的业务判断。
|
||||
# 要精确控制档位经 `SourceConfig.extra_body`(优先级高于本片段)
|
||||
# kimi-k3 的档位是 low/high/max;thinking.type 为月之暗面的开关形态
|
||||
"moonshot": ProviderProfile(
|
||||
name="moonshot",
|
||||
thinking=ThinkingWire(
|
||||
off={"thinking": {"type": "disabled"}},
|
||||
on_base={"thinking": {"type": "enabled"}},
|
||||
effort_key="reasoning_effort",
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
# 2026-08-02 经 new-api 中转实测(findings §2),2026-08-25 复测结论不变。
|
||||
# enable_thinking / thinking 两种写法均被静默丢弃(prompt_tokens 恒等于基线
|
||||
# 194),reasoning_effort 才是真开关——本段形态据此成立。
|
||||
# `on_base={}`: "开"本身不需要任何参数,档位(若模型支持)由 effort_key 附加。
|
||||
# 旧版此处硬编码 {"reasoning_effort": "medium"},那是替下游做的档位判断,
|
||||
# 且 medium 在 GLM/kimi/deepseek 的档位表里根本不存在——正是本次要消灭的
|
||||
"minimax": ProviderProfile(
|
||||
name="minimax",
|
||||
thinking_on={"reasoning_effort": "medium"},
|
||||
thinking_off={"reasoning_effort": "none"},
|
||||
thinking=ThinkingWire(
|
||||
off={"reasoning_effort": "none"}, on_base={}, effort_key="reasoning_effort"
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
# OpenAI 兼容基线段名: 实践中被复用为**任意**兼容厂商的兜底。两档此前标
|
||||
# None(未知),因为当时无法区分"厂商方言"与"标准字段";`reasoning_effort`
|
||||
# 是 OpenAI **官方**字段而非方言,发给经网关的兼容端点不会打到不认识它的
|
||||
# 厂商,故 2026-09-04 起给出标准形态。真正形态未知的 provider 仍走
|
||||
# register_provider 注册,而不是挂在本段下
|
||||
"openai": ProviderProfile(
|
||||
name="openai",
|
||||
thinking=ThinkingWire(
|
||||
off={"reasoning_effort": "none"}, on_base={}, effort_key="reasoning_effort"
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
# Claude 5 系原生是 thinking.type=adaptive + output_config.effort,Gemini 3 系
|
||||
# 原生是 thinkingConfig.thinkingLevel;两者的代际方言(Claude ≤4.5 的
|
||||
# budget_tokens、Gemini 2.x 的 thinkingBudget)由 new-api 的 canonical intent
|
||||
# 层吸收,本库只发 OpenAI 形态(设计 §3.4)
|
||||
"anthropic": ProviderProfile(
|
||||
name="anthropic",
|
||||
thinking=ThinkingWire(
|
||||
off={"reasoning_effort": "none"}, on_base={}, effort_key="reasoning_effort"
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
"google": ProviderProfile(
|
||||
name="google",
|
||||
thinking=ThinkingWire(
|
||||
off={"reasoning_effort": "none"}, on_base={}, effort_key="reasoning_effort"
|
||||
),
|
||||
strip_think_tags=False,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -315,8 +315,9 @@ def resolve_thinking(
|
||||
# Phase 1: 调用方不表态 —— 与 False 严格区分,用模型默认档
|
||||
if enable_thinking is None:
|
||||
return {}
|
||||
slot = profile.thinking_on if enable_thinking else profile.thinking_off
|
||||
direction = "thinking_on" if enable_thinking else "thinking_off"
|
||||
wire = profile.thinking
|
||||
slot = wire.on_base if enable_thinking else wire.off
|
||||
direction = "on_base" if enable_thinking else "off"
|
||||
# Phase 2: 形态未知 —— 提供了开关却不知道怎么发,静默放行就是欺骗调用方
|
||||
if slot is None:
|
||||
raise ThinkingUnsupportedError(
|
||||
|
||||
@@ -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