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:
@@ -57,6 +57,7 @@ _EXPECTED_COLUMNS = [
|
||||
"tenant_id",
|
||||
"meta",
|
||||
"thinking_observation",
|
||||
"reasoning_effort",
|
||||
]
|
||||
|
||||
|
||||
@@ -127,6 +128,8 @@ async def _record_minimal(
|
||||
"meta": "{}",
|
||||
# 同样已由 emitter 归一化: 枚举取 .value 后才下沉,recorder 只见裸 str
|
||||
"thinking_observation": "unknown",
|
||||
# 同理: `Effort` 归一成裸 str,不表态则是 None(与 'low' 必须分得开)
|
||||
"reasoning_effort": None,
|
||||
}
|
||||
fields.update(overrides)
|
||||
await recorder.record_llm_call(**fields)
|
||||
@@ -225,11 +228,12 @@ class TestObservabilityColumns:
|
||||
await _record_minimal(
|
||||
recorder, call_id="samp", sampling='{"seed": 42, "temperature": 0}'
|
||||
)
|
||||
await _record_minimal(recorder, call_id="tier", reasoning_effort="low")
|
||||
rows = await _fetch(
|
||||
sandbox.dsn,
|
||||
"SELECT call_id, cached_prompt_tokens, model_reported, sampling FROM llm_calls "
|
||||
"WHERE call_id = ANY($1::text[])",
|
||||
["hit", "zero", "model", "samp"],
|
||||
"SELECT call_id, cached_prompt_tokens, model_reported, sampling, "
|
||||
"reasoning_effort FROM llm_calls WHERE call_id = ANY($1::text[])",
|
||||
["hit", "zero", "model", "samp", "tier"],
|
||||
)
|
||||
by_id = {r["call_id"]: r for r in rows}
|
||||
assert by_id["hit"]["cached_prompt_tokens"] == 64
|
||||
@@ -239,6 +243,10 @@ class TestObservabilityColumns:
|
||||
# issue #4: PG 侧也须验非空 sampling 能读回原值(不只是列存在)
|
||||
assert json.loads(by_id["samp"]["sampling"]) == {"seed": 42, "temperature": 0}
|
||||
assert by_id["hit"]["sampling"] is None
|
||||
# issue #20: PG 侧同样要验档位读得回来——emitter 落的是裸 str,
|
||||
# 若哪天回退成 `Effort` 实例,asyncpg 编码不保证接受,写入会整行降级
|
||||
assert by_id["tier"]["reasoning_effort"] == "low"
|
||||
assert by_id["hit"]["reasoning_effort"] is None # 不表态是 NULL
|
||||
finally:
|
||||
await recorder.aclose()
|
||||
|
||||
@@ -580,11 +588,13 @@ _PRE_TENANT_INSERT = (
|
||||
)
|
||||
|
||||
|
||||
# `_PRE_TENANT_DDL` 的物理列(23 个): 由 `_EXPECTED_COLUMNS` 去掉此后新增的三列
|
||||
# `_PRE_TENANT_DDL` 的物理列(23 个): 由 `_EXPECTED_COLUMNS` 去掉此后新增的四列
|
||||
# 派生而非另抄一份——两份常量必然漂移,而漂移的表现是"manual 档没补列"这条断言假绿。
|
||||
# 去掉后的顺序与 DDL 逐字一致(这三列在 DDL 里本就排在末尾)。
|
||||
# 去掉后的顺序与 DDL 逐字一致(这四列在 DDL 里本就排在末尾)。
|
||||
_PRE_TENANT_COLUMNS = [
|
||||
c for c in _EXPECTED_COLUMNS if c not in ("tenant_id", "meta", "thinking_observation")
|
||||
c
|
||||
for c in _EXPECTED_COLUMNS
|
||||
if c not in ("tenant_id", "meta", "thinking_observation", "reasoning_effort")
|
||||
]
|
||||
|
||||
# 回读要逐列比对的字段: 物理列去掉库从不显式写的 created_at,恰好 22 个
|
||||
@@ -693,7 +703,7 @@ class TestCallerDimensionsAcceptance:
|
||||
"WHERE table_schema = $1 AND table_name = 'llm_calls' ORDER BY ordinal_position",
|
||||
schema,
|
||||
)
|
||||
# 22 → 25 个 recorder 字段(加 created_at 共 26 个物理列),且新列追加在末尾
|
||||
# 22 → 26 个 recorder 字段(加 created_at 共 27 个物理列),且新列追加在末尾
|
||||
assert [r["column_name"] for r in cols] == _EXPECTED_COLUMNS
|
||||
rows = await _fetch(
|
||||
schema_dsn,
|
||||
@@ -869,7 +879,7 @@ class TestManualSchemaModeAcceptance:
|
||||
"""22 字段旧表 + manual: 列一个不加,行照常落库,缺的三维度静默不写。
|
||||
|
||||
与 `test_pre_tenant_table_gains_columns_and_old_rows_stay_auditable` 恰成对照:
|
||||
同一张表、同一份负载,只有 `auto_migrate` 不同,列数就必须是 23 与 26 之别。
|
||||
同一张表、同一份负载,只有 `auto_migrate` 不同,列数就必须是 23 与 27 之别。
|
||||
"""
|
||||
schema_dsn, schema = pre_tenant_schema
|
||||
recorder = _recorder(schema_dsn, auto_migrate=False)
|
||||
@@ -898,8 +908,11 @@ class TestManualSchemaModeAcceptance:
|
||||
assert [m for m in captured_warnings if "补列失败" in m] == []
|
||||
notices = [m for m in captured_warnings if "auto_migrate=False" in m]
|
||||
assert len(notices) == 1 # 准备期一次讲清,不逐行刷屏
|
||||
# 逐字钉住三个维度: 前缀断言会让将来漏进告警的新列照样绿
|
||||
assert "以下维度不会被记录: tenant_id, meta, thinking_observation。" in notices[0]
|
||||
# 逐字钉住四个维度: 前缀断言会让将来漏进告警的新列照样绿
|
||||
assert (
|
||||
"以下维度不会被记录: tenant_id, meta, thinking_observation, reasoning_effort。"
|
||||
in notices[0]
|
||||
)
|
||||
finally:
|
||||
await recorder.aclose()
|
||||
|
||||
@@ -925,8 +938,11 @@ class TestManualSchemaModeAcceptance:
|
||||
assert recorder.telemetry_status.degraded is False
|
||||
notices = [m for m in captured_warnings if "auto_migrate=False" in m]
|
||||
assert len(notices) == 1 # 准备期一次,第二行不再重复
|
||||
# 逐字钉住三个维度: 前缀断言会让将来漏进告警的新列照样绿
|
||||
assert "以下维度不会被记录: tenant_id, meta, thinking_observation。" in notices[0]
|
||||
# 逐字钉住四个维度: 前缀断言会让将来漏进告警的新列照样绿
|
||||
assert (
|
||||
"以下维度不会被记录: tenant_id, meta, thinking_observation, reasoning_effort。"
|
||||
in notices[0]
|
||||
)
|
||||
# 提示里的 SQL 必须可直接粘贴执行,而不是只报个列名
|
||||
assert (
|
||||
"ALTER TABLE llm_calls ADD COLUMN tenant_id TEXT NOT NULL DEFAULT '';" in notices[0]
|
||||
@@ -984,7 +1000,7 @@ class TestPublishedSchemaScript:
|
||||
|
||||
await _execute_script(fresh_dsn, script)
|
||||
actual = [r["column_name"] for r in await _fetch(fresh_dsn, _PHYSICAL_COLUMNS_SQL, schema)]
|
||||
# 物理列 = 25 个 INSERT 字段 + 库从不显式写的 created_at;对着库常量比,不另抄一份
|
||||
# 物理列 = 26 个 INSERT 字段 + 库从不显式写的 created_at;对着库常量比,不另抄一份
|
||||
assert set(actual) == set(COLUMNS) | {"created_at"}
|
||||
# 列序也不许漂: 新列必须排在 created_at 之后,否则新建库与 ALTER 升级的列序分叉
|
||||
assert actual == _EXPECTED_COLUMNS
|
||||
|
||||
Reference in New Issue
Block a user