Files
qinglong/docs/adr/ADR-0170-durable-model-usage-ledger-and-bounded-project-accounting.md
T

7.4 KiB
Raw Blame History

ADR-0170Durable Model Usage Ledger 与有界 Project Accounting

  • 状态:Accepted
  • 日期:2026-07-26
  • 关联:RFC D-12、D-13、D-156、D-157、D-158、D-160ADR-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 分开返回 knownCostMicrosunknownCostInvocations。unknown cost 不能 解释为零;未来启用费用配额时,只要目标窗口不完整或存在 unknown cost,费用准入就 必须 fail closed,除非受审策略明确只约束 token 而不约束费用。

5. Retention 不能直接删除原始事实

首版保持 append-only,不实现 raw row deletion。未来 retention 必须先定义:

  1. 与原始 ledger digest 范围绑定的不可变 Project/time rollup
  2. 可证明完整覆盖的 retention receipt/tombstone
  3. SQLite 断电恢复、PostgreSQL promotion/backup restore 和审计导出的共同语义。

在这些事实存在前,不能为了节省空间直接删除 ledger、Completion 或 Start。路由设备 可通过不启用 AI、缩短产品允许的历史窗口和显式存储容量门控制成本,但不能静默丢账。

被否决方案

  1. 直接扫描 Completion JSON:没有稳定索引,路由设备和 Cluster 都会获得不可控查询。
  2. 只为 succeeded 建账:Provider 已产生费用但 Gateway 后续失败时会漏账。
  3. 无 cost 记为 0:会把未知事实伪装成免费调用并绕过费用配额。
  4. 异步写 ledger:崩溃和 COMMIT response loss 会产生 Completion/ledger 裂缝。
  5. 把账本拆成新 package:没有独立部署、依赖或权限边界,只会继续细化 package。
  6. 把账本加入默认 storage migration:禁用 AI 的路由设备也会承担 schema、备份和写放大。
  7. 直接删除旧行:会破坏 Completion 对账、审计与未知结果人工裁决的证据链。

当前验证

  • @qinglong/ai50 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_ai invocation 表、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 modulesedge-ai 为 4,212,508 bytes/508 files/41 modulesstandalone-ai 为 4,212,580 bytes/508 files/41 modules,均低于 5 MiB/640 files
  • 22-package 全量 build/test 退出 0。

后续门禁

  1. 基于不可变 ledger/rollup 的原子 Project token/cost quota admission
  2. price catalog、计价 revision、币种和 provider-reported/derived cost 来源契约;
  3. retention rollup、coverage receipt、导出、备份恢复与实机磁盘耗尽证据;
  4. 产品 read-only usage API/CLI/UI、认证、Policy、rate limit 与低敏审计;
  5. AI invocation 数据行级 partition/COMMIT-response-loss HA fault,而不只 schema promotion
  6. 在上述门禁完成前保持产品 AI route 和费用配额默认关闭。