Files
PolyGateway/src/polygateway/providers.py
T
iomgaa 3e869b9b39 docs: refresh the M3 capability evidence with the 08-25 retest
can_disable stays true — reasoning_effort=none still lands prompt 194,
completion 3, no prose. What the retest added are two limits worth
recording: the verdict is unobservable on the non-streaming path, where
reasoning is billed but neither prose nor usage detail comes back, and
enable_thinking / thinking:{enabled} remain inert on this model.

No behaviour changed, so there is no failing test to show first. The
evidence for a declaration that still holds is the retest itself, not
a unit test the library could write about its own claim.
2026-08-26 00:06:22 -04:00

113 lines
5.1 KiB
Python

"""provider 注册表(D11): 消灭 `"qwen" in provider` 式字符串猜测。
每个 provider 显式声明 thinking 参数注入形态与响应处理差异;查找按名字
**精确匹配**,未注册即装配期报错。注册是纯函数——返回新表,不修改共享
状态(纯 asyncio 中立铁律);client 经 `registry` 参数持有自己的表。
**本模块只存放声明,不做判断**: 拿这些声明去决定注入什么、响应算不算推理,
全部在 `thinking.py`(P7 决策逻辑与状态存储分离)。
"""
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 时并入请求体的参数片段(`enable_thinking` 为 None 时二者都不
注入,用模型默认);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`。
"""
name: str
thinking_on: Mapping[str, Any] | None
thinking_off: Mapping[str, Any] | None
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 兼容基线段名: 实践中被复用为**任意**兼容厂商的兜底(下游把
# kimi-k3 挂在 provider=openai 下),故不能下发任何厂商方言参数——发给
# 不认识它的厂商会 400。两档标 None(未知): 配了 enable_thinking 即在
# 装配期报错并指路,真 OpenAI 推理模型的用户走 register_provider
"openai": ProviderProfile(
name="openai",
thinking_on=None,
thinking_off=None,
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`(优先级高于本片段)
"minimax": ProviderProfile(
name="minimax",
thinking_on={"reasoning_effort": "medium"},
thinking_off={"reasoning_effort": "none"},
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