mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-23 03:18:09 +08:00
145 lines
7.4 KiB
Markdown
145 lines
7.4 KiB
Markdown
# 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 必须先定义:
|
||
|
||
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 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。
|
||
|
||
## 后续门禁
|
||
|
||
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 和费用配额默认关闭。
|