7.4 KiB
ADR-0170:Durable Model Usage Ledger 与有界 Project Accounting
- 状态:Accepted
- 日期:2026-07-26
- 关联:RFC D-12、D-13、D-156、D-157、D-158、D-160;ADR-0167、ADR-0168、ADR-0169
背景
ADR-0168 已把每次模型调用固定为既有 StepRun.kind=model 的 Start/Completion
receipt。Completion JSON 虽然包含 token 与可选费用,但它不适合直接承担 Project
账本:
- SQLite/PostgreSQL 只能对 JSON 做高成本扫描,无法使用 Project/时间索引;
- Provider 可能在 Gateway 最终失败、超时或结果未知时仍产生 token/费用;
- “没有费用值”和“费用为零”是两种不同事实;
- Completion 已提交但账本缺失会让 replay、配额和报表产生不一致;
- 低配路由设备不能因为 AI 未启用而创建表,也不能让一次汇总扫描无限行;
- Cluster 多副本需要与 Completion 相同的事务、复制和 promotion 语义。
该能力没有新的部署边界,不应再拆一个只有少量文件的 workspace package,也不能进入 默认 edge/standalone 的主 migration。
决策
1. Usage Ledger 是 Completion 的不可变派生事实
新增 qinglong/model-invocation-usage-ledger@v1,留在既有
@qinglong/ai/usage-ledger subpath。每行以 invocationId 唯一绑定:
- Project、Run、StepRun、Trace;
- Provider、model、policy revision;
- Completion digest、outcome、settled time;
- input/output bytes;
- input/output/total tokens;
- nullable
costMicros; - domain-separated ledger digest。
Ledger 不保存 Prompt、输出正文、SecretRef、credential、authorization header 或错误
正文。totalTokens 必须精确等于 input 与 output token 之和。
只要 Completion 携带 usage,就必须创建 ledger,不以 outcome=succeeded 为前提;
Provider 已计费但 Gateway 因预算、协议或后处理失败时仍保留事实。Completion 的 usage
为 null 时不创建 ledger,也绝不能合成 token=0、cost=0 的伪记录。
2. Completion 与 Ledger 必须同事务提交
SQLite/PostgreSQL repository 在规范化 Start 和 Completion 后确定性派生 ledger:
- 新 Completion 与非空 ledger 在同一事务插入;
- 任一数据库约束、StepRun/Run fence 或 ledger 写入失败时整体回滚;
- exact Completion replay 必须同时看到 exact ledger;
- 预期有 ledger 但缺失/损坏,或预期无 ledger 却存在行,均以 conflict fail closed;
- 不允许异步事件消费者、后台补写、timer 或 best-effort reconciliation。
这不会建立新的 invocation 状态机。Ledger 只是原 Completion 的一对一不可变投影, StepRun 仍是唯一执行状态权威。
3. Feature migration 继续独立且 append-only
不改写已验收的 9001 migration 或 checksum。新增:
- SQLite
9002-ai-model-usage-ledger; - PostgreSQL
pg-9002-ai-model-usage-ledger; - SQLite
ModelInvocationUsageLedger; - PostgreSQL
ql3_ai.model_invocation_usage_ledger。
表以复合外键绑定 (invocationId, completionDigest),mirrored columns 与 exact JSON
由数据库 CHECK 绑定,并建立 (projectId, settledAtMs, invocationId) keyset 索引。
PostgreSQL ql3_runtime 只取得 SELECT, INSERT,没有 UPDATE, DELETE;PUBLIC 和
其它业务角色不获得权限。
AI 未启用时不执行 9001/9002、不创建 ql3_ai 或 SQLite feature 表、不增加默认
edge/standalone packlist。没有新增 workspace package或第三方依赖。
4. 查询必须同时限制窗口、页和扫描行数
账本公开三种读取:
- invocationId 精确查找;
- Project +
[from, to)+(settledAtMs, invocationId)keyset 分页; - Project + 时间窗口聚合。
时间窗口最长 366 天,明细页最多 128 条。聚合不能只依赖时间窗口;双方言先按同一
索引最多读取 100001 行,超过 100,000 行返回稳定
MODEL_INVOCATION_USAGE_SUMMARY_LIMIT_EXCEEDED,不返回不完整总额。大规模 Cluster
需要后续不可变 rollup,而不是放宽单次扫描。
Summary 分开返回 knownCostMicros 与 unknownCostInvocations。unknown cost 不能
解释为零;未来启用费用配额时,只要目标窗口不完整或存在 unknown cost,费用准入就
必须 fail closed,除非受审策略明确只约束 token 而不约束费用。
5. Retention 不能直接删除原始事实
首版保持 append-only,不实现 raw row deletion。未来 retention 必须先定义:
- 与原始 ledger digest 范围绑定的不可变 Project/time rollup;
- 可证明完整覆盖的 retention receipt/tombstone;
- SQLite 断电恢复、PostgreSQL promotion/backup restore 和审计导出的共同语义。
在这些事实存在前,不能为了节省空间直接删除 ledger、Completion 或 Start。路由设备 可通过不启用 AI、缩短产品允许的历史窗口和显式存储容量门控制成本,但不能静默丢账。
被否决方案
- 直接扫描 Completion JSON:没有稳定索引,路由设备和 Cluster 都会获得不可控查询。
- 只为 succeeded 建账:Provider 已产生费用但 Gateway 后续失败时会漏账。
- 无 cost 记为 0:会把未知事实伪装成免费调用并绕过费用配额。
- 异步写 ledger:崩溃和 COMMIT response loss 会产生 Completion/ledger 裂缝。
- 把账本拆成新 package:没有独立部署、依赖或权限边界,只会继续细化 package。
- 把账本加入默认 storage migration:禁用 AI 的路由设备也会承担 schema、备份和写放大。
- 直接删除旧行:会破坏 Completion 对账、审计与未知结果人工裁决的证据链。
当前验证
@qinglong/ai:50 pass、1 条 PostgreSQL 条件 skip;真实 PostgreSQL 另 1 pass;- SQLite 覆盖 Completion+ledger 原子提交、整体 rollback、exact replay、缺行 fail closed、Project 查询/summary 与无 usage 不建行;
- PostgreSQL 18.4 migration/runtime 双角色真库覆盖 9002 DDL、append-only ACL、 原子提交、查询和 recovery;
- 9001 checksum 保持
69f72286fba2988ba372f006eb894a7f8b89f4b1acd9da68dc1cdafc3ca96ea7, 9002 checksum 为95ad6f46163b0bbc2583dddf492f91f767a00554683186f244d3f6a22a2ad00c; - PostgreSQL 18.4 arm64 HA 门在 timeline 1→2 前后精确比对四张
ql3_aiinvocation 表、9001/9002 history/checksum 与四表 runtime append-only ACL; physical streaming、remote_apply、fence-before-promote、pg_rewind、双 fresh control 和总passed=true; - dependency audit 覆盖 22 importer、AI 14 个源码文件,
findings=[]; - disabled AI 只加载 1 个模块,storage/provider loader 零调用,RSS 增量 409,600 bytes;
- 默认 edge 保持 3,902,728 bytes/478 files/40 modules;edge-ai 为 4,212,508 bytes/508 files/41 modules,standalone-ai 为 4,212,580 bytes/508 files/41 modules,均低于 5 MiB/640 files;
- 22-package 全量 build/test 退出 0。
后续门禁
- 基于不可变 ledger/rollup 的原子 Project token/cost quota admission;
- price catalog、计价 revision、币种和 provider-reported/derived cost 来源契约;
- retention rollup、coverage receipt、导出、备份恢复与实机磁盘耗尽证据;
- 产品 read-only usage API/CLI/UI、认证、Policy、rate limit 与低敏审计;
- AI invocation 数据行级 partition/COMMIT-response-loss HA fault,而不只 schema promotion;
- 在上述门禁完成前保持产品 AI route 和费用配额默认关闭。