Files
PolyGateway/research-wiki/schemas/llm-calls.md
T
iomgaa 578a144231 docs: sync the field counts and module map to 1.3.1
The telemetry field count is taken from inspect.signature, not from
memory, because that is the one the release checklist keeps catching.
llm-calls.md said 22 and was two rounds stale; fixing the title alone
would have left the table contradicting it, so tenant_id and meta are
documented too.

The production template needed no new column — it derives them with
LIKE. What it gained is an assertion that it must keep deriving them
and must not inline a column name, which is the drift that could
actually happen.

The changelog leads with the three breaking items. A patch number
carries no warning by design, so the entry has to.
2026-08-26 01:03:25 -04:00

9.5 KiB

type, node_id, title, date
type node_id title date
schema schema:llm-calls 表结构: llm_calls(遥测 25 字段) 2026-07-20

表结构: llm_calls(遥测 25 字段)

列定义(冻结,M1 设计 §4.4 / ARCH §7.8)

类型 说明
call_id TEXT PRIMARY KEY 每次尝试独立 UUID;INSERT OR IGNORE 幂等
parent_call_id / session_id TEXT 调用链路(agent step → LLM call)
model / provider / source_name TEXT NOT NULL 溯源;model 由旧 Protocol 的 model_name 更名(VT 迁移 §8)
messages / response / thinking TEXT NOT NULL messages 落库前多模态 part 摘要(与缓存 key 共用 digest_messages)
prompt_tokens / completion_tokens INTEGER NOT NULL usage 帧;帧缺失记 0/0(不编造估值,由 usage_source 标注)
usage_source TEXT NOT NULL measured / estimated / unavailable(2026-07-30 起三态,见下)
latency_ms INTEGER NOT NULL 尝试耗时;缓存命中 0
ttft_ms / max_inter_token_ms REAL 流式活性测量
cache_hit INTEGER NOT NULL DEFAULT 0 命中标记
error TEXT 异常信息;取消记 "cancelled"
cost REAL M2 起 pricing 换算;usage_source='unavailable' 的真实调用行为 NULL(缓存命中行例外,仍为 0.0)
created_at TEXT NOT NULL DEFAULT (datetime('now')) 落库时刻
cached_prompt_tokens INTEGER 供应商 prompt cache 命中的输入 token(2026-07-31,issue #3);NULL = 该源未上报,0 = 上报了真实零命中,两者不可混同
model_reported TEXT API 响应体实际返回的 model;NULL = 未上报。与 model(配置别名)可能分叉
sampling TEXT 本次调用的采样参数 canonical JSON(2026-07-31,issue #4);NULL = 未传。见下方口径
reasoning_tokens INTEGER 推理消耗的输出 token(2026-08-02,issue #6);含在 completion_tokens 内,不影响成本总额,只补归因。NULL = 本次调用未上报
tenant_id TEXT NOT NULL DEFAULT '' 调用方租户(2026-08-17,issue #11);缺省落哨兵空串而非 NULL——PG 的 RLS USING 对返回 NULL 的行一律隐藏且不报错,NULL 的租户不是「未归属」而是对所有人永久不可见
meta TEXT / JSONB NOT NULL DEFAULT '' / '{}' 调用方自定义维度(同批,≤16 个 KV);SQLite 存 canonical JSON 串,PG 存 JSONB
thinking_observation TEXT 本次推理是否真的发生的三态裁定(2026-08-25,issue #16/#17);observed / absent / unknown。见下方口径

usage/成本口径(2026-07-30,est_tokens 解耦)

usage_source 含义 生产者 cost
measured usage 帧完整可信 正常路径;OCR 成功行(0 token 是事实) 按 token 换算
estimated 有实测数字但可信度降级 打捞路径(收到 usage 帧但流被截断) 按 token 换算
unavailable 用量信息不可得 usage 帧缺失、失败尝试、终态失败 NULL

SUM(cost) 天然跳过 NULL,故账单汇总不再被虚构的估值污染;账目缺口的度量口径固定为 WHERE usage_source = 'unavailable' AND cache_hit = falsecache_hit 限定不可省:缓存命中行未产生新调用,cost 是事实上的 0.0 而非未知,本无账目缺口,漏掉该条件会让缺口度量偏高。

供应商 prompt cache 口径(2026-07-31,issue #3)

新增两列排在 created_at 之后——旧表只能经 ALTER TABLE ADD COLUMN 追加到末尾,DDL 里若插在前面,新建库与升级库的物理列序会分叉(列序断言无合规修法)。两个后端在初始化期幂等补列:CREATE TABLE IF NOT EXISTS 不会给旧表加列,不补则每行写入被逐行 warning 丢弃、遥测静默全失。两侧都先探测缺列再 ALTER(ADD COLUMN IF NOT EXISTS 即使列已存在也先取 ACCESS EXCLUSIVE 锁,遥测是内联 await,锁共享审计表会拖垮业务调用),且补列失败只降级为逐行丢弃,绝不让 recorder 整体失能——两侧纪律必须对称。

cache_hitPolyGateway 自身响应缓存,与供应商 prompt cache 是两回事。缓存命中行的这两列是原样回放的历史值(与 model/prompt_tokens 同一口径),故命中率度量口径固定为:

SELECT SUM(cached_prompt_tokens)::float / NULLIF(SUM(prompt_tokens), 0)
FROM llm_calls WHERE cache_hit = false AND cached_prompt_tokens IS NOT NULL;

WHERE cache_hit = false 不可省,理由与上面 cost 缺口口径同源:回放行计入即重复计数。

采样参数口径(2026-07-31,issue #4)

reasoning_tokens 的 NULL 语义与 cached_prompt_tokens 不同: 后者的 NULL 是"该源不报这个数",前者只能读作"本次调用未上报"——中转在上游不返回 usage 时会用本地 tokenizer 补算并整体替换 usage 对象,把 completion_tokens_details 一并吃掉(实测同一请求 10 轮呈 6:4 双峰)。故当时的统计口径是 IS NULL OR = 0 才算"未推理",写 = 0 的条件永远不成立——实测三家供应商在未推理时都是整个 details 缺失,无人上报字面 0不可用 completion_tokens 反推是否推理: 两档的输出长度分布重叠(关闭档实测最高 46,开启档最低 13)。

该口径 2026-08-25 作废(issue #16/#17): 供应商可能整体停报 completion_tokens_details(MiniMax 这一路实测已停),此时 NULL 只意味着「没上报」而非「没推理」——同一次调用里库拿得到 185 字符推理正文。统计一律改按新列 thinking_observation 分组,见下方「推理观测口径」。

sampling 列 = 「调用方采样意图 ⊎ 生效源 extra_body」的 canonical JSON,空则 NULL。不含结构化输出注入的 response_format——列名是采样参数,schema 不是,且数 KB schema 逐行落库会让审计表无谓膨胀。补列纪律与 issue #3 两列逐字相同(排在末尾、先探测再 ALTER、失败只逐行降级)。

三个 emit 入口的取值必须各自定死,否则同一列在不同行含义不同:

入口 调用者 有生效源? 记什么
emit_attempt RetryMW(最内) merge(source.extra_body, request.sampling)
emit_cache_hit TelemetryMW(最外) request.sampling
emit_terminal_failure TelemetryMW request.sampling

后两行缺 extra_body 是客观事实而非口径瑕疵——它们没有"生效源"可言,与 model/source_name 在终态行置空是同一先例;缓存命中行亦无损:sampling 已进缓存 key,能命中即意味调用级参数与历史那次逐字相同。三者统一读 request.sampling 而非 request.overlay(后者在 RetryMW 处已被结构化注入污染、在 TelemetryMW 处未被污染,直接用必然三行分叉)。

OCR / embedding 路径的该列恒为 NULL:两条路径的 transport 不发 extra_body(embed payload 硬编码 {model, input}、MonkeyOCR 只发 multipart),故其源在构造期就被剥离——不剥离则该列会记录一个从未发出的参数,那是数据造假而非参数失效。

复现某批实验的解码条件:

SELECT DISTINCT sampling FROM llm_calls
WHERE session_id = $1 AND cache_hit = false AND error IS NULL;

推理观测口径(2026-08-25,issue #16/#17)

thinking_observation响应侧的裁定结果,不是请求侧的声明: 推理正文(thinking)非空即 observed(正文是事实本身,压倒 usage 明细这一转述);正文空而 reasoning_tokens > 0observed;reasoning_tokens == 0absent(上游明确上报未推理);两个信号双缺为 unknown

unknown 不得并进「未推理」。它是本列存在的全部理由: MiniMax 这一路上游 2026-08-25 起不再返回 completion_tokens_details,reasoning_tokens 因此恒 NULL,而同一次调用里库拿得到 185 字符推理正文——旧口径 reasoning_tokens IS NULL OR = 0 会把这类调用统计成「没推理」。该旧口径自本版起作废,统计一律按本列分组。M3 非流式档更极端: 推理已计费(completion 53 vs 关闭档 3)却不回传正文,该档只能是 unknown,任何把它读成「没推理」的报表都在撒谎。

按模型看各观测态占比,用于发现某模型从哪天起观测不到推理:

SELECT model,
       thinking_observation,
       count(*)                                                          AS calls,
       round(100.0 * count(*) / sum(count(*)) OVER (PARTITION BY model), 1) AS pct
FROM llm_calls
WHERE cache_hit = false AND error IS NULL
  AND created_at >= now() - interval '7 days'
GROUP BY model, thinking_observation
ORDER BY model, calls DESC;

三条限定各有理由: cache_hit = falsecost/cached_prompt_tokens 同源——缓存命中行原样回放历史观测值,计入即重复计数;error IS NULL 排除失败尝试与终态失败行,那些行的本列恒为 unknown(无响应可裁定,默认值本身不撒谎),混进来会把「观测不到」的占比整体抬高;时间窗是为了让变化可见——某模型的 unknown 占比从 0 跳到 100%,正是它停报推理信号的那一天。补列之前写入的历史行本列为 NULL,与 unknown 是两回事(前者是那时还没有这一列),跨版本对比须显式区分。

埋点位置(单一 helper 铁律)

  • middleware/telemetry.py::TelemetryEmitter 是全库唯一 record_llm_call 调用点;
  • TelemetryMW(最外层)记缓存命中与最终失败;RetryMW._emit 经同一 Emitter 逐次记尝试;
  • 写失败降级 warning 不冒泡;取消路径 finally 尽力记录。

评估基线

首版无历史基线,标"待首次运行后建立";验收断言: 单测覆盖成功/失败/缓存命中/取消四路径各产生恰一行;并发 50 协程写全落库。