feat: track logical call statistics across governed calls

This commit is contained in:
2026-09-09 10:04:39 -04:00
parent 300ced5dbd
commit 87c261bf73
13 changed files with 650 additions and 10 deletions
+72
View File
@@ -818,3 +818,75 @@ class TestExplicitCacheMigration:
finally:
for transport in transports:
await transport.aclose()
class TestCallStatsNotPoisoned:
"""缓存不得回放历史统计(1.3.5 设计 §3)。
统计描述**本次**调用;把上次那条存进去再放出来,等于对调用方谎称这次
重试了 N 次、耗了 M 毫秒。
"""
async def test_serialized_payload_carries_no_call_stats_key(self):
from polygateway.types import CallStats
backend = InMemoryCache()
mw = _mw(backend)
stats = CallStats(logical_call_id="lc-1", attempts=3, total_latency_ms=900)
terminal = _Terminal(_resp(call_stats=stats))
await mw(ChatRequest(messages=_MSGS), terminal)
key = build_cache_key("m", _MSGS, "proj", None)
stored = json.loads(await backend.get(key))
assert "call_stats" not in stored # asdict 会把它摊成 dict,必须显式剔除
async def test_historic_dict_never_impersonates_call_stats(self):
"""旧条目里的 `call_stats` dict 会被 `_RESPONSE_FIELDS` 放行,必须显式覆盖。
不覆盖就会有一个 dict 冒充 `CallStats` 从公共 API 漏给调用方,
`resp.call_stats.attempts` 当场 `AttributeError`。
"""
backend = InMemoryCache()
mw = _mw(backend)
key = build_cache_key("m", _MSGS, "proj", None)
poisoned = {
"content": "legacy",
"thinking": "",
"model": "m",
"provider": "p",
"prompt_tokens": 1,
"completion_tokens": 2,
"latency_ms": 30,
"ttft_ms": 5.0,
"max_inter_token_ms": 2.0,
"cache_hit": False,
"call_id": "orig",
"source_name": "s1",
"usage_source": "measured",
"call_stats": {
"logical_call_id": "stale-lc",
"attempts": 7,
"total_latency_ms": 9999,
},
}
await backend.set(key, json.dumps(poisoned), ttl_s=100)
terminal = _Terminal(_resp())
hit = await mw(ChatRequest(messages=_MSGS), terminal)
assert hit.content == "legacy" and terminal.calls == 0 # 真的走了缓存
assert hit.call_stats is None # dict 不得冒充 CallStats
async def test_cache_key_is_unchanged_by_the_new_field(self):
"""新增内部字段不得扰动 key 公式,否则存量缓存全量冷启动(黄金值)。"""
from polygateway.types import _CallContext
class _Clock:
def __call__(self):
return 1000.0
ctx = _CallContext(now=_Clock())
bare = build_cache_key("m", _MSGS, "proj", None)
assert bare == build_cache_key("m", _MSGS, "proj", None)
# 带上下文的请求与不带的请求必须落在同一个 key 上
with_ctx = ChatRequest(messages=_MSGS, call_context=ctx)
without = ChatRequest(messages=_MSGS)
assert with_ctx.cache_namespace == without.cache_namespace
assert digest_messages(with_ctx.messages) == digest_messages(without.messages)