feat: expose bare generation time and hedge flags in CallStats
CallStats gains hedges/generation_ms/hedge_won (all defaulted, appended after total_latency_ms); _CallContext counts them via record_generation (overwrite for chat/OCR, accumulate for embedding batches) and register_hedge, and snapshot carries them out. All three _attempt implementations time only the transport call itself on the same injected clock as total_latency_ms; the chat sink is recorded by the orchestrator so hedge winner attribution stays with T3. Hedge counters stay 0/False until the T3 orchestration lands. Red-green evidence: tests/outputs/137/t2/ (10 new tests AttributeError red, then green; full unit+contracts 1621 passed).
This commit is contained in:
@@ -93,6 +93,39 @@ class FakeTransport:
|
||||
return action
|
||||
|
||||
|
||||
class _GenClockTransport:
|
||||
"""委托 FakeTransport 的薄包装: 每次调用返回前按脚本推进注入钟(1.3.7 批次 C)。
|
||||
|
||||
generation_ms 的口径是"只计 transport 调用本身",故推进必须发生在被包
|
||||
transport 内部;退避耗时由用例自带的 sleep 闭包推进,与本包装无关。
|
||||
"""
|
||||
|
||||
def __init__(self, script, advances, clock):
|
||||
self._inner = FakeTransport(script)
|
||||
self._advances = list(advances)
|
||||
self._clock = clock
|
||||
|
||||
@property
|
||||
def calls(self):
|
||||
return self._inner.calls
|
||||
|
||||
async def complete(
|
||||
self, *, messages, source, stream, overlay, call_id, reasoning_effort, first_token_event
|
||||
):
|
||||
advance = self._advances.pop(0)
|
||||
result = await self._inner.complete(
|
||||
messages=messages,
|
||||
source=source,
|
||||
stream=stream,
|
||||
overlay=overlay,
|
||||
call_id=call_id,
|
||||
reasoning_effort=reasoning_effort,
|
||||
first_token_event=first_token_event,
|
||||
)
|
||||
self._clock.advance(advance)
|
||||
return result
|
||||
|
||||
|
||||
class HangingGate(InMemoryGate):
|
||||
"""在指定记账写回处永久挂起的门控: 把"取消落在某个 await 上"变成确定性事件。
|
||||
|
||||
@@ -141,6 +174,8 @@ def _harness(
|
||||
selector=None,
|
||||
pacer=None,
|
||||
gate=None,
|
||||
transport=None,
|
||||
sleep=None,
|
||||
):
|
||||
clock = clock or FakeClock()
|
||||
limiter = InMemoryLimiter(
|
||||
@@ -151,8 +186,8 @@ def _harness(
|
||||
now=clock,
|
||||
)
|
||||
gate = gate if gate is not None else InMemoryGate(config=_BREAKER, now=clock)
|
||||
transport = FakeTransport(script)
|
||||
sleep = FakeSleep()
|
||||
transport = transport if transport is not None else FakeTransport(script)
|
||||
sleep = sleep if sleep is not None else FakeSleep()
|
||||
mw = RetryMW(
|
||||
scope="llm",
|
||||
sources=sources,
|
||||
@@ -916,6 +951,54 @@ class TestRateLimitPushback:
|
||||
assert len(transport.calls) == 3
|
||||
|
||||
|
||||
class TestGenerationMs:
|
||||
"""裸生成时间(1.3.7 H8): 只计 transport 调用本身,不含退避/准入/遥测收尾。"""
|
||||
|
||||
def _ctx(self, clock):
|
||||
from polygateway.types import _CallContext
|
||||
|
||||
return _CallContext(now=clock)
|
||||
|
||||
async def test_generation_ms_excludes_backoff_and_admission(self):
|
||||
"""[Transient, ok] 脚本: 退避推进 5s、成功次 transport 推进 0.25s。
|
||||
|
||||
generation_ms 恒等于成功次 transport 的 250ms;若口径混入了退避,
|
||||
它会涨到 5250ms 量级——与 total_latency_ms 的下界断言互为对偶。
|
||||
推进量取二进制可精确表示值(0.25/5.0): int 截断下 0.2 之类会因浮点
|
||||
误差落到 199,断言随之抖动(同 test_types 既有用例只用 1.5/2.0 的惯例)。
|
||||
"""
|
||||
clock = FakeClock()
|
||||
|
||||
async def advancing_sleep(seconds):
|
||||
clock.advance(seconds)
|
||||
|
||||
transport = _GenClockTransport(
|
||||
[TransientError("boom", source_name="a"), _ok()], [0.0, 0.25], clock
|
||||
)
|
||||
mw, *_ = _harness(
|
||||
[_src("a")],
|
||||
[],
|
||||
clock=clock,
|
||||
transport=transport,
|
||||
sleep=advancing_sleep,
|
||||
rng=lambda: 2.0, # backoff = 2.0 * (0.5 + 2.0) = 5.0s
|
||||
)
|
||||
ctx = self._ctx(clock)
|
||||
resp = await mw(dataclasses.replace(_REQ, call_context=ctx))
|
||||
assert resp.content == "ok" and len(transport.calls) == 2
|
||||
stats = ctx.snapshot()
|
||||
assert stats.generation_ms == 250
|
||||
assert stats.total_latency_ms >= 5250
|
||||
|
||||
async def test_generation_ms_zero_hedge_flags_without_hedging(self):
|
||||
"""无对冲时 hedges/hedge_won 恒 0/False(对冲登记是 T3 的事)。"""
|
||||
mw, *_ = _harness([_src("a")], [_ok()])
|
||||
ctx = self._ctx(FakeClock())
|
||||
await mw(dataclasses.replace(_REQ, call_context=ctx))
|
||||
stats = ctx.snapshot()
|
||||
assert stats.hedges == 0 and stats.hedge_won is False
|
||||
|
||||
|
||||
class TestLogicalAttemptCounting:
|
||||
"""尝试登记在 transport 调用**之前**(1.3.5 设计 §4)。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user