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
+137
View File
@@ -1362,3 +1362,140 @@ async def test_synthetic_runtime_protocol_and_legacy_call_signatures():
assert client._transport._clients == {}
finally:
await client.aclose()
class _StatsClock:
"""确定性单调钟;测试主动推进以断言"哪些区段计入了总耗时""""
def __init__(self, start=1000.0):
self.t = start
def __call__(self):
return self.t
def advance(self, seconds):
self.t += seconds
class _TickingCache:
"""假缓存后端: 每次 IO 推进注入钟。
不推进时钟的替身会让"缓存 IO 计入总耗时"的断言退化成恒等于 0 的空转绿
(计划 §T1 替身构造要求)。
"""
def __init__(self, clock, tick=0.25):
self._clock = clock
self._tick = tick
self._data = {}
async def get(self, key):
self._clock.advance(self._tick)
return self._data.get(key)
async def set(self, key, value, ttl_s):
self._clock.advance(self._tick)
self._data[key] = value
class _TickingRecorder:
"""假 recorder: 写入时推进注入钟,用于断言内联遥测收尾计入总耗时。"""
def __init__(self, clock, tick=0.5):
self._clock = clock
self._tick = tick
self.rows = []
async def record_llm_call(self, **fields):
self._clock.advance(self._tick)
self.rows.append(fields)
class TestLogicalCallStats:
"""一次公开 chat 调用的统计(1.3.5 设计 §3)。"""
_MSG = [{"role": "user", "content": "hi"}]
async def test_success_reports_one_attempt(self):
async with _client() as client:
resp = await client.chat(self._MSG)
assert resp.call_stats is not None
assert resp.call_stats.attempts == 1
assert resp.call_stats.logical_call_id
async def test_concurrent_calls_do_not_share_counters_or_ids(self):
"""同一 client 并发两路必须各自计数与各自 ID(库铁律「纯 asyncio 中立」)。
上下文若被提升成 client 实例属性,这条就会红——那正是 VT
`evolve_llm = llm` 教训的同一形态。
"""
async with _client() as client:
a, b = await asyncio.gather(client.chat(self._MSG), client.chat(self._MSG))
assert a.call_stats.logical_call_id != b.call_stats.logical_call_id
assert a.call_stats.attempts == b.call_stats.attempts == 1
async def test_cache_hit_is_zero_attempts_with_a_fresh_logical_id(self):
"""命中不产生网关调用 → 0 尝试;且是**新**逻辑调用,不回放历史统计。"""
clock = _StatsClock()
cache = _TickingCache(clock)
async with _client(
cache=cache, cache_namespace="proj", cache_ttl_s=600, now=clock
) as client:
first = await client.chat(self._MSG)
second = await client.chat(self._MSG)
assert first.cache_hit is False and first.call_stats.attempts == 1
assert second.cache_hit is True
assert second.call_stats.attempts == 0
assert second.call_stats.logical_call_id != first.call_stats.logical_call_id
async def test_cache_io_counts_into_total_latency(self):
"""缓存读写是本次调用真实花掉的时间,必须进总耗时(设计 §3)。"""
clock = _StatsClock()
cache = _TickingCache(clock, tick=0.25)
async with _client(
cache=cache, cache_namespace="proj", cache_ttl_s=600, now=clock
) as client:
hit = (await client.chat(self._MSG), await client.chat(self._MSG))[1]
# 命中路径只有一次 get(0.25s),无网关调用
assert hit.call_stats.attempts == 0
assert hit.call_stats.total_latency_ms == 250
async def test_inline_telemetry_teardown_counts_into_total_latency(self):
"""成功响应的快照含返回前已完成的内联遥测耗时(设计 §6)。"""
clock = _StatsClock()
recorder = _TickingRecorder(clock, tick=0.5)
async with _client(telemetry=recorder, now=clock) as client:
resp = await client.chat(self._MSG)
assert recorder.rows # 确实写了行,否则本断言空转
assert resp.call_stats.total_latency_ms == 500
async def test_milliseconds_not_seconds(self):
"""毫秒/秒不混用: 1.5s 必须是 1500 而不是 1 或 1.5。"""
clock = _StatsClock()
recorder = _TickingRecorder(clock, tick=1.5)
async with _client(telemetry=recorder, now=clock) as client:
resp = await client.chat(self._MSG)
assert resp.call_stats.total_latency_ms == 1500
async def test_stats_work_without_any_telemetry(self):
"""统计生效与否**不由 telemetry 是否启用决定**(设计 §3.5)。"""
async with _client(telemetry=None) as client:
resp = await client.chat(self._MSG)
assert resp.call_stats is not None and resp.call_stats.attempts == 1
async def test_failure_exception_carries_no_stats_attribute(self):
"""本版**不向异常对象附加统计**(设计 §3.1): 第三方可能复用同一异常实例。"""
def reject(request):
return httpx.Response(400, json={"error": {"message": "bad"}})
async with _client(handler=reject) as client:
with pytest.raises(RequestRejectedError) as exc:
await client.chat(self._MSG)
assert hasattr(exc.value, "call_stats") is False
async def test_input_validation_stays_outside_the_stats_boundary(self):
"""校验异常保持原行为,发生在统计边界之外(设计 §3)。"""
async with _client() as client:
with pytest.raises(ValueError, match="meta"):
await client.chat(self._MSG, meta={"BAD-KEY": 1})