feat: warn when the capability table and reality disagree
The M3 evidence sat at 08-02 for twenty-three days while nobody could tell whether it still held. A declaration that goes stale in silence is the failure this issue is really about, so the library now compares what it declared against what it just observed and says so when the two part ways. Judgement is separated from logging: reconcile_thinking returns the warning text, so tests assert on the text instead of parsing logs. Two cases that look alike are kept apart — a model whose capability is registered gets a drift warning quoting its evidence, an unregistered one is never told the table said anything, because it never did. False x UNKNOWN stays silent on purpose. UNKNOWN cannot falsify anything, and warning on it would fire on every disabled call M3 makes over the plain endpoint. A warning that always fires is not a warning.
This commit is contained in:
@@ -179,3 +179,65 @@ def _warn_unregistered(model: str, profile: ProviderProfile, slot: Mapping[str,
|
||||
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 更新登记"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user