Files
qinglong/docs/adr/ADR-0171-atomic-project-model-quota-reservation-and-settlement.md

149 lines
7.4 KiB
Markdown
Raw Permalink 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-0171:原子 Project Model Quota Reservation 与 Settlement
- 状态:Accepted
- 日期:2026-07-27
- 关联:RFC D-12、D-13、D-157、D-158、D-160、D-161ADR-0167、ADR-0168、ADR-0170
## 背景
ADR-0170 的不可变 UsageLedger 能回答“已经发生了多少用量”,但单独在 provider
调用前读取 summary 不能构成安全配额:
- 两个 Cluster 副本可能同时读到剩余额度并共同超卖;
- provider 返回前不知道精确 token/费用,不能只在完成后扣减;
- crash、caller abort 或 provider outcome unknown 时释放额度会允许重试绕过预算;
- 只按 token 汇总会遗漏 invocation 数量和 nullable cost 的不确定性;
- Router 设备不能获得后台 rollup、timer 或常驻 quota service
- 配额准入若与 `StepRun ready → running` 分事务,会产生有调用权但无预留,或有预留
但无执行权的裂缝。
配额没有独立部署、依赖或权限边界,因此不新增 workspace package。它继续属于已有
`@qinglong/ai` 可选能力。
## 决策
### 1. 固定窗口策略与最坏情况预留
`ModelInvocationPolicy` 可选携带 Project quota
- 独立 quota policy revision
- 固定、epoch 对齐的 `1 minute | 1 hour | 1 day` 窗口;
- `maxInvocations``maxTokens`
- nullable `maxCostMicros`
准入按单次 model policy 的 `maxTotalTokens` 和 nullable `maxCostMicros` 预留,而不是按
请求方估计值预留。Project 开启费用配额但单次调用没有费用上限时,配置失败关闭;
不能把 unknown cost 当成零。
窗口边界由数据库时钟决定。Gateway 的 `occurredAtMs`、HTTP 时间或 Worker 时间不能
选择计费窗口。
### 2. Reservation 与 Settlement 都是不可变 receipt
新增:
- `qinglong/model-invocation-quota-admission@v1`
- `qinglong/model-invocation-quota-reservation@v1`
- `qinglong/model-invocation-quota-settlement@v1`
Reservation 一对一绑定 invocation、Project、model/quota policy revision、窗口、窗口
预算、单次预留与独立 digest。Settlement 一对一绑定 Reservation digest 和 Completion
digest。
Completion 有精确 usage 时,Settlement 使用实际 token;仅在已经建立费用预留时使用
实际费用并释放未用预留。未启用费用配额时 quota settlement 的费用保持 null,真实
billing cost 仍由 UsageLedger 保存。usage 为 null 时保留全部 token 预留;费用有配额
但实际费用未知时保留全部费用预留。实际值超过预留属于 durable contract 破坏并失败
关闭。
Reservation/Settlement 不是第二执行状态机。StepRun 仍是执行状态权威,二者只解释
窗口内的有效占用。
### 3. 准入与完成必须分别原子提交
SQLite `BEGIN IMMEDIATE` 在同一事务中完成:
1. 数据库时钟窗口计算;
2. 当前窗口有效占用聚合;
3. quota fence
4. StepRun/Run/Event/Mutation
5. ModelInvocationStart
6. QuotaReservation。
PostgreSQL 在相同事务流程前,对
`JSON.stringify([projectId, windowStartMs, windowMs])` 的稳定 hash 获取 transaction
advisory lock。同一 Project/窗口的不同实例因此串行化;hash collision 最多扩大串行
范围,不能导致少算。没有可变 quota bucket,也不需要给 runtime `UPDATE` 权限。
Completion、UsageLedger 和 QuotaSettlement 与 StepRun terminal mutation 同事务写入。
exact replay 必须同时复验 Start/Completion、Ledger、Reservation、Settlement;任一
预期 receipt 缺失、意外存在或 digest 漂移都 conflict fail closed。Recovery 发现已有
Reservation 时必须通过 quota-aware completionunknown outcome 保留预留。
### 4. 9003 仍是可选 append-only feature migration
不改写 9001/9002。新增:
- SQLite `9003-ai-model-usage-quota`
- PostgreSQL `pg-9003-ai-model-usage-quota`
- Reservation/Settlement 两张双方言表及 Project/window 索引。
PostgreSQL `ql3_runtime` 对新表只有 `SELECT, INSERT`,没有 `UPDATE, DELETE`。PUBLIC
和其它业务角色不获得权限。AI 禁用时不执行 9001/9002/9003,不建表、不加载 provider
或 credential,也没有 timer。
代码继续留在 `@qinglong/ai/usage-quota`、现有 Gateway/coordinator/repository/Profile
subpath;没有新增 package 或第三方依赖。这一边界符合“按部署/依赖/权限拆包,而不是按
文件数拆包”的 QL3 package 规则。
### 5. Profile 只公开内部只读能力
Active AI Profile 增加 bounded ledger page、bounded summary 和当前 quota window usage
三种内部读取。它们复用 storage authority 和 drain 计数,不创建后台服务。
本 ADR 不开放 HTTP/MCP/UI route。产品入口仍必须另行完成 Principal、Project Policy、
rate limit、低敏审计和错误屏蔽,不能把内部 capability 等同于已授权 API。
## 被否决方案
1. provider 调用前先读 UsageLedger:多副本存在 TOCTOU,会超卖。
2. provider 完成后才扣额度:预算不能阻止本次外部费用。
3. unknown outcome 立即释放:重试可循环绕过配额。
4. 可变 Project quota bucket:需要 runtime UPDATE、CAS/recovery 与第二套可变权威。
5. 依赖应用时钟选择窗口:调用方可把用量写入其它窗口。
6. 为 quota 新拆 package/service:没有独立部署收益,会增加路由器制品和依赖碎片。
7. 默认迁移 quota 表:未启用 AI 的设备承担无效 schema、备份和写放大。
## 当前验证
- `@qinglong/ai`60 pass、1 条 PostgreSQL 条件 skip;真实 PostgreSQL 18 另 1 pass
- SQLite 覆盖 reservation+Start 原子准入、超额整体 rollback、known usage 释放、
unknown recovery 保留 token/cost 与 Profile 只读组合;
- PostgreSQL 18 双 runtime 连接并发争抢 `maxInvocations=1` 时恰好一笔成功,另一笔稳定
返回 `MODEL_PROJECT_QUOTA_EXCEEDED`
- PostgreSQL runtime 对 Reservation/Settlement 只有 SELECT/INSERT
- 9001/9002 checksum 保持不变;SQLite 9003 checksum 为
`fa734aac1a3f5affaf69f4fbe53a2c6ca628255ecdcde14c08b87b49d8162012`
PostgreSQL 9003 checksum 为
`13ea1a904eb799bcae1b474d76b164a70748bdcca8e1e6ded9952921a291a855`
- PostgreSQL 18.4 arm64 physical HA 在 `remote_apply`、timeline 1→2 promotion、
fence-before-promote、`pg_rewind` 和双 fresh control 下,精确复验六张 `ql3_ai`
表、9001/9002/9003 history/checksum 与六表 runtime append-only ACL 前后完全一致,
`optionalAiFeatureSchemaSurvivesPromotion=true` 且总 `passed=true`
- 22-package build/test 全部退出 0dependency audit 覆盖 22 importer、AI 15 个源码
文件且 `findings=[]`
- 默认 edge 为 3,902,728 bytes/478 files/40 modulesedge-ai 为
4,288,019 bytes/510 files/41 modulesstandalone-ai 为
4,288,091 bytes/510 files/41 modules,均低于 5 MiB/640 filesdisabled AI
只加载 1 个模块且三个 Profile loader 均为 0
- 没有新增 workspace package 或第三方依赖。
## 后续门禁
1. 增加 price catalog、币种、计价 revision 和 provider/derived cost 来源契约;
2. 增加不可变 rollup、coverage receipt、retention 与磁盘耗尽证据;
3. 完成受认证、Project Policy-fenced 的 usage/quota API、CLI/UI 和 rate limit
4. 增加 invocation reservation/settlement COMMIT-response-loss 与 promotion 期间的
数据行级 fault,而不只验证 schema promotion
5. 在产品权限与运维门完成前保持 AI route 和 Project quota 默认关闭。