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:
@@ -370,7 +370,11 @@ class EmbeddingClient:
|
||||
# 登记在 transport 调用**之前**(同 RetryMW): 失败与取消的尝试也真的发出去了
|
||||
context.register_attempt()
|
||||
try:
|
||||
# 裸生成时间(1.3.7 H8): 计时只包 transport 调用本身(同 RetryMW 口径),
|
||||
# 与本链路 total_latency_ms 同一只注入钟;分批累加在成功分支登记
|
||||
gen_started = self._now()
|
||||
result = await self._transport.embed(texts=batch, source=source, call_id=call_id)
|
||||
gen_ms = int((self._now() - gen_started) * 1000)
|
||||
if self._expected_dim is not None and result.dim != self._expected_dim:
|
||||
raise ResultInvalidError(
|
||||
f"{source.name} 维度 {result.dim} 不符期望 {self._expected_dim}",
|
||||
@@ -384,6 +388,7 @@ class EmbeddingClient:
|
||||
actual = result.prompt_tokens
|
||||
# 真实 usage 恰为 0 也是已知事实, 后续取消不得改写成 est
|
||||
settlement_known = True
|
||||
context.record_generation(gen_ms, accumulate=True)
|
||||
await self._record_quietly(self._breaker.record_success(entry))
|
||||
await self._record_quietly(self._quota.mark_progress())
|
||||
latency_ms = int((self._now() - started) * 1000)
|
||||
|
||||
@@ -246,12 +246,20 @@ class RetryMW:
|
||||
await self._admission.on_no_runnable(gate_rejections, reasons, clock)
|
||||
continue
|
||||
async with clock.attempting() as attempt:
|
||||
outcome = await self._attempt(request, *picked, reasons, attempt_fails)
|
||||
# 每轮一个 sink: 裸生成时间由编排裁定归属(T2 单路 = 成功那轮;
|
||||
# T3 对冲 = 赢家那一路),`_attempt` 只负责把本次 transport 耗时投进来
|
||||
generation_sink: list[int] = []
|
||||
outcome = await self._attempt(
|
||||
request, *picked, reasons, attempt_fails, generation_sink=generation_sink
|
||||
)
|
||||
rate_limited = _is_rate_limited(outcome)
|
||||
if rate_limited:
|
||||
# 免了重试预算就得进 stall 账,否则这段耗时无人治理(见 StallClock)
|
||||
attempt.refund()
|
||||
if isinstance(outcome, LLMResponse):
|
||||
# 与 register_attempt 同款 None 守卫: 库内现场构造的请求跳过登记
|
||||
if request.call_context is not None:
|
||||
request.call_context.record_generation(generation_sink[0], accumulate=False)
|
||||
return outcome
|
||||
if not rate_limited:
|
||||
fails += 1
|
||||
@@ -275,6 +283,8 @@ class RetryMW:
|
||||
entry: GateDecision,
|
||||
reasons: dict[str, str],
|
||||
attempt_fails: dict[str, int],
|
||||
*,
|
||||
generation_sink: list[int],
|
||||
) -> LLMResponse | _Failed:
|
||||
call_id = str(uuid.uuid4())
|
||||
started = self._now()
|
||||
@@ -288,6 +298,10 @@ class RetryMW:
|
||||
if request.call_context is not None:
|
||||
request.call_context.register_attempt()
|
||||
try:
|
||||
# 裸生成时间(1.3.7 H8): 计时只包 transport 调用本身,起点紧贴调用前、
|
||||
# 终点为返回后首句(中间无 await);取消落进来时 transport 未返回,不计。
|
||||
# 与该链路 total_latency_ms 同一只注入钟,差值(波动开销)才有意义
|
||||
gen_started = self._now()
|
||||
result = await self._transport.complete(
|
||||
messages=request.messages,
|
||||
source=source,
|
||||
@@ -300,6 +314,7 @@ class RetryMW:
|
||||
# T3 对冲编排接线前恒为 None: 调用方不观测首 token(计划 §3.1)
|
||||
first_token_event=None,
|
||||
)
|
||||
generation_sink.append(int((self._now() - gen_started) * 1000))
|
||||
if result.usage_source == "unavailable":
|
||||
# 用量不可得时按入场预扣量结算(delta==0),否则押金会被整笔退回,
|
||||
# 对"从不返回 usage 帧"的源等于 TPM 闸失效(设计 §3.2 #9)
|
||||
|
||||
@@ -395,11 +395,16 @@ class OcrClient:
|
||||
# layout 的 POST + ZIP GET 在同一次 `_invoke` 内,故这里只登记 **1** 次
|
||||
context.register_attempt()
|
||||
try:
|
||||
# 裸生成时间(1.3.7 H8): 计时只包 transport 调用本身(同 RetryMW 口径);
|
||||
# layout 的 POST + ZIP GET 在同一次 `_invoke` 内,属同一段生成耗时
|
||||
gen_started = self._now()
|
||||
result = await self._invoke(kind, image, source, call_id)
|
||||
gen_ms = int((self._now() - gen_started) * 1000)
|
||||
await self._record_quietly(self._breaker.record_success(entry))
|
||||
await self._record_quietly(self._quota.mark_progress())
|
||||
self._feed_outcome(source.name, ok=True)
|
||||
latency_ms = int((self._now() - started) * 1000)
|
||||
context.record_generation(gen_ms, accumulate=False)
|
||||
await self._emit(
|
||||
kind,
|
||||
operation,
|
||||
|
||||
@@ -317,23 +317,42 @@ class CallStats:
|
||||
含缓存 IO、退避等待、准入等待、重问、分批与内联记账。
|
||||
"总耗时减最后一次尝试耗时"**不等于**纯等待(含其他本地工作)。"""
|
||||
|
||||
hedges: int = 0
|
||||
"""本次逻辑调用实际并发发出的对冲路数(触发但准入失败静默不计);1.3.6 及以前恒 0。"""
|
||||
generation_ms: int = 0
|
||||
"""裸生成时间: 赢家/成功那次 transport 调用的墙钟时长(口径见设计 §4.5 H8)。"""
|
||||
hedge_won: bool = False
|
||||
"""赢家是否为对冲路;无对冲恒 False。"""
|
||||
|
||||
|
||||
class _CallContext:
|
||||
"""私有可变逻辑调用上下文: 只持计数、单调时钟与终态去重位,不做 I/O。
|
||||
|
||||
**每调用一个实例**的单任务对象: chat 重试、结构化重问、embedding 分批
|
||||
都在同一任务内串行推进,故计数无需锁。**严禁提升为 client 实例属性**
|
||||
**每逻辑调用一个实例**,可多任务并发登记(对冲);全部方法无 await,
|
||||
事件循环内任务安全。**严禁提升为 client 实例属性**
|
||||
——那会让同一 client 的并发调用互相串掉计数与逻辑 ID(库铁律"纯 asyncio 中立"、
|
||||
VT `evolve_llm = llm` 教训的同一形态)。
|
||||
"""
|
||||
|
||||
__slots__ = ("_attempts", "_now", "_started", "_terminal_claimed", "logical_call_id")
|
||||
__slots__ = (
|
||||
"_attempts",
|
||||
"_generation_ms",
|
||||
"_hedge_won",
|
||||
"_hedges",
|
||||
"_now",
|
||||
"_started",
|
||||
"_terminal_claimed",
|
||||
"logical_call_id",
|
||||
)
|
||||
|
||||
def __init__(self, *, now: Callable[[], float]) -> None:
|
||||
self.logical_call_id = str(uuid.uuid4())
|
||||
self._now = now
|
||||
self._started = now()
|
||||
self._attempts = 0
|
||||
self._generation_ms = 0
|
||||
self._hedges = 0
|
||||
self._hedge_won = False
|
||||
self._terminal_claimed = False
|
||||
|
||||
def register_attempt(self) -> None:
|
||||
@@ -344,12 +363,24 @@ class _CallContext:
|
||||
"""
|
||||
self._attempts += 1
|
||||
|
||||
def record_generation(self, elapsed_ms: int, *, accumulate: bool) -> None:
|
||||
"""chat/OCR 覆盖(结构化重问最后一轮为准);embedding 分批累加。"""
|
||||
self._generation_ms = self._generation_ms + elapsed_ms if accumulate else elapsed_ms
|
||||
|
||||
def register_hedge(self, *, hedge_won: bool) -> None:
|
||||
"""对冲路实际发出即计数;赢家裁定后一次性登记。"""
|
||||
self._hedges += 1
|
||||
self._hedge_won = hedge_won
|
||||
|
||||
def snapshot(self) -> CallStats:
|
||||
"""同步冻结当前快照;**绝不 await**,可多次调用。"""
|
||||
return CallStats(
|
||||
logical_call_id=self.logical_call_id,
|
||||
attempts=self._attempts,
|
||||
total_latency_ms=int((self._now() - self._started) * 1000),
|
||||
hedges=self._hedges,
|
||||
generation_ms=self._generation_ms,
|
||||
hedge_won=self._hedge_won,
|
||||
)
|
||||
|
||||
def claim_terminal(self) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user