refactor: make capability a tier list, since "can it be off" is one entry
The boolean could say a model reasons or does not. It could not say what GLM-5.3 and Gemini 3 Pro actually do: refuse to stop reasoning while still letting you ask for less. So capability becomes the list of tiers a model serves, and `none`'s presence in it is what "can_disable" now reads. Effort carries `auto` alongside the strength tiers. Nine of the models on our gateway are pure switches with no tier to name, and without `auto` they would have to borrow a strength tier to mean "on" — which is the exact bug this work exists to remove. Tiers land as documented guesses from four registries that agree; every entry says so in its evidence, and task 10 replaces them with measurements.
This commit is contained in:
@@ -32,6 +32,8 @@ from polygateway.thinking import (
|
|||||||
resolve_thinking,
|
resolve_thinking,
|
||||||
)
|
)
|
||||||
from polygateway.types import (
|
from polygateway.types import (
|
||||||
|
EFFORT_ORDER,
|
||||||
|
Effort,
|
||||||
EmbeddingResponse,
|
EmbeddingResponse,
|
||||||
LLMResponse,
|
LLMResponse,
|
||||||
OcrLayoutElement,
|
OcrLayoutElement,
|
||||||
@@ -46,6 +48,8 @@ __version__ = "1.3.2"
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DEFAULT_PROFILES",
|
"DEFAULT_PROFILES",
|
||||||
|
"EFFORT_ORDER",
|
||||||
|
"Effort",
|
||||||
"AllSourcesExhausted",
|
"AllSourcesExhausted",
|
||||||
"CircuitOpenError",
|
"CircuitOpenError",
|
||||||
"EmbeddingClient",
|
"EmbeddingClient",
|
||||||
|
|||||||
+176
-12
@@ -15,7 +15,7 @@ from typing import Any
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from polygateway.providers import ProviderProfile
|
from polygateway.providers import ProviderProfile
|
||||||
from polygateway.types import ThinkingObservation
|
from polygateway.types import EFFORT_ORDER, Effort, ThinkingObservation
|
||||||
|
|
||||||
|
|
||||||
def observe_thinking(*, thinking: str, reasoning_tokens: int | None) -> ThinkingObservation:
|
def observe_thinking(*, thinking: str, reasoning_tokens: int | None) -> ThinkingObservation:
|
||||||
@@ -54,50 +54,214 @@ class ThinkingUnsupportedError(ValueError):
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ThinkingCapability:
|
class ThinkingCapability:
|
||||||
"""某个**具体模型**能否关闭推理(issue #5);登记必须附实测证据与日期。
|
"""某个**具体模型**支持哪些推理档位(设计 §3.2);登记必须附证据与日期。
|
||||||
|
|
||||||
与 `ProviderProfile` 的分工: 后者声明**形态**(参数长什么样,按 provider 变,
|
与 `ProviderProfile` 的分工: 后者声明**形态**(参数长什么样,按 provider 变,
|
||||||
数年不变一次),本类声明**能力**(按 model 变,同一 provider 每代都变)。二者
|
数年不变一次),本类声明**能力**(按 model 变,同一 provider 每代都变)。二者
|
||||||
合一在 provider 级表达不了代际差异——实测 MiniMax-M3 可关闭推理,而同厂的
|
合一在 provider 级表达不了代际差异——实测 MiniMax-M3 可关闭推理,而同厂的
|
||||||
M2.7/M2.5 三种参数形态全部无效(findings §2.3),profile 一格管不住三个模型。
|
M2.7/M2.5 三种参数形态全部无效(findings §2.3),profile 一格管不住三个模型。
|
||||||
|
|
||||||
|
**档位清单而非布尔**(2026-09-04): 旧版是 `can_disable: bool`,表达不了
|
||||||
|
"关不掉但能调到最低档"这第三种情况——而 GLM-5.3 系与 Gemini 3 Pro 都是它。
|
||||||
|
现在"能不能关"就是 `Effort.NONE` 在不在清单里,是派生量而非独立字段;三个
|
||||||
|
派生量一律不存字段,存了必与清单漂移。
|
||||||
|
|
||||||
`evidence` 不是装饰: 能力表过期是必然事件,没有出处就无从判断该不该信它。
|
`evidence` 不是装饰: 能力表过期是必然事件,没有出处就无从判断该不该信它。
|
||||||
|
文档推定与实测必须在 evidence 里说清楚是哪种——前者会被 new-api 中转改写
|
||||||
|
(LiteLLM 里同一个 kimi-k3 在 `moonshot/` 下三档、`perplexity/` 下六档)。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
can_disable: bool
|
supported_efforts: tuple[Effort, ...]
|
||||||
evidence: str
|
evidence: str
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""构造期校验: 空清单与重复档都是登记错误,不能等到请求期才炸。"""
|
||||||
|
if not self.supported_efforts:
|
||||||
|
raise ValueError("supported_efforts 至少要有一档: 空清单表达不了任何能力")
|
||||||
|
if len(set(self.supported_efforts)) != len(self.supported_efforts):
|
||||||
|
raise ValueError(f"supported_efforts 有重复档: {self.supported_efforts}")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_disable(self) -> bool:
|
||||||
|
"""能否关闭推理 = `none` 在不在清单里(旧 `can_disable` 字段的等价物)。"""
|
||||||
|
return Effort.NONE in self.supported_efforts
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cheapest_effort(self) -> Effort | None:
|
||||||
|
"""除 `none` 外最省的一档;关不掉时作为**可执行替代**推荐给调用方。
|
||||||
|
|
||||||
|
`AUTO` 参与候选(纯开关型模型只有它可推荐),但因不在 `EFFORT_ORDER` 中,
|
||||||
|
仅当没有任何强度档时才被选中。全清单只有 `none` 时返回 None——那种模型
|
||||||
|
没有"最省的开启档"可言。
|
||||||
|
"""
|
||||||
|
tiers = [e for e in EFFORT_ORDER if e is not Effort.NONE and e in self.supported_efforts]
|
||||||
|
if tiers:
|
||||||
|
return tiers[0]
|
||||||
|
return Effort.AUTO if Effort.AUTO in self.supported_efforts else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_tiered(self) -> bool:
|
||||||
|
"""是否档位型(除 `none`/`auto` 外仍有强度档)。
|
||||||
|
|
||||||
|
用途是**告警文案**: 对纯开关型模型说"可选档位: ..."是错的,它没有档位。
|
||||||
|
"""
|
||||||
|
return any(e not in (Effort.NONE, Effort.AUTO) for e in self.supported_efforts)
|
||||||
|
|
||||||
|
|
||||||
|
# 证据分两类,evidence 里必须自报家门:
|
||||||
|
# 实测 = 经 new-api 中转打过真实请求(最硬,不得被文档推定覆盖);
|
||||||
|
# 文档推定 = 官方文档 / OpenRouter / cherry-studio / LiteLLM 四方交叉(待实测校正)。
|
||||||
|
_MEASURED = "2026-08-02 经 new-api 中转实测"
|
||||||
|
_DOC = "2026-09-04 文档推定(官方文档 + OpenRouter + cherry-studio + LiteLLM 四方交叉),待经 new-api 实测"
|
||||||
|
|
||||||
DEFAULT_CAPABILITIES: Mapping[str, ThinkingCapability] = MappingProxyType(
|
DEFAULT_CAPABILITIES: Mapping[str, ThinkingCapability] = MappingProxyType(
|
||||||
{
|
{
|
||||||
|
# —— 实测条目(2026-08-02/08-25),证据原文保留 ——
|
||||||
"MiniMax-M3": ThinkingCapability(
|
"MiniMax-M3": ThinkingCapability(
|
||||||
can_disable=True,
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
evidence=(
|
evidence=(
|
||||||
"2026-08-02 经 new-api 中转实测 N=10: reasoning_effort=none 稳定关闭,零跳变;"
|
"2026-08-02 经 new-api 中转实测 N=10: reasoning_effort=none 稳定关闭,零跳变;"
|
||||||
"2026-08-25 复测依然成立(prompt 194 = 基线、completion 3、无推理正文)。"
|
"2026-08-25 复测依然成立(prompt 194 = 基线、completion 3、无推理正文)。"
|
||||||
"两条限制(findings 2026-08-25-thinking-observability-regression §3.1/§5): "
|
"两条限制(findings 2026-08-25-thinking-observability-regression §3.1/§5): "
|
||||||
"① 非流式路径观测不到推理信号——推理已计费,但正文与 usage 明细都不回传;"
|
"① 非流式路径观测不到推理信号——推理已计费,但正文与 usage 明细都不回传;"
|
||||||
"② enable_thinking / thinking:{type:enabled} 对本模型无效,仅 reasoning_effort 是真开关"
|
"② enable_thinking / thinking:{type:enabled} 对本模型无效,仅 reasoning_effort 是真开关。"
|
||||||
|
"无强度档: 官方只有开/关两态(thinking.type disabled/adaptive)"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"MiniMax-M2.7": ThinkingCapability(
|
"MiniMax-M2.7": ThinkingCapability(
|
||||||
can_disable=False,
|
supported_efforts=(Effort.AUTO,),
|
||||||
evidence=(
|
evidence=(
|
||||||
"2026-08-02 实测 reasoning_effort=none / thinking:{disabled} / thinking:{adaptive} "
|
"2026-08-02 实测 reasoning_effort=none / thinking:{disabled} / thinking:{adaptive} "
|
||||||
"各 N=3 全部无效;OpenRouter 注册表登记 mandatory:true,models.dev 登记无控制手段"
|
"各 N=3 全部无效;OpenRouter 注册表登记 mandatory:true,models.dev 登记无控制手段。"
|
||||||
|
"MiniMax 官方亦承认 M2.x 接受 disabled 但推理仍开着"
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
"MiniMax-M2.5": ThinkingCapability(
|
"MiniMax-M2.5": ThinkingCapability(
|
||||||
can_disable=False,
|
supported_efforts=(Effort.AUTO,),
|
||||||
evidence="2026-08-02 实测同 M2.7: 三种形态各 N=3 全部无效;外部注册表同样登记为强制推理",
|
evidence="2026-08-02 实测同 M2.7: 三种形态各 N=3 全部无效;外部注册表同样登记为强制推理",
|
||||||
),
|
),
|
||||||
"qwen3.7-plus": ThinkingCapability(
|
"qwen3.7-plus": ThinkingCapability(
|
||||||
can_disable=True,
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
evidence="2026-08-02 实测 enable_thinking=false 关闭(completion 5 token,无推理)",
|
evidence=(
|
||||||
|
"2026-08-02 实测 enable_thinking=false 关闭(completion 5 token,无推理)。"
|
||||||
|
"无强度档: OpenRouter 登记本型号只支持 reasoning 开关,不支持 reasoning_effort"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
"deepseek-v4-pro": ThinkingCapability(
|
"deepseek-v4-pro": ThinkingCapability(
|
||||||
can_disable=True,
|
supported_efforts=(Effort.NONE, Effort.HIGH, Effort.MAX),
|
||||||
evidence="2026-08-02 实测 thinking:{type:disabled} 关闭(completion 3 token,无推理)",
|
evidence=(
|
||||||
|
"关闭档为 2026-08-02 实测(thinking:{type:disabled},completion 3 token,无推理);"
|
||||||
|
f"强度档为{_DOC}: 官方 thinking_mode 文档列 Non-think/Think High/Think Max 三态,默认 high"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
# —— 文档推定条目(2026-09-04),待 T10 经 new-api 实测校正 ——
|
||||||
|
"deepseek-v4-flash": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.HIGH, Effort.MAX),
|
||||||
|
evidence=f"{_DOC}: 官方文档「deepseek-v4-flash 与 deepseek-v4-pro 一致」,默认 high",
|
||||||
|
),
|
||||||
|
"deepseek-v4-flash-vision-exp": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.HIGH, Effort.MAX),
|
||||||
|
evidence=f"{_DOC}: 同 v4-flash 一档(OpenRouter 登记支持 reasoning_effort)",
|
||||||
|
),
|
||||||
|
"glm-5.3": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.LOW, Effort.HIGH, Effort.MAX),
|
||||||
|
evidence=(
|
||||||
|
f"{_DOC}: **推理不可关闭**——智谱官方文档明确 thinking.type 只接受 enabled,"
|
||||||
|
"官方迁移建议是改用 enabled + reasoning_effort=low;cherry-studio 标 toggle:false、"
|
||||||
|
"OpenRouter 标 mandatory:true,三源一致。默认 max。"
|
||||||
|
"注: issue #20 实测的 reasoning_effort=none 是**未定义值**,短提示词下 rt≈1.2 像是关了,"
|
||||||
|
"5552 token 长上下文下跳到 0/54/167 即露馅"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"glm-5.3-flash": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.LOW, Effort.HIGH, Effort.MAX),
|
||||||
|
evidence=f"{_DOC}: 同 glm-5.3(cherry-studio 的 pattern 'glm-5[.-]3' 覆盖两者),默认 max",
|
||||||
|
),
|
||||||
|
"glm-5.2": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.HIGH, Effort.MAX),
|
||||||
|
evidence=(
|
||||||
|
f"{_DOC}: cherry-studio 登记 none/high/max(官方端点默认 max,百炼上默认 high)。"
|
||||||
|
"注意: issue #20 记录本渠道对 glm-5.2 的请求 6/6 回报 model=glm-5.3,疑被路由,实测时须核对 model_reported"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"glm-5": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: OpenRouter 登记只支持 reasoning 开关、无 reasoning_effort;cherry-studio 标 toggle:true",
|
||||||
|
),
|
||||||
|
"glm-5.1": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: 同 glm-5(OpenRouter reasoning.mandatory=false 且无 supported_efforts)",
|
||||||
|
),
|
||||||
|
"glm-4.6v": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: OpenRouter 登记无 reasoning_effort;VLM,推理控制同 glm-4.x 系开关型",
|
||||||
|
),
|
||||||
|
"kimi-k3": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.LOW, Effort.HIGH, Effort.MAX),
|
||||||
|
evidence=(
|
||||||
|
f"{_DOC}: 官方 reasoning_effort 三档 low/high/max,默认 max。"
|
||||||
|
"**保守登记为不可关**——官方档位表无 none,而 OpenRouter 标 mandatory:false,两源分歧待实测;"
|
||||||
|
"保守方向的代价是下游配 none 会报错并被指向 low,反方向的代价是静默失效(issue #20 的病)。"
|
||||||
|
"另: 官方提示切换档位会使 prefix cache 失效,不宜在会话中途改档"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"gpt-5.4": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.LOW, Effort.MEDIUM, Effort.HIGH, Effort.XHIGH),
|
||||||
|
evidence=f"{_DOC}: OpenRouter 登记 none/low/medium/high/xhigh,默认 medium;LiteLLM 登记 minimal 不支持",
|
||||||
|
),
|
||||||
|
"gpt-5.5": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.LOW, Effort.MEDIUM, Effort.HIGH, Effort.XHIGH),
|
||||||
|
evidence=f"{_DOC}: 同 gpt-5.4(OpenRouter supported_efforts 一致,默认 medium)",
|
||||||
|
),
|
||||||
|
"claude-opus-5": ThinkingCapability(
|
||||||
|
supported_efforts=(
|
||||||
|
Effort.NONE,
|
||||||
|
Effort.LOW,
|
||||||
|
Effort.MEDIUM,
|
||||||
|
Effort.HIGH,
|
||||||
|
Effort.XHIGH,
|
||||||
|
Effort.MAX,
|
||||||
|
),
|
||||||
|
evidence=(
|
||||||
|
f"{_DOC}: Anthropic 官方 adaptive thinking + output_config.effort 五档(low/medium/high/"
|
||||||
|
"xhigh/max),默认 high;OpenRouter 标 mandatory:false 故可关。"
|
||||||
|
"关闭档依赖 new-api 把 reasoning_effort=none 转成 thinking 关闭形态,待实测确认"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"claude-sonnet-5": ThinkingCapability(
|
||||||
|
supported_efforts=(
|
||||||
|
Effort.NONE,
|
||||||
|
Effort.LOW,
|
||||||
|
Effort.MEDIUM,
|
||||||
|
Effort.HIGH,
|
||||||
|
Effort.XHIGH,
|
||||||
|
Effort.MAX,
|
||||||
|
),
|
||||||
|
evidence=f"{_DOC}: 同 claude-opus-5(OpenRouter supported_efforts 与默认档一致)",
|
||||||
|
),
|
||||||
|
"gemini-3.1-pro": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.LOW, Effort.MEDIUM, Effort.HIGH),
|
||||||
|
evidence=(
|
||||||
|
f"{_DOC}: **推理不可关闭**——Google 官方文档明确 Gemini 3 Pro / 3.1 Pro 无法关闭思考,"
|
||||||
|
"OpenRouter 亦标 mandatory:true。thinking_level 三档;默认档两源打架"
|
||||||
|
"(官方文档说 HIGH,OpenRouter 说 medium),待实测"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"qwen-plus-latest": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: 百炼 enable_thinking 开关型(thinking_budget 是 token 预算,本库不支持预算型)",
|
||||||
|
),
|
||||||
|
"qwen3.5-flash": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: 同 qwen-plus-latest(OpenRouter 登记无 reasoning_effort)",
|
||||||
|
),
|
||||||
|
"qwen3.6-plus": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: 同 qwen-plus-latest(OpenRouter 登记无 reasoning_effort)",
|
||||||
|
),
|
||||||
|
"qwen3.7-max": ThinkingCapability(
|
||||||
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||||
|
evidence=f"{_DOC}: 同 qwen3.7-plus 一代(OpenRouter 登记无 reasoning_effort)",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -167,6 +167,53 @@ def canonical_sampling_json(merged: Mapping[str, Any]) -> str | None:
|
|||||||
return json.dumps(dict(merged), sort_keys=True, ensure_ascii=False)
|
return json.dumps(dict(merged), sort_keys=True, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
class Effort(StrEnum):
|
||||||
|
"""推理强度档位的封闭词汇(设计 §3.1)。
|
||||||
|
|
||||||
|
取值直接写进请求体(`reasoning_effort` 等键),**改名即改变发出去的字节**,
|
||||||
|
且会进缓存 key 与遥测落库,历史数据会断层。
|
||||||
|
|
||||||
|
八档而非六档: `none`(不推理)与 `auto`(推理,档位由模型自定)必须同时存在。
|
||||||
|
`auto` 不可省——newapi 上 26 个可调用模型里有 9 个是**纯开关型**(qwen 五个、
|
||||||
|
MiniMax-M3、glm-5/5.1/4.6v),它们能开推理却没有强度档可填;没有 `auto` 就只
|
||||||
|
能拿某个强度档冒充"开",而那正是本次要修的病根(旧 `thinking_on` 硬编码
|
||||||
|
`medium`,可 `medium` 在 GLM/kimi/deepseek 的档位表里根本不存在)。
|
||||||
|
|
||||||
|
词汇取四家参考实现共同收敛的一套(cherry-studio 的 canonical selection、
|
||||||
|
OpenRouter 的 `supported_efforts`、LiteLLM 的 `reasoning_effort_levels`、
|
||||||
|
new-api 的 `relayconvert/reasoning`),不自创。
|
||||||
|
|
||||||
|
**枚举定义在最内层而非决策层**: 它是 `SourceConfig`/`ChatRequest`/
|
||||||
|
`LLMResponse` 的字段类型,放进 `thinking.py` 会让 `types.py` 反向 import
|
||||||
|
决策模块(P7 依赖铁律),与 `ThinkingObservation` 同一理由。
|
||||||
|
"""
|
||||||
|
|
||||||
|
NONE = "none"
|
||||||
|
AUTO = "auto"
|
||||||
|
MINIMAL = "minimal"
|
||||||
|
LOW = "low"
|
||||||
|
MEDIUM = "medium"
|
||||||
|
HIGH = "high"
|
||||||
|
XHIGH = "xhigh"
|
||||||
|
MAX = "max"
|
||||||
|
|
||||||
|
|
||||||
|
EFFORT_ORDER: tuple[Effort, ...] = (
|
||||||
|
Effort.NONE,
|
||||||
|
Effort.MINIMAL,
|
||||||
|
Effort.LOW,
|
||||||
|
Effort.MEDIUM,
|
||||||
|
Effort.HIGH,
|
||||||
|
Effort.XHIGH,
|
||||||
|
Effort.MAX,
|
||||||
|
)
|
||||||
|
"""由弱到强的强度序;`AUTO` **不在其中**——它是"由模型自定",在强弱轴上没有位置。
|
||||||
|
|
||||||
|
供能力表求"最省的开启档"与 `nearest` 映射取最近档。公开(非 `_` 前缀)是因为
|
||||||
|
`thinking.py` 要跨模块消费它,跨模块引用私有名是坏味道。
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class ThinkingObservation(StrEnum):
|
class ThinkingObservation(StrEnum):
|
||||||
"""一次调用中"推理是否真的发生"的裁定结果(issue #16/#17)。
|
"""一次调用中"推理是否真的发生"的裁定结果(issue #16/#17)。
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from polygateway.thinking import (
|
|||||||
register_capability,
|
register_capability,
|
||||||
resolve_thinking,
|
resolve_thinking,
|
||||||
)
|
)
|
||||||
from polygateway.types import ThinkingObservation
|
from polygateway.types import Effort, ThinkingObservation
|
||||||
|
|
||||||
|
|
||||||
def _warnings():
|
def _warnings():
|
||||||
@@ -125,7 +125,7 @@ class TestThinkingCapability:
|
|||||||
assert get_capability("some-brand-new-model") is None
|
assert get_capability("some-brand-new-model") is None
|
||||||
|
|
||||||
def test_register_capability_is_pure(self):
|
def test_register_capability_is_pure(self):
|
||||||
table = register_capability("x-1", ThinkingCapability(True, "实测"))
|
table = register_capability("x-1", ThinkingCapability((Effort.NONE, Effort.AUTO), "实测"))
|
||||||
assert get_capability("x-1", table=table) is not None
|
assert get_capability("x-1", table=table) is not None
|
||||||
assert get_capability("x-1") is None # 默认表未被污染
|
assert get_capability("x-1") is None # 默认表未被污染
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ class TestResolveThinking:
|
|||||||
|
|
||||||
def test_unknown_shape_beats_capability_check(self):
|
def test_unknown_shape_beats_capability_check(self):
|
||||||
"""第 2 步先于第 4 步: 形态未知时无从注入,能力如何无关紧要。"""
|
"""第 2 步先于第 4 步: 形态未知时无从注入,能力如何无关紧要。"""
|
||||||
cap = ThinkingCapability(can_disable=False, evidence="构造")
|
cap = ThinkingCapability((Effort.AUTO,), "构造")
|
||||||
with pytest.raises(ValueError, match="register_provider"):
|
with pytest.raises(ValueError, match="register_provider"):
|
||||||
resolve_thinking(get_provider("openai"), cap, False, model="whatever")
|
resolve_thinking(get_provider("openai"), cap, False, model="whatever")
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ class TestReconcileThinking:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_CAP = ThinkingCapability(
|
_CAP = ThinkingCapability(
|
||||||
can_disable=True, evidence="2026-08-02 实测 reasoning_effort=none 可关闭"
|
(Effort.NONE, Effort.AUTO), "2026-08-02 实测 reasoning_effort=none 可关闭"
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_off_but_observed_with_a_registered_capability_blames_the_table(self):
|
def test_off_but_observed_with_a_registered_capability_blames_the_table(self):
|
||||||
@@ -311,3 +311,80 @@ class TestReconcileThinking:
|
|||||||
)
|
)
|
||||||
is None
|
is None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEffortVocabulary:
|
||||||
|
"""八档封闭词汇(设计 §3.1);`auto` 不可省——9 个纯开关型模型无强度档可填。"""
|
||||||
|
|
||||||
|
def test_none_and_auto_are_distinct_members(self):
|
||||||
|
assert Effort.NONE != Effort.AUTO
|
||||||
|
assert Effort("none") is Effort.NONE
|
||||||
|
assert Effort("auto") is Effort.AUTO
|
||||||
|
|
||||||
|
def test_vocabulary_is_exactly_eight(self):
|
||||||
|
assert len(list(Effort)) == 8
|
||||||
|
|
||||||
|
def test_values_are_wire_literals(self):
|
||||||
|
# 档位值直接写进请求体,改名即改变发出去的字节
|
||||||
|
assert [e.value for e in Effort] == [
|
||||||
|
"none",
|
||||||
|
"auto",
|
||||||
|
"minimal",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"xhigh",
|
||||||
|
"max",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class TestCapabilityTierList:
|
||||||
|
"""能力表从 bool 变成档位清单(设计 §3.2);三个派生量不存字段,存了必漂移。"""
|
||||||
|
|
||||||
|
def test_capability_derives_can_disable(self):
|
||||||
|
assert ThinkingCapability((Effort.NONE, Effort.AUTO), "实测").can_disable is True
|
||||||
|
assert ThinkingCapability((Effort.LOW, Effort.MAX), "实测").can_disable is False
|
||||||
|
|
||||||
|
def test_cheapest_effort_skips_none(self):
|
||||||
|
# 「关不掉时的可执行替代」取的是除 none 外最弱的一档
|
||||||
|
assert (
|
||||||
|
ThinkingCapability((Effort.LOW, Effort.HIGH, Effort.MAX), "实测").cheapest_effort
|
||||||
|
is Effort.LOW
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
ThinkingCapability((Effort.NONE, Effort.HIGH, Effort.MAX), "实测").cheapest_effort
|
||||||
|
is Effort.HIGH
|
||||||
|
)
|
||||||
|
assert ThinkingCapability((Effort.NONE, Effort.AUTO), "实测").cheapest_effort is Effort.AUTO
|
||||||
|
assert ThinkingCapability((Effort.AUTO,), "实测").cheapest_effort is Effort.AUTO
|
||||||
|
|
||||||
|
def test_cheapest_effort_is_none_when_only_none(self):
|
||||||
|
# 只能关不能开: 没有可推荐的「最省的开启档」
|
||||||
|
assert ThinkingCapability((Effort.NONE,), "实测").cheapest_effort is None
|
||||||
|
|
||||||
|
def test_is_tiered_excludes_none_and_auto(self):
|
||||||
|
# 纯开关型模型不该被告知「可选档位」——它没有档位
|
||||||
|
assert ThinkingCapability((Effort.NONE, Effort.AUTO), "实测").is_tiered is False
|
||||||
|
assert ThinkingCapability((Effort.AUTO,), "实测").is_tiered is False
|
||||||
|
assert ThinkingCapability((Effort.LOW, Effort.MAX), "实测").is_tiered is True
|
||||||
|
|
||||||
|
def test_empty_efforts_rejected(self):
|
||||||
|
with pytest.raises(ValueError, match="至少"):
|
||||||
|
ThinkingCapability((), "实测")
|
||||||
|
|
||||||
|
def test_duplicate_efforts_rejected(self):
|
||||||
|
with pytest.raises(ValueError, match="重复"):
|
||||||
|
ThinkingCapability((Effort.LOW, Effort.LOW), "实测")
|
||||||
|
|
||||||
|
def test_glm53_cannot_be_disabled(self):
|
||||||
|
# 三源一致(智谱官方文档/cherry-studio/OpenRouter): thinking.type 只接受 enabled
|
||||||
|
cap = get_capability("glm-5.3")
|
||||||
|
assert cap is not None
|
||||||
|
assert cap.can_disable is False
|
||||||
|
assert cap.cheapest_effort is Effort.LOW
|
||||||
|
|
||||||
|
def test_m2_series_still_cannot_be_disabled(self):
|
||||||
|
# 迁移回归: 旧表用 can_disable=False 表达的事实,新表用「none 不在清单里」表达
|
||||||
|
assert get_capability("MiniMax-M2.7").can_disable is False
|
||||||
|
assert get_capability("MiniMax-M2.5").can_disable is False
|
||||||
|
assert get_capability("MiniMax-M3").can_disable is True
|
||||||
|
|||||||
Reference in New Issue
Block a user