feat: record stream end mode in telemetry
llm_calls gains a nullable stream_end TEXT column (auto backfill, manual-mode trimming as in 1.3.8). TelemetryRecorder.record_llm_call takes a new required keyword stream_end; the emitter reads it from the response on attempt rows and writes NULL for failed, cache-hit and terminal rows. Red: 18 tests in tests/unit/test_telemetry.py and test_ports.py failed against the pre-change source (missing column, 38-field signature, stream_end absent from emitted rows).
This commit is contained in:
@@ -221,6 +221,9 @@ class _AttemptUsage:
|
||||
# 两字段留在此处只为让"尝试视图"形状完整,默认 None 即"无沉默观测"
|
||||
stall_ms: int | None = None
|
||||
stall_count: int | None = None
|
||||
# 1.3.10(#31): 与 stall 相反,结束方式**从 response 取**——LLMResponse 本次有
|
||||
# 该字段,免去约 50 处 emit_attempt 调用点加参;失败尝试 None = 无流可言
|
||||
stream_end: str | None = None
|
||||
|
||||
@classmethod
|
||||
def of(cls, response: LLMResponse | None) -> _AttemptUsage:
|
||||
@@ -239,6 +242,7 @@ class _AttemptUsage:
|
||||
model_reported=response.model_reported,
|
||||
reasoning_tokens=response.reasoning_tokens,
|
||||
thinking_observation=response.thinking_observation,
|
||||
stream_end=response.stream_end,
|
||||
)
|
||||
|
||||
|
||||
@@ -370,7 +374,7 @@ def _assert_recorder_shape(recorder: TelemetryRecorder) -> None:
|
||||
|
||||
|
||||
class TelemetryEmitter:
|
||||
"""从请求与结果组装 38 字段并写入 recorder;一切写失败降级 warning。"""
|
||||
"""从请求与结果组装 39 字段并写入 recorder;一切写失败降级 warning。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -464,6 +468,7 @@ class TelemetryEmitter:
|
||||
total_latency_ms=None,
|
||||
stall_ms=stall_ms,
|
||||
stall_count=stall_count,
|
||||
stream_end=usage.stream_end,
|
||||
)
|
||||
|
||||
async def emit_cache_hit(
|
||||
@@ -514,6 +519,8 @@ class TelemetryEmitter:
|
||||
# 缓存命中行未产生 transport 观测: 无沉默可言,显式 None
|
||||
stall_ms=None,
|
||||
stall_count=None,
|
||||
# 命中/终态行无流可言(缓存命中已在 CacheMW 置 None,此处显式不信任)
|
||||
stream_end=None,
|
||||
)
|
||||
|
||||
async def emit_terminal_failure(
|
||||
@@ -569,6 +576,8 @@ class TelemetryEmitter:
|
||||
# 终态行无具体源与 transport 观测: 显式 None
|
||||
stall_ms=None,
|
||||
stall_count=None,
|
||||
# 命中/终态行无流可言(缓存命中已在 CacheMW 置 None,此处显式不信任)
|
||||
stream_end=None,
|
||||
)
|
||||
|
||||
async def _record(
|
||||
@@ -615,6 +624,8 @@ class TelemetryEmitter:
|
||||
# 本方法只搬运(同 reasoning_effort 先例)——
|
||||
stall_ms: int | None,
|
||||
stall_count: int | None,
|
||||
# —— 1.3.10: 结束方式(#31);入口已定型,本方法只搬运 ——
|
||||
stream_end: str | None,
|
||||
) -> None:
|
||||
try:
|
||||
# 成本换算(M2 §6): 成功行按单价换算;缓存命中 0.0(未产生新调用);
|
||||
@@ -685,6 +696,7 @@ class TelemetryEmitter:
|
||||
total_latency_ms=total_latency_ms,
|
||||
stall_ms=stall_ms,
|
||||
stall_count=stall_count,
|
||||
stream_end=stream_end,
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
||||
@@ -290,7 +290,7 @@ class TelemetryStatusProvider(Protocol):
|
||||
|
||||
@runtime_checkable
|
||||
class TelemetryRecorder(Protocol):
|
||||
"""遥测后端;38 字段冻结(M1 设计 §4.4 + issue #3/#4/#11/#16/#20 + 1.3.5/1.3.8),唯一调用点是 TelemetryEmitter。
|
||||
"""遥测后端;39 字段冻结(M1 设计 §4.4 + issue #3/#4/#11/#16/#20 + 1.3.5/1.3.8/1.3.10),唯一调用点是 TelemetryEmitter。
|
||||
|
||||
新增参数不设默认值: 库外无第三方实现者(三项目迁移时删除了各自的同名
|
||||
Protocol),完整签名的成本为零,而少写一列会被 emitter 的降级吞成 warning。
|
||||
@@ -357,4 +357,5 @@ class TelemetryRecorder(Protocol):
|
||||
total_latency_ms: int | None,
|
||||
stall_ms: int | None,
|
||||
stall_count: int | None,
|
||||
stream_end: str | None,
|
||||
) -> None: ...
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
多处各存一份必然漂移,而漂移的表现是"下游照打印的 SQL 建完表,库仍报缺列"。
|
||||
|
||||
**`COLUMNS` 是 INSERT 字段序,不是物理列序**: 数据库自填的 `created_at` 不在其中(它带
|
||||
`DEFAULT now()` / `datetime('now')`,库从不显式写它)。物理表列 = 38 个 INSERT 字段 +
|
||||
`created_at` = 39;列数断言一律按物理列数写,两套口径混用是最易错处。
|
||||
`DEFAULT now()` / `datetime('now')`,库从不显式写它)。物理表列 = 39 个 INSERT 字段 +
|
||||
`created_at` = 40;列数断言一律按物理列数写,两套口径混用是最易错处。
|
||||
|
||||
本模块只依赖标准库: `telemetry/` 与 `backends/`、`transports/`、`structured/` 同层且
|
||||
互不依赖(import-linter 契约执法)。
|
||||
@@ -64,7 +64,8 @@ CREATE TABLE IF NOT EXISTS llm_calls (
|
||||
attempts INTEGER,
|
||||
total_latency_ms INTEGER,
|
||||
stall_ms INTEGER,
|
||||
stall_count INTEGER
|
||||
stall_count INTEGER,
|
||||
stream_end TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
@@ -108,7 +109,8 @@ CREATE TABLE IF NOT EXISTS llm_calls (
|
||||
attempts INTEGER,
|
||||
total_latency_ms INTEGER,
|
||||
stall_ms INTEGER,
|
||||
stall_count INTEGER
|
||||
stall_count INTEGER,
|
||||
stream_end TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
@@ -145,6 +147,8 @@ SQLITE_BACKFILL = (
|
||||
# 没记过这件事",与"观测过且零沉默"(0)是两回事
|
||||
("stall_ms", "INTEGER"),
|
||||
("stall_count", "INTEGER"),
|
||||
# 1.3.10 结束方式(#31): 可空无默认,旧行 NULL = 补列之前没记过;值域见 StreamEnd
|
||||
("stream_end", "TEXT"),
|
||||
)
|
||||
|
||||
# PG 补列的列定义。语句由此派生成两份文本(见下),使"库内执行的那份"与"打印给
|
||||
@@ -174,6 +178,8 @@ _PG_BACKFILL_DECLS = (
|
||||
# 1.3.8 两列,列序与 SQLITE_BACKFILL 逐项对齐(理由同上)
|
||||
("stall_ms", "INTEGER"),
|
||||
("stall_count", "INTEGER"),
|
||||
# 1.3.10 结束方式(#31): 可空无默认,旧行 NULL = 补列之前没记过;值域见 StreamEnd
|
||||
("stream_end", "TEXT"),
|
||||
)
|
||||
|
||||
# 新列排在 created_at 之后: 与旧表 ALTER 追加的位置一致(见 SQLITE_BACKFILL 同款注释)。
|
||||
@@ -226,6 +232,8 @@ COLUMNS = (
|
||||
# —— 1.3.8 沉默归因(issue #27)——
|
||||
"stall_ms",
|
||||
"stall_count",
|
||||
# —— 1.3.10 结束方式(#31)——
|
||||
"stream_end",
|
||||
)
|
||||
|
||||
_COLUMN_SET = frozenset(COLUMNS)
|
||||
|
||||
@@ -143,7 +143,7 @@ class SQLiteRecorder:
|
||||
logger.warning("SQLite 遥测补列失败(写入将逐行降级): {}", exc)
|
||||
|
||||
async def record_llm_call(self, **fields: object) -> None:
|
||||
"""写一行遥测;字段集合即 38 字段冻结签名(ports.TelemetryRecorder)。
|
||||
"""写一行遥测;字段集合即 39 字段冻结签名(ports.TelemetryRecorder)。
|
||||
|
||||
取值按 `self._columns`(manual 档可能已被裁剪),与 `self._insert` 的
|
||||
占位符同序——两者必须一起改,分开改就是把值写进错位的列。
|
||||
|
||||
Reference in New Issue
Block a user