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,
|
||||
)
|
||||
from polygateway.types import (
|
||||
EFFORT_ORDER,
|
||||
Effort,
|
||||
EmbeddingResponse,
|
||||
LLMResponse,
|
||||
OcrLayoutElement,
|
||||
@@ -46,6 +48,8 @@ __version__ = "1.3.2"
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_PROFILES",
|
||||
"EFFORT_ORDER",
|
||||
"Effort",
|
||||
"AllSourcesExhausted",
|
||||
"CircuitOpenError",
|
||||
"EmbeddingClient",
|
||||
|
||||
+176
-12
@@ -15,7 +15,7 @@ from typing import Any
|
||||
from loguru import logger
|
||||
|
||||
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:
|
||||
@@ -54,50 +54,214 @@ class ThinkingUnsupportedError(ValueError):
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThinkingCapability:
|
||||
"""某个**具体模型**能否关闭推理(issue #5);登记必须附实测证据与日期。
|
||||
"""某个**具体模型**支持哪些推理档位(设计 §3.2);登记必须附证据与日期。
|
||||
|
||||
与 `ProviderProfile` 的分工: 后者声明**形态**(参数长什么样,按 provider 变,
|
||||
数年不变一次),本类声明**能力**(按 model 变,同一 provider 每代都变)。二者
|
||||
合一在 provider 级表达不了代际差异——实测 MiniMax-M3 可关闭推理,而同厂的
|
||||
M2.7/M2.5 三种参数形态全部无效(findings §2.3),profile 一格管不住三个模型。
|
||||
|
||||
**档位清单而非布尔**(2026-09-04): 旧版是 `can_disable: bool`,表达不了
|
||||
"关不掉但能调到最低档"这第三种情况——而 GLM-5.3 系与 Gemini 3 Pro 都是它。
|
||||
现在"能不能关"就是 `Effort.NONE` 在不在清单里,是派生量而非独立字段;三个
|
||||
派生量一律不存字段,存了必与清单漂移。
|
||||
|
||||
`evidence` 不是装饰: 能力表过期是必然事件,没有出处就无从判断该不该信它。
|
||||
文档推定与实测必须在 evidence 里说清楚是哪种——前者会被 new-api 中转改写
|
||||
(LiteLLM 里同一个 kimi-k3 在 `moonshot/` 下三档、`perplexity/` 下六档)。
|
||||
"""
|
||||
|
||||
can_disable: bool
|
||||
supported_efforts: tuple[Effort, ...]
|
||||
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(
|
||||
{
|
||||
# —— 实测条目(2026-08-02/08-25),证据原文保留 ——
|
||||
"MiniMax-M3": ThinkingCapability(
|
||||
can_disable=True,
|
||||
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||
evidence=(
|
||||
"2026-08-02 经 new-api 中转实测 N=10: reasoning_effort=none 稳定关闭,零跳变;"
|
||||
"2026-08-25 复测依然成立(prompt 194 = 基线、completion 3、无推理正文)。"
|
||||
"两条限制(findings 2026-08-25-thinking-observability-regression §3.1/§5): "
|
||||
"① 非流式路径观测不到推理信号——推理已计费,但正文与 usage 明细都不回传;"
|
||||
"② enable_thinking / thinking:{type:enabled} 对本模型无效,仅 reasoning_effort 是真开关"
|
||||
"② enable_thinking / thinking:{type:enabled} 对本模型无效,仅 reasoning_effort 是真开关。"
|
||||
"无强度档: 官方只有开/关两态(thinking.type disabled/adaptive)"
|
||||
),
|
||||
),
|
||||
"MiniMax-M2.7": ThinkingCapability(
|
||||
can_disable=False,
|
||||
supported_efforts=(Effort.AUTO,),
|
||||
evidence=(
|
||||
"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(
|
||||
can_disable=False,
|
||||
supported_efforts=(Effort.AUTO,),
|
||||
evidence="2026-08-02 实测同 M2.7: 三种形态各 N=3 全部无效;外部注册表同样登记为强制推理",
|
||||
),
|
||||
"qwen3.7-plus": ThinkingCapability(
|
||||
can_disable=True,
|
||||
evidence="2026-08-02 实测 enable_thinking=false 关闭(completion 5 token,无推理)",
|
||||
supported_efforts=(Effort.NONE, Effort.AUTO),
|
||||
evidence=(
|
||||
"2026-08-02 实测 enable_thinking=false 关闭(completion 5 token,无推理)。"
|
||||
"无强度档: OpenRouter 登记本型号只支持 reasoning 开关,不支持 reasoning_effort"
|
||||
),
|
||||
),
|
||||
"deepseek-v4-pro": ThinkingCapability(
|
||||
can_disable=True,
|
||||
evidence="2026-08-02 实测 thinking:{type:disabled} 关闭(completion 3 token,无推理)",
|
||||
supported_efforts=(Effort.NONE, Effort.HIGH, Effort.MAX),
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""一次调用中"推理是否真的发生"的裁定结果(issue #16/#17)。
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from polygateway.thinking import (
|
||||
register_capability,
|
||||
resolve_thinking,
|
||||
)
|
||||
from polygateway.types import ThinkingObservation
|
||||
from polygateway.types import Effort, ThinkingObservation
|
||||
|
||||
|
||||
def _warnings():
|
||||
@@ -125,7 +125,7 @@ class TestThinkingCapability:
|
||||
assert get_capability("some-brand-new-model") is None
|
||||
|
||||
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") is None # 默认表未被污染
|
||||
|
||||
@@ -175,7 +175,7 @@ class TestResolveThinking:
|
||||
|
||||
def test_unknown_shape_beats_capability_check(self):
|
||||
"""第 2 步先于第 4 步: 形态未知时无从注入,能力如何无关紧要。"""
|
||||
cap = ThinkingCapability(can_disable=False, evidence="构造")
|
||||
cap = ThinkingCapability((Effort.AUTO,), "构造")
|
||||
with pytest.raises(ValueError, match="register_provider"):
|
||||
resolve_thinking(get_provider("openai"), cap, False, model="whatever")
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestReconcileThinking:
|
||||
"""
|
||||
|
||||
_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):
|
||||
@@ -311,3 +311,80 @@ class TestReconcileThinking:
|
||||
)
|
||||
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