feat: wire the tier through the transport and keep each tier's warning distinct

The transport now hands back the tier it actually sent, and that tier
rides TransportResult into LLMResponse. It is not the requested one:
under EFFORT_FALLBACK=nearest a medium request goes out as low, and
telemetry grouping by the requested tier would file the row under a tier
that never left the process.

Reconciliation judges the same tier instead of the old enable_thinking
bool, and the warning throttle keys on it. Keyed on the bool, every tier
of one model shared a single key, so the second contradiction was
silenced for the lifetime of the transport. The predicate is an identity
check against Effort.NONE on purpose -- the member's value is the
non-empty string "none", so any truthiness test would send every strength
tier down the "asked to disable" branch and invert the alarm.
This commit is contained in:
2026-09-05 05:00:29 -04:00
parent 5dfb15e6a2
commit 848dc0aa7f
8 changed files with 279 additions and 48 deletions
+3
View File
@@ -395,6 +395,9 @@ class RetryMW:
reasoning_tokens=result.reasoning_tokens,
# 裁定归 transport(它才见得到原始信号),本层只搬运不改判
thinking_observation=result.thinking_observation,
# 同理: 实际档由做注入的那一层裁定(`nearest` 映射后与请求档分叉),
# 本层若"顺手"改读 request.reasoning_effort,记的就是从未发出过的档
applied_effort=result.applied_effort,
)
async def _emit(
+22 -9
View File
@@ -575,7 +575,7 @@ def _warn_unregistered(
def reconcile_thinking(
*,
enable_thinking: bool | None,
effort: Effort | None,
observation: ThinkingObservation,
capability: ThinkingCapability | None,
model: str,
@@ -585,6 +585,17 @@ def reconcile_thinking(
能力表过期是必然事件(M3 的 evidence 曾停在 8-02 整整 23 天),而过期的
表现是静默错觉。本函数把它变成可报警事件,代价是一次枚举比较。
**判据是档位而非布尔**(2026-09-05,设计 §4.3): `Effort.NONE` 走"要求关闭"
一支,其余任何档走"要求开启"一支,`None`(不表态)仍沉默。判据必须写成
`is Effort.NONE` 的**身份比较**——它的取值是非空串 `"none"`,任何靠真值性
的写法(`if not effort`)都恒为假,会把每个强度档送进关闭分支,告警方向整个
颠倒。传入的应是**实际发出去**的那一档(`nearest` 映射后与请求档分叉),
否则文案会说一个从未发出过的档。
**不新增**「档位高低 vs `reasoning_tokens` 多少」的对账(设计 §4.3/§11 第 1
条): 二者没有可判定的函数关系(实测同一档 rt 在 8~56 之间跳),拿它报警必然
是噪声,而噪声等于没有告警。该问题归 §11 的压测,不进库。
**只判定、不打日志**: 文案作为返回值交给调用点,单测才能直接断言告警内容,
而不必去解析日志格式;节流也才能留在握有实例状态的 transport 里。
@@ -593,24 +604,26 @@ def reconcile_thinking(
与遥测落地,处置权归下游。
"""
# Phase 1: 调用方不表态 —— 没提要求就无从谈"违背"
if enable_thinking is None:
if effort is None:
return None
# Phase 2: 要求关闭 —— 只有 OBSERVED 能证伪。UNKNOWN 没有证伪力,拿它报警
# 等于每次关闭调用都喊一遍(M3 关闭档恒落此档),噪声即等于没有告警
if enable_thinking is False:
if effort is Effort.NONE:
if observation is not ThinkingObservation.OBSERVED:
return None
return _off_but_observed(model, capability)
# Phase 3: 要求开启 —— ABSENT 是正面证伪,UNKNOWN 是"看不见",两者文案不可混
# Phase 3: 要求开启(含 auto 与各强度档)—— ABSENT 是正面证伪,UNKNOWN 是
# "看不见",两者文案不可混。文案写出**是哪一档**: transport 的节流键正按档
# 分离,文案不分档的话,两条告警长得一模一样,看的人分不出是哪一档出的问题
if observation is ThinkingObservation.ABSENT:
return (
f"模型 {model!r}enable_thinking=True 未生效: 已注入开启参数,"
f"模型 {model!r}reasoning_effort={effort.value!r} 未生效: 已注入开启参数,"
f"上游却明确上报本次未推理(reasoning_tokens=0)"
)
if observation is ThinkingObservation.UNKNOWN:
return (
f"模型 {model!r}enable_thinking=True 无法确认是否生效: 已注入开启参数,"
f"但本次响应观测不到任何推理信号(推理正文与 usage 明细双缺)。"
f"模型 {model!r}reasoning_effort={effort.value!r} 无法确认是否生效: "
f"已注入开启参数,但本次响应观测不到任何推理信号(推理正文与 usage 明细双缺)。"
f"若走的是非流式路径,推理内容可能已计费却不回传"
)
return None
@@ -624,12 +637,12 @@ def _off_but_observed(model: str, capability: ThinkingCapability | None) -> str:
"""
if capability is None:
return (
f"模型 {model!r}enable_thinking=False 未被满足: 实测观测到推理发生,"
f"模型 {model!r}reasoning_effort='none' 未被满足: 实测观测到推理发生,"
f"且该模型的推理能力尚未登记(本次按 provider 形态尽力注入)。"
f"请实测后用 register_capability 登记其真实能力"
)
return (
f"模型 {model!r}enable_thinking=False 未被满足: 实测观测到推理发生,"
f"模型 {model!r}reasoning_effort='none' 未被满足: 实测观测到推理发生,"
f"而能力表登记 can_disable={capability.can_disable}(evidence: {capability.evidence})。"
f"能力表可能已过期——请复测后用 register_capability 更新登记"
)
+44 -26
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import json
import re
import time
from dataclasses import replace
from typing import TYPE_CHECKING, Any
import httpx
@@ -333,7 +334,7 @@ class OpenAICompatTransport:
# 一个容器会让两种告警的生命周期纠缠在一起——将来任一侧想加清空/过期策略,
# 都会连带改掉另一侧的行为。(键空间恰好不相交,故当下**不会**互相压制;
# 分开维护的理由是语义,不是碰撞)
self._warned_mismatches: set[tuple[str, str, bool | None]] = set()
self._warned_mismatches: set[tuple[str, str, Effort | None]] = set()
self._client_factory = client_factory or _default_client_factory
self._clients: dict[str, httpx.AsyncClient] = {}
@@ -353,7 +354,13 @@ class OpenAICompatTransport:
stream: bool,
overlay: dict[str, Any],
reasoning_effort: Effort | None,
) -> dict[str, Any]:
) -> tuple[dict[str, Any], Effort | None]:
"""组装请求体,并交回本次**实际**发出去的档位(`None` = 未表态,不注入)。
返回二元组而非只返回 payload: 实际档在 `nearest` 映射后与请求档分叉,而
除本函数外没有第二处知道映射结果——不交出去,遥测就只能事后再算一遍,
算出来必是请求档。
"""
payload: dict[str, Any] = {"model": source.model, "messages": messages, "stream": stream}
if stream:
payload["stream_options"] = {"include_usage": True} # 强制 usage 帧(三项目同款)
@@ -364,25 +371,26 @@ class OpenAICompatTransport:
self._warned_models.add(source.model)
# 三层优先级在此汇合: 请求级 > 源级 > enable_thinking 语法糖(设计 §4.2)。
# 判定与装配守卫共用同一个纯函数,两处分叉就会变成"装配期放行、运行期报错"
payload.update(
resolve_thinking(
profile,
capability,
effective_effort(
request_effort=reasoning_effort,
source_effort=source.reasoning_effort,
enable_thinking=source.enable_thinking,
),
model=source.model,
fallback=source.effort_fallback,
warn_unregistered=first_time,
).payload
resolution = resolve_thinking(
profile,
capability,
effective_effort(
request_effort=reasoning_effort,
source_effort=source.reasoning_effort,
enable_thinking=source.enable_thinking,
),
model=source.model,
# 源级 `EFFORT_FALLBACK` 必须真的走到这里: 硬编码 "error" 会让人类明确
# 要求实现的 `nearest` 在零告警下变成死代码(2026-09-05 独立验证查出)
fallback=source.effort_fallback,
warn_unregistered=first_time,
)
payload.update(resolution.payload)
# 顺序即优先级(issue #4 设计决策 A): 配置级 extra_body 在前,调用级
# overlay(含结构化注入)在后覆盖之。两行不可调换
payload.update(source.extra_body)
payload.update(overlay)
return payload
return payload, resolution.applied_effort
async def complete(
self,
@@ -401,7 +409,7 @@ class OpenAICompatTransport:
"""
profile = get_provider(source.provider, registry=self._registry)
try:
payload = self._build_payload(
payload, applied_effort = self._build_payload(
messages=messages,
source=source,
profile=profile,
@@ -432,27 +440,37 @@ class OpenAICompatTransport:
except httpx.TransportError as exc:
# VT 宽集: 覆盖断连/协议错误/读写失败(设计 §9 行 8)
raise TransientError(f"{source.name} 网络错误: {exc}", **ctx) from exc
# 此处是唯一同时握有请求方向与响应结果的地方,对账只能落在这里
# 实际发出去的档只有 `_build_payload` 知道,而组装 TransportResult 的两条
# 路径都在更深一层。在此唯一汇合点补齐,好过给两条路径各加一个参数——那正是
# 遥测那边被明令禁止的"复制参数列表"形态,两条路径迟早只改一条
result = replace(result, applied_effort=applied_effort)
# 此处是唯一同时握有请求档位与响应结果的地方,对账只能落在这里
self._warn_on_thinking_mismatch(source, result)
return result
def _warn_on_thinking_mismatch(self, source: SourceConfig, result: TransportResult) -> None:
"""声明与观测矛盾即 warning;按 (source, model, direction) 节流,同组合只喊一次。
"""声明与观测矛盾即 warning;按 (source, model, 实际档位) 节流,同组合只喊一次。
三段缺一不可。**方向**: 同一模型的开、关两档是两个独立的矛盾。**源名**:
多源多账号是本库的核心场景,同一 model 跨 N 个源是常态,而每个源背后是
独立的账号/网关,一个源的行为不代表另一个——漏掉源名,5 个源里第一个出
问题的喊完一次,其余四个永久静音。逐次调用刷屏会把告警变成噪声,噪声等于
没有告警。
三段缺一不可。**档位**: 同一模型的 low 与 max 是两个独立的矛盾,共用一个
键会让第二个永久静音(旧版拿 `enable_thinking` 当第三段,而档位根本不经过
那个字段,于是同一模型的所有档共用一个键)。**源名**: 多源多账号是本库的
核心场景,同一 model 跨 N 个源是常态,而每个源背后是独立的账号/网关,一个
源的行为不代表另一个——漏掉源名,5 个源里第一个出问题的喊完一次,其余四个
永久静音。逐次调用刷屏会把告警变成噪声,噪声等于没有告警。
档位取 `result.applied_effort`(真正发出去的那一档)而非请求档: `nearest`
映射后二者分叉,而对账问的是"我发出去的要求有没有被满足"——拿一个从未发出
过的档去对账,文案与键都指向了一次不存在的请求。被映射到同一档的两个请求
因此共用一个键,这正是它们该有的关系(同一条实际要求,同一个矛盾)。
**先判键再对账**: `reconcile_thinking` 会拼含完整 `evidence` 的长字符串,
而非流式档每次调用都命中这一分支,节流后再拼是纯粹的热路径浪费。
"""
key = (source.name, source.model, source.enable_thinking)
key = (source.name, source.model, result.applied_effort)
if key in self._warned_mismatches:
return
message = reconcile_thinking(
enable_thinking=source.enable_thinking,
effort=result.applied_effort,
observation=result.thinking_observation,
capability=get_capability(source.model, table=self._capabilities),
model=source.model,
+17
View File
@@ -326,6 +326,16 @@ class LLMResponse:
实测开启档 completion 53 vs 关闭档 3),该档即为 `UNKNOWN`。
要判"确实没推理"只认 `ABSENT`(上游明确上报 0)。"""
applied_effort: Effort | None = None
"""本次调用**真正发出去**的推理档位(issue #20);`None` = 调用方未表态。
与 `ChatRequest.reasoning_effort`(请求档)可能分叉: 源上配了
`EFFORT_FALLBACK=nearest` 时,请求 `medium` 而模型只有 low/high/max,实际发
出的是 `low`。遥测按本字段分组,记请求档会把整行挂在一个从未发出过的档下。
`None` 不是"没推理": 库不表态时也不推定模型自己的默认档——"没看见"不许说成
"发生了"(同 `thinking_observation` 的 `UNKNOWN` 一脉)。"""
@dataclass(frozen=True)
class ChatRequest:
@@ -438,6 +448,13 @@ class TransportResult:
默认 `UNKNOWN` 而非 `ABSENT`: 不做裁定的 transport(OCR/embedding 等)沉默
时,不该替上游做出"没推理"这个它从未做过的声明。"""
applied_effort: Effort | None = None
"""本次调用真正发出去的推理档位(issue #20),由做注入的 transport 填。
只有做了注入的那一层知道它: `nearest` 映射后请求档与实际档分叉(请求
`medium` → 实发 `low`),中间件事后再算一遍必然算成请求档。默认 `None` 是
"未表态/不注入推理参数"(OCR、embedding 等 transport 沉默即此)。"""
@dataclass(frozen=True)
class SourceConfig:
+46 -1
View File
@@ -312,6 +312,52 @@ class TestEffortFallbackWiring:
assert captured == [] # 请求根本没发出去
class TestAppliedTierReachesTheCaller:
"""`LLMResponse.applied_effort` 报的是**真正发出去的**那一档(计划 T8-5)。
对下游是新能力(它终于能知道这次跑在哪档),对遥测是前置条件: 记请求档会让
按档分组的压测把整行挂在一个从未发出过的档下,而那种数据错得看不出来。
"""
async def test_response_carries_the_mapped_tier(self):
"""glm-5.3 无 `medium`: 开了 nearest 后实际跑的是 low,响应必须这么说。"""
source = _source(provider="zhipu", model="glm-5.3", effort_fallback="nearest")
async with _client(sources=[source]) as client:
resp = await client.chat(
[{"role": "user", "content": "hi"}], reasoning_effort=Effort.MEDIUM
)
assert resp.applied_effort is Effort.LOW
async def test_no_statement_leaves_the_field_none(self):
async with _client() as client:
resp = await client.chat([{"role": "user", "content": "hi"}])
assert resp.applied_effort is None
class TestUnsupportedTierIsRefusedNotRetried:
"""档位不可满足 = 请求本身的问题: 报 `RequestRejectedError`,不重试、不伤熔断。
重试与换源都不会让它变对(设计 §10),而把它计进熔断更糟——一次配置错误会
把一个健康的源关掉,拖垮与推理无关的所有调用。
"""
async def test_tier_error_never_reaches_the_gateway_or_the_breaker(self):
sent = []
def handler(request):
sent.append(request)
return _sse()
# 阈值取 1: 只要这次失败被计进熔断,门当场开路,断言立刻可见
gate = InMemoryGate(config=BreakerConfig(1, 60.0, 120.0))
source = _source(name="zp", provider="zhipu", model="glm-5.3")
async with _client(sources=[source], handler=handler, breaker=gate) as client:
with pytest.raises(RequestRejectedError, match="无法关闭推理"):
await client.chat([{"role": "user", "content": "hi"}], reasoning_effort=Effort.NONE)
assert sent == [], "请求根本不该发出去: 档位不可满足在组装期就已判定"
assert (await gate.try_enter("zp", "w")).allowed, "配置错误不得计入熔断失败"
class TestRequestTierNormalization:
"""`chat(reasoning_effort=...)` 是公共入口,裸字符串必须在此归一(issue #20)。
@@ -515,7 +561,6 @@ class TestModelFingerprint:
expected = "qwen-max|" + hashlib.sha256(mark.encode("utf-8")).hexdigest()
assert build_model_fingerprint([_source(extra_body={"temperature": 0})]) == expected
def test_declared_tier_fingerprint_is_a_golden(self):
"""配了档位那一侧的指纹字面量也要钉死: 它变了就是该源整段缓存冷启动。
+55 -1
View File
@@ -23,7 +23,7 @@ from polygateway.transports.openai_compat import (
_iter_sse_deltas,
_sse_data_payload,
)
from polygateway.types import ChatRequest, LLMResponse, SourceConfig, ThinkingObservation
from polygateway.types import ChatRequest, Effort, LLMResponse, SourceConfig, ThinkingObservation
def _source(**overrides):
@@ -629,6 +629,60 @@ class TestThinkingReconciliation:
hits = [m for m in messages if "MiniMax-M3" in m]
assert len(hits) == 2, f"两个方向各应告警一次,实得 {len(hits)}"
async def test_each_tier_of_one_model_earns_its_own_warning(self):
"""同一源同一模型的两个强度档是**两个独立的矛盾**,不得共用一个节流键。
节流键沿用旧的 `enable_thinking` 三态时,两次请求的键逐字相同(都是
`None`——档位根本不经过那个字段),于是 `max` 档的矛盾被 `low` 档那次
永久静音。档位化后 low 与 max 各喊一次,重复的 low 仍只喊一次。
"""
transport = _transport_for(self._zero_signal)
source = _source(name="zp", provider="zhipu", model="glm-5.3")
messages: list[str] = []
sink_id = logger.add(messages.append, level="WARNING")
try:
await _complete(transport, source, reasoning_effort=Effort.LOW)
await _complete(transport, source, reasoning_effort=Effort.LOW)
await _complete(transport, source, reasoning_effort=Effort.MAX)
finally:
logger.remove(sink_id)
hits = [m for m in messages if "glm-5.3" in m]
assert len(hits) == 2, f"low 与 max 应各告警一次,实得 {len(hits)}"
def _zero_signal(self, request):
"""零推理信号的成功响应 → UNKNOWN,与"要求开启"矛盾(M3 实测形态)。"""
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
class TestAppliedTierLeavesTheTransport:
"""本次**实际**发出去的档必须随 TransportResult 上浮(设计 §4.1 / 计划 T8-5)。
不上浮就只能由遥测自己再算一遍请求档,而 `nearest` 映射后两者不同——压测
要按档分组的那一列会挂在一个从未真正发出过的档下,且错得看不出来。
"""
def _ok(self, request):
return _sse_stream(_chunk(content="ok"), _chunk(usage=_USAGE))
async def test_result_carries_the_mapped_tier_not_the_requested_one(self):
"""glm-5.3 只有 low/high/max: 请求 `medium`,实际发出的是 `low`。"""
transport = _transport_for(self._ok)
source = _source(provider="zhipu", model="glm-5.3", effort_fallback="nearest")
result = await _complete(transport, source, reasoning_effort=Effort.MEDIUM)
assert result.applied_effort is Effort.LOW
async def test_result_carries_the_tier_that_was_asked_for_when_supported(self):
transport = _transport_for(self._ok)
source = _source(provider="zhipu", model="glm-5.3")
result = await _complete(transport, source, reasoning_effort=Effort.MAX)
assert result.applied_effort is Effort.MAX
async def test_no_statement_stays_none(self):
"""不表态时库既不注入也不推定模型默认档——"没看见"不许说成"发生了""""
transport = _transport_for(self._ok)
result = await _complete(transport, _source())
assert result.applied_effort is None
class TestNonStreamFastPath:
async def test_non_stream_parses_message(self):
+27
View File
@@ -288,6 +288,33 @@ class TestObservabilityPassthrough:
resp = await mw(_REQ)
assert resp.thinking_observation is ThinkingObservation.UNKNOWN
async def test_applied_tier_reaches_the_response(self):
"""issue #20: 实际发出的档由 transport 裁定,本层只搬运。
搬运这一步漏掉,`LLMResponse.applied_effort` 恒为 None,而遥测正是从这个
字段取"这一行跑在哪档"——整列会静默地全是 NULL。
"""
result = TransportResult(
content="ok",
thinking="",
prompt_tokens=10,
completion_tokens=5,
usage_source="measured",
ttft_ms=12.0,
max_inter_token_ms=3.0,
raw={},
applied_effort=Effort.HIGH,
)
mw, *_ = _harness([_src("a")], [result])
resp = await mw(_REQ)
assert resp.applied_effort is Effort.HIGH
async def test_unstated_tier_stays_none(self):
"""不表态的调用不得被填成某个档: 那等于替调用方声称它做过一个选择。"""
mw, *_ = _harness([_src("a")], [_ok()])
resp = await mw(_REQ)
assert resp.applied_effort is None
class TestRetryAndFailover:
async def test_transient_switches_source_then_succeeds(self):
+65 -11
View File
@@ -445,10 +445,14 @@ class TestResolveThinking:
class TestReconcileThinking:
"""声明 × 观测对账(设计 §5): 矛盾出文案,不表态出 None。
"""声明 × 观测对账(设计 §4.3): 矛盾出文案,不表态出 None。
文案本身是被断言对象——判定与日志分离正是为此: 告警内容可直接比对,不必
去解析日志格式。
判据自 2026-09-05 起是**档位**而非布尔(设计 §4.3): `Effort.NONE` 走"要求
关闭"一支,其余档走"要求开启"一支。档位化不是换个参数名——文案里写的是本次
真正发出去的那一档,而 transport 的节流键正按它分离,两者必须同源。
"""
_CAP = ThinkingCapability(
@@ -458,7 +462,7 @@ class TestReconcileThinking:
def test_off_but_observed_with_a_registered_capability_blames_the_table(self):
"""已登记却实测推理了 = 能力表漂移: 必须附 evidence 与更新指路。"""
msg = reconcile_thinking(
enable_thinking=False,
effort=Effort.NONE,
observation=ThinkingObservation.OBSERVED,
capability=self._CAP,
model="MiniMax-M3",
@@ -471,7 +475,7 @@ class TestReconcileThinking:
def test_off_but_observed_unregistered_never_claims_a_table_entry(self):
"""未登记模型没有"能力表声称"这回事——说它就是撒谎。"""
msg = reconcile_thinking(
enable_thinking=False,
effort=Effort.NONE,
observation=ThinkingObservation.OBSERVED,
capability=None,
model="MiniMax-M9",
@@ -483,13 +487,13 @@ class TestReconcileThinking:
def test_registered_and_unregistered_wordings_differ(self):
registered = reconcile_thinking(
enable_thinking=False,
effort=Effort.NONE,
observation=ThinkingObservation.OBSERVED,
capability=self._CAP,
model="MiniMax-M3",
)
unregistered = reconcile_thinking(
enable_thinking=False,
effort=Effort.NONE,
observation=ThinkingObservation.OBSERVED,
capability=None,
model="MiniMax-M3",
@@ -500,7 +504,7 @@ class TestReconcileThinking:
def test_on_but_absent_is_a_contradiction(self, capability):
"""上游明确上报未推理: 这是唯一的正面证伪,与能力表登记与否无关。"""
msg = reconcile_thinking(
enable_thinking=True,
effort=Effort.AUTO,
observation=ThinkingObservation.ABSENT,
capability=capability,
model="qwen3.7-plus",
@@ -512,7 +516,7 @@ class TestReconcileThinking:
def test_on_but_unknown_admits_it_cannot_confirm(self, capability):
"""issue #17 的诚实版本: 明说"我注入了,但我看不见结果""""
msg = reconcile_thinking(
enable_thinking=True,
effort=Effort.AUTO,
observation=ThinkingObservation.UNKNOWN,
capability=capability,
model="MiniMax-M3",
@@ -529,7 +533,7 @@ class TestReconcileThinking:
"""
assert (
reconcile_thinking(
enable_thinking=False,
effort=Effort.NONE,
observation=ThinkingObservation.ABSENT,
capability=self._CAP,
model="qwen3.7-plus",
@@ -541,7 +545,7 @@ class TestReconcileThinking:
"""UNKNOWN 没有证伪力: 拿它报警等于每次关闭调用都喊(M3 关闭档恒落此档)。"""
assert (
reconcile_thinking(
enable_thinking=False,
effort=Effort.NONE,
observation=ThinkingObservation.UNKNOWN,
capability=self._CAP,
model="MiniMax-M3",
@@ -557,7 +561,7 @@ class TestReconcileThinking:
"""调用方不表态,就无从谈"违背""""
assert (
reconcile_thinking(
enable_thinking=None,
effort=None,
observation=observation,
capability=self._CAP,
model="MiniMax-M3",
@@ -568,7 +572,7 @@ class TestReconcileThinking:
def test_on_and_observed_is_exactly_what_was_asked_for(self):
assert (
reconcile_thinking(
enable_thinking=True,
effort=Effort.AUTO,
observation=ThinkingObservation.OBSERVED,
capability=self._CAP,
model="MiniMax-M3",
@@ -576,6 +580,56 @@ class TestReconcileThinking:
is None
)
@pytest.mark.parametrize("effort", [Effort.LOW, Effort.HIGH, Effort.MAX])
def test_a_strength_tier_is_an_on_request_not_an_off_one(self, effort):
"""强度档必须走"要求开启"一支: 观测到推理正是它要的结果,不得报警。
判据写成真值性(`if not effort`)会在这里翻车——`Effort.NONE` 的取值是
非空串 `"none"`,恒为真;那种写法会把每一个强度档都送进"要求关闭"分支,
于是"想了"被当成矛盾,而"没想"反倒沉默,告警方向整个颠倒。
"""
assert (
reconcile_thinking(
effort=effort,
observation=ThinkingObservation.OBSERVED,
capability=ThinkingCapability((Effort.LOW, Effort.HIGH, Effort.MAX), "构造"),
model="glm-5.3",
)
is None
)
def test_the_wording_names_the_tier_that_was_asked_for(self):
"""文案要写出**本次这一档**: 节流键按档分离,文案不分档就看不出是哪一档。"""
low = reconcile_thinking(
effort=Effort.LOW,
observation=ThinkingObservation.ABSENT,
capability=None,
model="glm-5.3",
)
max_ = reconcile_thinking(
effort=Effort.MAX,
observation=ThinkingObservation.ABSENT,
capability=None,
model="glm-5.3",
)
assert low is not None and max_ is not None
assert "low" in low and "max" in max_
assert low != max_
def test_none_and_observed_is_the_issue_20_contradiction(self):
"""请求 `none` 却观测到推理 —— issue #20 要恢复的那条报警,判据是**档位相等**。
与上一条互为对照: 同样是 OBSERVED,`none` 必须喊、强度档必须沉默。把分支
条件写反(`is not Effort.NONE`)会让这两条同时红,单有一条则抓不住。
"""
msg = reconcile_thinking(
effort=Effort.NONE,
observation=ThinkingObservation.OBSERVED,
capability=None,
model="glm-5.3",
)
assert msg is not None and "none" in msg
class TestEffortVocabulary:
"""八档封闭词汇(设计 §3.1);`auto` 不可省——9 个纯开关型模型无强度档可填。"""