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
+107 -2
View File
@@ -8,11 +8,12 @@ import dataclasses
import json
import math
import re
from collections.abc import Mapping
import uuid
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
from enum import StrEnum
from types import MappingProxyType
from typing import Any
from typing import Any, Literal
from loguru import logger
@@ -274,6 +275,91 @@ class ThinkingObservation(StrEnum):
UNKNOWN = "unknown"
CallOperation = Literal["chat", "embed", "recognize_text", "parse_layout"]
"""遥测 `operation` 列的值域: **公开方法**四值,由调用点给定。
与 `PolyGatewayError.operation`(HTTP 子操作,如 `download_result`)是**两个语义**,
不做自动转换;链路上任何位置都不得读 `exc.operation` 来填本列(设计 §5 I1/I2)。"""
CALL_OPERATIONS: tuple[CallOperation, ...] = ("chat", "embed", "recognize_text", "parse_layout")
EventKind = Literal["attempt", "cache_hit", "terminal_failure"]
"""一行遥测描述的事件形态;旧行 NULL,不回填。
终态行与 attempt 行**不是重复事实**(前者描述逻辑终态,后者描述单次尝试),
故禁止按 `error IS NOT NULL` 跨两类直接计失败调用次数(设计 §6/§8)。"""
EVENT_KINDS: tuple[EventKind, ...] = ("attempt", "cache_hit", "terminal_failure")
@dataclass(frozen=True)
class CallStats:
"""一次**公开调用**(而非单次尝试)的统计快照(设计 §3)。
四种响应各平铺三字段会立刻漂移,故收敛成单一对象并由包根导出。
第三方合成响应的 `None` 表示**未知**,不得伪造 0。
"""
logical_call_id: str
"""每次公开调用一个 UUID;重试、结构化重问、embedding 分批共享同一个。
不占用既有 `parent_call_id`(后者是调用方的业务关联,语义不变)。"""
attempts: int
"""准入后实际调用 transport 端口的次数;含免预算 429 与端口本地拒绝。
**不是 HTTP 请求条数**: OCR layout 的 POST + ZIP GET 在同一次 transport
调用内,计 1 次。缓存命中与空输入是合法的零尝试。"""
total_latency_ms: int
"""从输入校验通过到返回/异常传播前的单调时钟快照。
含缓存 IO、退避等待、准入等待、重问、分批与内联记账。
"总耗时减最后一次尝试耗时"**不等于**纯等待(含其他本地工作)。"""
class _CallContext:
"""私有可变逻辑调用上下文: 只持计数、单调时钟与终态去重位,不做 I/O。
**每调用一个实例**的单任务对象: chat 重试、结构化重问、embedding 分批
都在同一任务内串行推进,故计数无需锁。**严禁提升为 client 实例属性**
——那会让同一 client 的并发调用互相串掉计数与逻辑 ID(库铁律"纯 asyncio 中立"
VT `evolve_llm = llm` 教训的同一形态)。
"""
__slots__ = ("_attempts", "_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._terminal_claimed = False
def register_attempt(self) -> None:
"""transport 调用**前**登记一次尝试(含免预算 429 与端口本地拒绝)。
登记点在调用前而非成功后: 否则失败与取消的尝试会从计数里消失,
而那正是诊断时最需要看见的那几次。
"""
self._attempts += 1
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),
)
def claim_terminal(self) -> bool:
"""首次 `True`、其后 `False`: 保证每逻辑调用至多写一条终态行。"""
if self._terminal_claimed:
return False
self._terminal_claimed = True
return True
@dataclass(frozen=True)
class LLMResponse:
"""一次治理调用的统一响应(与三项目超集兼容,ARCH §5.1)。"""
@@ -336,6 +422,9 @@ class LLMResponse:
`None` 不是"没推理": 库不表态时也不推定模型自己的默认档——"没看见"不许说成
"发生了"(同 `thinking_observation` 的 `UNKNOWN` 一脉)。"""
call_stats: CallStats | None = None
"""本次**逻辑调用**的统计快照(1.3.5);`None` = 未知,不得读成 0。"""
@dataclass(frozen=True)
class ChatRequest:
@@ -382,6 +471,16 @@ class ChatRequest:
而档位要经能力表校验、要进缓存 key、要落遥测——混进直通层等于放弃这三样,
正是 issue #20 里下游手写 `extra_body` 绕过全部治理的那条路。"""
# —— 库内部逻辑调用上下文(1.3.5;追加在末尾,不扰动既有字段的位置构造)——
call_context: _CallContext | None = field(default=None, compare=False, repr=False)
"""库内部逻辑调用上下文;`None` = 库内现场构造的请求,遥测 `logical_call_id` 落 NULL。
`compare=False, repr=False` 不是洁癖: 进 `compare` 会让两个内容相同的请求因
"不是同一次调用"而不相等,进 `repr` 则把库内部件泄进调用方的日志。
洋葱各层经 `dataclasses.replace` 派生请求时保留**同一引用**(不是拷贝),
重试/重问/分批才能共享同一个逻辑 ID 与计数。"""
@dataclass(frozen=True)
class Usage:
@@ -722,6 +821,8 @@ class OcrTextResult:
latency_ms: int
call_id: str
raw: dict[str, Any]
call_stats: CallStats | None = None
"""本次逻辑调用的统计快照(1.3.5);`None` = 未知。"""
@dataclass(frozen=True)
@@ -739,6 +840,8 @@ class OcrLayoutResult:
latency_ms: int
call_id: str
raw: dict[str, Any]
call_stats: CallStats | None = None
"""本次逻辑调用的统计快照(1.3.5);`None` = 未知。"""
@dataclass(frozen=True)
@@ -783,3 +886,5 @@ class EmbeddingResponse:
call_id: str
source_name: str
cost: float | None = None
call_stats: CallStats | None = None
"""本次逻辑调用(含全部分批)的统计快照(1.3.5);`None` = 未知。"""