feat: record sampling parameters in telemetry (port 20 to 21 fields)

Each of the three emitter entry points has a pinned meaning: only the
attempt path has an effective source, so only it merges extra_body.
This commit is contained in:
2026-07-31 21:30:45 -04:00
parent b6e4cc3f3b
commit 4516761dbe
6 changed files with 141 additions and 16 deletions
+12
View File
@@ -18,6 +18,7 @@ from loguru import logger
from polygateway.errors import GatewayUnavailableError, GovernanceBackendError
from polygateway.middleware.cache import digest_messages
from polygateway.types import canonical_sampling_json, merge_sampling
if TYPE_CHECKING:
from collections.abc import Callable
@@ -63,6 +64,10 @@ class TelemetryEmitter:
error=error,
cached_prompt_tokens=response.cached_prompt_tokens if response else None,
model_reported=response.model_reported if response else None,
# 唯一有"生效源"的入口,故是唯一能并上 extra_body 的(设计决策 D)
sampling=canonical_sampling_json(
merge_sampling(source.extra_body, request.sampling)
),
)
async def emit_cache_hit(self, *, request: ChatRequest, response: LLMResponse) -> None:
@@ -87,6 +92,9 @@ class TelemetryEmitter:
# 统计供应商缓存命中率必须带 WHERE cache_hit = false,否则重复计数。
cached_prompt_tokens=response.cached_prompt_tokens,
model_reported=response.model_reported,
# 由最外层 TelemetryMW 调用,手上没有 source。缓存命中行无损:
# sampling 已进缓存 key,能命中即意味调用级参数与历史那次逐字相同
sampling=canonical_sampling_json(request.sampling),
)
async def emit_terminal_failure(
@@ -111,6 +119,8 @@ class TelemetryEmitter:
error=error,
cached_prompt_tokens=None,
model_reported=None,
# 无具体源,与 model/provider/source_name 置空同一先例(设计决策 D)
sampling=canonical_sampling_json(request.sampling),
)
async def _record(
@@ -133,6 +143,7 @@ class TelemetryEmitter:
error: str | None,
cached_prompt_tokens: int | None,
model_reported: str | None,
sampling: str | None,
) -> None:
try:
# 成本换算(M2 §6): 成功行按单价换算;缓存命中 0.0(未产生新调用);
@@ -172,6 +183,7 @@ class TelemetryEmitter:
cost=cost,
cached_prompt_tokens=cached_prompt_tokens,
model_reported=model_reported,
sampling=sampling,
)
except asyncio.CancelledError:
raise
+1
View File
@@ -274,4 +274,5 @@ class TelemetryRecorder(Protocol):
cost: float | None,
cached_prompt_tokens: int | None,
model_reported: str | None,
sampling: str | None,
) -> None: ...
+4 -1
View File
@@ -41,7 +41,8 @@ CREATE TABLE IF NOT EXISTS llm_calls (
cost DOUBLE PRECISION,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
cached_prompt_tokens INTEGER,
model_reported TEXT
model_reported TEXT,
sampling TEXT
);
"""
@@ -49,6 +50,7 @@ CREATE TABLE IF NOT EXISTS llm_calls (
_BACKFILL = (
("cached_prompt_tokens", "ALTER TABLE llm_calls ADD COLUMN cached_prompt_tokens INTEGER"),
("model_reported", "ALTER TABLE llm_calls ADD COLUMN model_reported TEXT"),
("sampling", "ALTER TABLE llm_calls ADD COLUMN sampling TEXT"),
)
# 探测现有列;尊重 search_path(to_regclass 按当前 search_path 解析)
@@ -78,6 +80,7 @@ _COLUMNS = (
"cost",
"cached_prompt_tokens",
"model_reported",
"sampling",
)
_INSERT = (
+9 -3
View File
@@ -36,13 +36,18 @@ CREATE TABLE IF NOT EXISTS llm_calls (
cost REAL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
cached_prompt_tokens INTEGER,
model_reported TEXT
model_reported TEXT,
sampling TEXT
);
"""
# 新列必须排在 created_at 之后: 旧表只能经 ALTER 追加到末尾,新建库若把它们
# 插在前面,两条路径的物理列序会分叉(列序断言测试无合规修法)。
_BACKFILL_COLUMNS = (("cached_prompt_tokens", "INTEGER"), ("model_reported", "TEXT"))
_BACKFILL_COLUMNS = (
("cached_prompt_tokens", "INTEGER"),
("model_reported", "TEXT"),
("sampling", "TEXT"),
)
_COLUMNS = (
"call_id",
@@ -65,6 +70,7 @@ _COLUMNS = (
"cost",
"cached_prompt_tokens",
"model_reported",
"sampling",
)
_INSERT = (
@@ -120,7 +126,7 @@ class SQLiteRecorder:
logger.warning("SQLite 遥测补列失败(写入将逐行降级): {}", exc)
async def record_llm_call(self, **fields: object) -> None:
"""写一行遥测;字段集合即 20 字段冻结签名(ports.TelemetryRecorder)。"""
"""写一行遥测;字段集合即 21 字段冻结签名(ports.TelemetryRecorder)。"""
if self._conn is None:
return
row = tuple(fields[col] for col in _COLUMNS)