feat: track logical call statistics across governed calls
This commit is contained in:
+107
-2
@@ -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` = 未知。"""
|
||||
|
||||
Reference in New Issue
Block a user