2a50ddcf12
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.
412 lines
21 KiB
Python
412 lines
21 KiB
Python
"""推理这件事的全部**决策**: 请求侧注入形态、响应侧结果裁定、二者的对账。
|
|
|
|
与 `providers.py` 的分工: 那里是**注册表**(provider 长什么样,静态声明的存放
|
|
与查找),这里是**决策**(拿声明和响应做判断)。P7"决策逻辑与状态存储分离"。
|
|
|
|
本模块**不定义** `ThinkingObservation` —— 它是 `LLMResponse` 的字段类型,归最
|
|
内层 `types.py`;定义在这里会让 `types.py` 反向 import 决策模块(依赖铁律)。
|
|
"""
|
|
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from types import MappingProxyType
|
|
from typing import Any
|
|
|
|
from loguru import logger
|
|
|
|
from polygateway.providers import ProviderProfile
|
|
from polygateway.types import EFFORT_ORDER, Effort, ThinkingObservation
|
|
|
|
|
|
def observe_thinking(*, thinking: str, reasoning_tokens: int | None) -> ThinkingObservation:
|
|
"""由多信号裁定推理是否发生;判据按**证据硬度**排序(issue #16/#17)。
|
|
|
|
推理正文是事实本身,`reasoning_tokens` 是对事实的转述——转述缺失时事实仍然
|
|
作数。2026-08-25 实测: MiniMax 这一路已不再返回
|
|
`usage.completion_tokens_details`,而同一次调用里库拿得到 185 字符推理正文;
|
|
只认 token 数的判据会把这种情形误判成"没推理"。
|
|
|
|
正文判据取 `strip()` 而非 truthy: 网关响应是外部输入,纯空白串不是证据(P5)。
|
|
|
|
判不出来时返回 `UNKNOWN` 而非 `ABSENT`——**不许把"没看见"说成"没发生"**。
|
|
"""
|
|
if thinking.strip():
|
|
return ThinkingObservation.OBSERVED
|
|
# 负数与 None 同档: `ABSENT` 是"上游明确上报未推理"这个最强的正面结论,坏
|
|
# 数据给不出它。当前 transport 已在边界把负数归 None,这里仍要自己闭合——本
|
|
# 函数对外承诺"外部输入校验后使用",第二个 transport 直接填该值时,漏判会
|
|
# 给出一个方向相反的强结论(P5)
|
|
if reasoning_tokens is None or reasoning_tokens < 0:
|
|
return ThinkingObservation.UNKNOWN
|
|
return ThinkingObservation.OBSERVED if reasoning_tokens > 0 else ThinkingObservation.ABSENT
|
|
|
|
|
|
class ThinkingUnsupportedError(ValueError):
|
|
"""推理开关无法满足: 形态未知或该模型不支持该方向(issue #5)。
|
|
|
|
是 `ValueError` 的子类而非 `errors.py` 四分类之一——它描述的是**配置**
|
|
不可满足(装配期就该炸),不是一次调用的运行时失败。transport 在请求期
|
|
捕获它并翻译为 `RequestRejectedError` 再进四分类。单列一个类型是为了让
|
|
捕获点能精确到它,而不是宽catch 整个 `ValueError`(那会把序列化等无关
|
|
错误误贴成"推理开关无法满足")。
|
|
"""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ThinkingCapability:
|
|
"""某个**具体模型**支持哪些推理档位(设计 §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/` 下六档)。
|
|
"""
|
|
|
|
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(
|
|
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 是真开关。"
|
|
"无强度档: 官方只有开/关两态(thinking.type disabled/adaptive)"
|
|
),
|
|
),
|
|
"MiniMax-M2.7": ThinkingCapability(
|
|
supported_efforts=(Effort.AUTO,),
|
|
evidence=(
|
|
"2026-08-02 实测 reasoning_effort=none / thinking:{disabled} / thinking:{adaptive} "
|
|
"各 N=3 全部无效;OpenRouter 注册表登记 mandatory:true,models.dev 登记无控制手段。"
|
|
"MiniMax 官方亦承认 M2.x 接受 disabled 但推理仍开着"
|
|
),
|
|
),
|
|
"MiniMax-M2.5": ThinkingCapability(
|
|
supported_efforts=(Effort.AUTO,),
|
|
evidence="2026-08-02 实测同 M2.7: 三种形态各 N=3 全部无效;外部注册表同样登记为强制推理",
|
|
),
|
|
"qwen3.7-plus": ThinkingCapability(
|
|
supported_efforts=(Effort.NONE, Effort.AUTO),
|
|
evidence=(
|
|
"2026-08-02 实测 enable_thinking=false 关闭(completion 5 token,无推理)。"
|
|
"无强度档: OpenRouter 登记本型号只支持 reasoning 开关,不支持 reasoning_effort"
|
|
),
|
|
),
|
|
"deepseek-v4-pro": ThinkingCapability(
|
|
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)",
|
|
),
|
|
}
|
|
)
|
|
"""在用模型的推理能力登记(YAGNI: 不覆盖全世界,未登记走 `resolve_thinking` 退化)。"""
|
|
|
|
|
|
def get_capability(
|
|
model: str, *, table: Mapping[str, ThinkingCapability] | None = None
|
|
) -> ThinkingCapability | None:
|
|
"""按模型名精确查找;未登记返回 None(= 能力未知,由调用方决定如何退化)。
|
|
|
|
与 `get_provider` 未注册即报错不同: provider 是配置里写死的少数几个值,
|
|
写错就是配置错误;而模型名千变万化,新模型上线不该被库挡住(设计 §5 R4)。
|
|
"""
|
|
return (DEFAULT_CAPABILITIES if table is None else table).get(model)
|
|
|
|
|
|
def register_capability(
|
|
model: str,
|
|
capability: ThinkingCapability,
|
|
*,
|
|
base: Mapping[str, ThinkingCapability] | None = None,
|
|
) -> dict[str, ThinkingCapability]:
|
|
"""纯函数注册: 返回 base(缺省 DEFAULT_CAPABILITIES)+ 新条目的新表,同名覆盖。"""
|
|
table = dict(DEFAULT_CAPABILITIES if base is None else base)
|
|
table[model] = capability
|
|
return table
|
|
|
|
|
|
def resolve_thinking(
|
|
profile: ProviderProfile,
|
|
capability: ThinkingCapability | None,
|
|
enable_thinking: bool | None,
|
|
*,
|
|
model: str,
|
|
warn_unregistered: bool = True,
|
|
) -> Mapping[str, Any]:
|
|
"""三态 + 两层能力 → 请求体注入片段;不可满足时 ValueError。
|
|
|
|
调用点负责翻译: 装配期直接冒泡(配置错误),transport 内翻译为
|
|
`RequestRejectedError`(四分类之一)。判定顺序即语义,不可调换——形态未知时
|
|
无从注入,能力如何无关紧要,故 Phase 2 必须先于 Phase 4;未登记模型没有
|
|
`can_disable` 可读,故 Phase 3 必须先于 Phase 4。
|
|
|
|
`model` 只用于错误与告警文案: 报错能定位到具体模型才有可操作性,而
|
|
`capability` 为 None(未登记)时无从从别处取得模型名。
|
|
|
|
`warn_unregistered=False` 供请求热路径去重用: 装配期已经喊过一次,逐次
|
|
调用再喊只会刷屏。判定结果不受此参数影响。
|
|
"""
|
|
# 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"
|
|
# Phase 2: 形态未知 —— 提供了开关却不知道怎么发,静默放行就是欺骗调用方
|
|
if slot is None:
|
|
raise ThinkingUnsupportedError(
|
|
f"provider {profile.name!r} 的 {direction} 形态未知(模型 {model!r}): "
|
|
f"本库不知道该 provider 如何表达这一档。请用 register_provider 注册形态,"
|
|
f"或改用 SourceConfig.extra_body 直接下发供应商参数"
|
|
)
|
|
# Phase 3: 能力未登记 —— 新模型上线不该被库挡住,但也不该假装成功
|
|
if capability is None:
|
|
if warn_unregistered:
|
|
_warn_unregistered(model, profile, slot)
|
|
return slot
|
|
# Phase 4: 明确不支持关闭 —— 调用方要的是"不推理"的语义保证,给不了必须说
|
|
if enable_thinking is False and not capability.can_disable:
|
|
raise ThinkingUnsupportedError(
|
|
f"模型 {model!r} 无法关闭推理,enable_thinking=False 无法满足: "
|
|
f"{capability.evidence}。该模型的推理是固有属性,任何参数都关不掉——"
|
|
f"需要关闭思维链请换用支持关闭的模型"
|
|
)
|
|
return slot
|
|
|
|
|
|
def _warn_unregistered(model: str, profile: ProviderProfile, slot: Mapping[str, Any]) -> None:
|
|
logger.warning(
|
|
"模型 {} 的推理能力未登记,按 provider {} 的形态尽力注入 {};"
|
|
"若该模型实际不支持这一档,本次设置将静默失效。实测后请用 register_capability 登记",
|
|
model,
|
|
profile.name,
|
|
dict(slot),
|
|
)
|
|
|
|
|
|
def reconcile_thinking(
|
|
*,
|
|
enable_thinking: bool | None,
|
|
observation: ThinkingObservation,
|
|
capability: ThinkingCapability | None,
|
|
model: str,
|
|
) -> str | None:
|
|
"""把静态声明与运行时观测对账;矛盾返回告警文案,无矛盾返回 None。
|
|
|
|
能力表过期是必然事件(M3 的 evidence 曾停在 8-02 整整 23 天),而过期的
|
|
表现是静默错觉。本函数把它变成可报警事件,代价是一次枚举比较。
|
|
|
|
**只判定、不打日志**: 文案作为返回值交给调用点,单测才能直接断言告警内容,
|
|
而不必去解析日志格式;节流也才能留在握有实例状态的 transport 里。
|
|
|
|
**不抛错**: 一次观测不足以否决一次成功的调用;可观测性属遥测方向,降级即
|
|
warning(P5 的"报错而非放行"只约束限流/熔断)。矛盾结果已随 `LLMResponse`
|
|
与遥测落地,处置权归下游。
|
|
"""
|
|
# Phase 1: 调用方不表态 —— 没提要求就无从谈"违背"
|
|
if enable_thinking is None:
|
|
return None
|
|
# Phase 2: 要求关闭 —— 只有 OBSERVED 能证伪。UNKNOWN 没有证伪力,拿它报警
|
|
# 等于每次关闭调用都喊一遍(M3 关闭档恒落此档),噪声即等于没有告警
|
|
if enable_thinking is False:
|
|
if observation is not ThinkingObservation.OBSERVED:
|
|
return None
|
|
return _off_but_observed(model, capability)
|
|
# Phase 3: 要求开启 —— ABSENT 是正面证伪,UNKNOWN 是"看不见",两者文案不可混
|
|
if observation is ThinkingObservation.ABSENT:
|
|
return (
|
|
f"模型 {model!r} 的 enable_thinking=True 未生效: 已注入开启参数,"
|
|
f"上游却明确上报本次未推理(reasoning_tokens=0)"
|
|
)
|
|
if observation is ThinkingObservation.UNKNOWN:
|
|
return (
|
|
f"模型 {model!r} 的 enable_thinking=True 无法确认是否生效: 已注入开启参数,"
|
|
f"但本次响应观测不到任何推理信号(推理正文与 usage 明细双缺)。"
|
|
f"若走的是非流式路径,推理内容可能已计费却不回传"
|
|
)
|
|
return None
|
|
|
|
|
|
def _off_but_observed(model: str, capability: ThinkingCapability | None) -> str:
|
|
"""关闭请求未被满足的两种说法;登记与否决定该说哪一句。
|
|
|
|
两者必须分开: `resolve_thinking` 对未登记模型的告警是**事前猜测**,这里是
|
|
**事后实证**。对未登记模型说"能力表声称可关闭"是错的——它根本没登记。
|
|
"""
|
|
if capability is None:
|
|
return (
|
|
f"模型 {model!r} 的 enable_thinking=False 未被满足: 实测观测到推理发生,"
|
|
f"且该模型的推理能力尚未登记(本次按 provider 形态尽力注入)。"
|
|
f"请实测后用 register_capability 登记其真实能力"
|
|
)
|
|
return (
|
|
f"模型 {model!r} 的 enable_thinking=False 未被满足: 实测观测到推理发生,"
|
|
f"而能力表登记 can_disable={capability.can_disable}(evidence: {capability.evidence})。"
|
|
f"能力表可能已过期——请复测后用 register_capability 更新登记"
|
|
)
|