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:
@@ -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):
|
||||
"""配了档位那一侧的指纹字面量也要钉死: 它变了就是该源整段缓存冷启动。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user