feat: record which tier a call actually ran at
Twenty-five columns and not one of them answered "which tier was this?", so the question the whole issue exists to settle - does a higher tier buy anything - had no way to group its data. The three emit entry points deliberately disagree, the way sampling already does. A successful attempt records what the transport actually sent: with EFFORT_FALLBACK=nearest a request for medium goes out as low, and recomputing here would file the row under a tier that never left the process. A failed attempt has no response to read, so it falls back to the requested tier - which is exactly right for the tier errors that are rejected before any HTTP happens, because the rejected tier is the signal. Cache hits and terminal failures have no chosen source at all, so a source-level tier is not a thing they could report. emit_attempt now demands to be told whether the path reasons at all. Embedding and OCR share the emitter but never send reasoning parameters; without the flag a source that mistakenly carries ENABLE_THINKING would hang a tier on a call that could not possibly have run at one. The value lands as a plain str. StrEnum is a str subclass and asyncpg promises nothing about encoding subclasses, and a telemetry write that fails is only a warning - Postgres would just quietly lose the column. NULL means nobody declared a tier, which is not the same statement as 'none', and the two must never be folded together.
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
多处各存一份必然漂移,而漂移的表现是"下游照打印的 SQL 建完表,库仍报缺列"。
|
||||
|
||||
**`COLUMNS` 是 INSERT 字段序,不是物理列序**: 数据库自填的 `created_at` 不在其中(它带
|
||||
`DEFAULT now()` / `datetime('now')`,库从不显式写它)。物理表列 = 25 个 INSERT 字段 +
|
||||
`created_at` = 26;列数断言一律按物理列数写,两套口径混用是最易错处。
|
||||
`DEFAULT now()` / `datetime('now')`,库从不显式写它)。物理表列 = 26 个 INSERT 字段 +
|
||||
`created_at` = 27;列数断言一律按物理列数写,两套口径混用是最易错处。
|
||||
|
||||
本模块只依赖标准库: `telemetry/` 与 `backends/`、`transports/`、`structured/` 同层且
|
||||
互不依赖(import-linter 契约执法)。
|
||||
@@ -51,7 +51,8 @@ CREATE TABLE IF NOT EXISTS llm_calls (
|
||||
reasoning_tokens INTEGER,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
meta TEXT NOT NULL DEFAULT '{}',
|
||||
thinking_observation TEXT
|
||||
thinking_observation TEXT,
|
||||
reasoning_effort TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
@@ -82,7 +83,8 @@ CREATE TABLE IF NOT EXISTS llm_calls (
|
||||
reasoning_tokens INTEGER,
|
||||
tenant_id TEXT NOT NULL DEFAULT '',
|
||||
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
thinking_observation TEXT
|
||||
thinking_observation TEXT,
|
||||
reasoning_effort TEXT
|
||||
);
|
||||
"""
|
||||
|
||||
@@ -100,6 +102,9 @@ SQLITE_BACKFILL = (
|
||||
# 可空: 补列之前的行没有裁定结果,NULL 如实表达"这行根本没记过这件事",
|
||||
# 与哨兵串 'unknown'(库确实裁过但判不出来)是两回事,不得混同
|
||||
("thinking_observation", "TEXT"),
|
||||
# 同样可空,但这里 NULL 表达的是"调用方没表态"(issue #20): 它与 'none'
|
||||
# (明确要求不推理)是两回事,折叠成任一档都等于替上游声称了它没说过的事
|
||||
("reasoning_effort", "TEXT"),
|
||||
)
|
||||
|
||||
# PG 补列的列定义。语句由此派生成两份文本(见下),使"库内执行的那份"与"打印给
|
||||
@@ -114,6 +119,7 @@ _PG_BACKFILL_DECLS = (
|
||||
("meta", "JSONB NOT NULL DEFAULT '{}'::jsonb"),
|
||||
# 可空,理由同 SQLITE_BACKFILL 同名项
|
||||
("thinking_observation", "TEXT"),
|
||||
("reasoning_effort", "TEXT"),
|
||||
)
|
||||
|
||||
# 新列排在 created_at 之后: 与旧表 ALTER 追加的位置一致(见 SQLITE_BACKFILL 同款注释)。
|
||||
@@ -151,6 +157,7 @@ COLUMNS = (
|
||||
"tenant_id",
|
||||
"meta",
|
||||
"thinking_observation",
|
||||
"reasoning_effort",
|
||||
)
|
||||
|
||||
_COLUMN_SET = frozenset(COLUMNS)
|
||||
|
||||
@@ -143,7 +143,7 @@ class SQLiteRecorder:
|
||||
logger.warning("SQLite 遥测补列失败(写入将逐行降级): {}", exc)
|
||||
|
||||
async def record_llm_call(self, **fields: object) -> None:
|
||||
"""写一行遥测;字段集合即 25 字段冻结签名(ports.TelemetryRecorder)。
|
||||
"""写一行遥测;字段集合即 26 字段冻结签名(ports.TelemetryRecorder)。
|
||||
|
||||
取值按 `self._columns`(manual 档可能已被裁剪),与 `self._insert` 的
|
||||
占位符同序——两者必须一起改,分开改就是把值写进错位的列。
|
||||
|
||||
Reference in New Issue
Block a user