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

145 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 分开返回 `knownCostMicros``unknownCostInvocations`。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/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_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 和费用配额默认关闭。