Files
qinglong/docs/adr/ADR-0172-immutable-model-price-quote-and-canonical-cost-settlement.md

147 lines
7.6 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-0172:不可变 Model Price Quote 与 Canonical Cost Settlement
- 状态:Accepted
- 日期:2026-07-27
- 关联:RFC D-12、D-13、D-157、D-160、D-161、D-162ADR-0167、ADR-0168、ADR-0170、ADR-0171
## 背景
ADR-0170/0171 已经建立不可变 usage ledger 与 provider I/O 前的 Project quota
reservation,但费用仍有三个未封闭的权威裂缝:
- provider 返回的 `usage.costMicros` 既不可验证,也可能缺失、使用不同币种或被 adapter
错误换算;
- model policy 的人工 `maxCostMicros` 只能作为上限,不能证明调用使用了哪一版价格;
- 价格变化后,如果只保存 model 和最终费用,就无法按历史 revision 重算、审计或精确重放;
- Cluster 多副本可能在一次调用期间观察到不同价格,本机重启也可能丢失当时的计价输入;
- 为价格目录、报价和结算各拆 package/service 会放大路由设备的 importer、制品与运维成本,
却没有独立部署或权限收益。
## 决策
### 1. Policy 必须选择一个精确、不可变的价格 revision
`ModelInvocationPolicy.priceRevision` 与 provider/model 共同选择
`qinglong/model-price-catalog-entry@v1`。条目固定:
- `USD`
- 每百万 input/output token 的整数 micro-USD rate
- publisher time 与内容摘要;
- provider、model、price revision 的精确身份。
启用单次费用上限或 Project cost quota 时,`priceRevision` 和精确 catalog entry 都是必需
条件;缺失必须在 provider I/O 前失败关闭。无费用约束且 `priceRevision=null` 的调用继续
允许 usage cost 为 nullable,不强迫所有部署启用计价。
当前 catalog 是有界、注入式 `ModelPriceCatalogResolver`。它冻结运行时消费契约,但不
宣称已经完成数据库 catalog、签名发布、激活、撤销或管理 UI;这些运营能力必须另行建立
权限和发布协议。
### 2. Provider I/O 前生成不可变最坏情况 Quote
Gateway 在 durable admission 前生成
`qinglong/model-invocation-price-quote@v1`。Quote 绑定 invocation、Project、model policy
revision、provider/model、price revision、币种、input/output rate、catalog digest、
token 上限与独立 digest。
费用预留按 `maxTotalTokens``maxOutputTokens` 中合法且最昂贵的 input/output 分配计算,
不能用调用方估计值。每个 token 维度分别向上取整到整数 micro-USD,并用 `BigInt` 做中间
运算;越界失败关闭。Quote 的 `reservedCostMicros` 同时是单次费用 fence 和 Project
cost quota reservation 的权威输入,替代人工复制的 reservation 数值。
### 3. Provider 报价不是账单权威
provider 返回 usage 后,Gateway 只信任经过既有上限校验的 input/output/total token。
`usage.costMicros` 无论是 null、错误还是任意值,都由 Quote 重新计算的 canonical cost
替换。返回给调用方、Completion、UsageLedger、QuotaSettlement 使用同一个 canonical
usage。
`qinglong/model-invocation-price-settlement@v1` 绑定 Quote digest、Completion digest、
精确 token、canonical cost、settled time 与独立 digest。usage 缺失时不得创建零费用
Settlementunknown outcome 保留 Quote 和 quota reservation,但不伪造价格结算。
### 4. Quote 与 Settlement 进入既有原子事务
准入事务原子提交:
1. `StepRun ready → running`、Run/Event/Mutation
2. ModelInvocationStart
3. PriceQuote
4. 可选 QuotaReservation。
完成事务原子提交:
1. terminal StepRun/Run/Event/Mutation
2. Completion
3. 可选 UsageLedger
4. usage 存在时的 PriceSettlement
5. 可选 QuotaSettlement。
exact replay 必须复验全部预期事实;Quote/Settlement 缺失、意外存在、digest 或字段漂移均
失败关闭。持久化 JSON 的一致性比较按字段进行,不依赖 SQLite TEXT 或 PostgreSQL JSONB
的对象键顺序。
### 5. 9004 保持可选、append-only 和双方言一致
不改写 9001/9002/9003。新增:
- SQLite `9004-ai-model-pricing-snapshots`
- PostgreSQL `pg-9004-ai-model-pricing-snapshots`
- PriceQuote/PriceSettlement 两张双方言表。
PostgreSQL `ql3_runtime` 只有 `SELECT, INSERT`,没有 `UPDATE, DELETE`;其它业务角色与
PUBLIC 不获得权限。Quote 与 Settlement 不是第二状态机,StepRun 仍是执行权威。
### 6. 不新增 package、依赖或后台服务
价格 contract、resolver 和计算留在已有 `@qinglong/ai/pricing`Gateway、coordinator、
repository 与 Profile 复用现有 AI package。没有新增 workspace package或第三方依赖。
AI 禁用时不解析 catalog、不执行 90019004、不加载 provider/credential/storage,也不
创建 timer。edge/standalone 只在显式启用 AI 时把该模块纳入制品;Cluster 可注入未来的
数据库/配置发布 resolver,但不能把价格管理 authority 放进常驻 runtime。
本 ADR 不开放 HTTP/MCP/UI,也不把静态 resolver 解释为产品级价格目录管理。
## 被否决方案
1. 直接信任 provider `usage.costMicros`:来源、币种、revision 与舍入不可验证。
2. 只保存最终费用:价格变更后不能历史重算或证明计价输入。
3. 调用完成后读取最新价格:一次调用可能跨 revision,Cluster 副本会产生不同结果。
4. 用浮点美元计价:序列化与舍入会产生跨运行时漂移。
5. 把 unknown usage 结算为零:允许预算绕过并伪造账单事实。
6. 为 pricing 新建 workspace package/service:没有独立部署、依赖或权限边界,只会增加
路由器制品、lockfile importer 和 CI 成本。
7. 立即把 catalog 管理写入 runtime 表:会把运营写 authority 与调用 runtime 合并。
## 当前验证
- `@qinglong/ai`70 项测试,69 pass、1 条 PostgreSQL 条件 skip;真实 PostgreSQL 18
migration/runtime 分角色集成另 1 pass
- SQLite 覆盖 exact revision、最坏情况 Quote、provider cost 覆盖、PriceSettlement、
quota 原子提交、exact replay,以及 priced unknown recovery 保留 Quote/全额 reservation
且不创建 Settlement
- PostgreSQL 真实集成覆盖 JSONB 回读、9004 DDL/ACL、Quote/Completion/Ledger/
PriceSettlement/QuotaSettlement 同一事务;
- SQLite 9004 checksum 为
`572e37d2f44df43a50b51a07c1b4b0bb87fbb22e9cafbd3421ec7ab250036951`
PostgreSQL 9004 checksum 为
`d38b12c2640fdd9fe21dc43a4743fb3480c988fa0a87e210fd81074d87569d2f`
- QL3 22 个 package importer 的完整拓扑 build/test 退出 0dependency audit 覆盖
`@qinglong/ai` 16 个源码文件且 `findings=[]`
- edge-ai 为 4,363,769 bytes/512 files/41 loaded modulesstandalone-ai 为
4,363,841 bytes/512 files/41 modules,均低于 5 MiB/640 filesdisabled AI 只加载
1 个模块、RSS 增量 425,984 bytes,三种 Profile 的 storage/provider loader 均为 0
- PostgreSQL 18.4 arm64 physical HA 在 timeline 1→2 promotion 前后精确复验八张
`ql3_ai` 表、90019004 history/checksum 和八表 runtime append-only ACL
`optionalAiFeatureSchemaSurvivesPromotion=true` 且总 `passed=true`
## 后续门禁
1. 建立签名或受权限保护的 durable catalog publisher、activation/revoke 与配置 ceremony
2. 明确非 USD 币种、汇率 revision、cached-input/批处理等 provider-specific 计价维度;
3. 增加 Quote/Settlement 数据行级 COMMIT-response-loss、promotion 与并发 revision 切换
fault,而不只验证 schema promotion
4. 完成认证、Project Policy、rate limit、低敏 audit 的 pricing/usage/quota API、CLI/UI
5. 完成不可变 rollup、coverage receipt、retention 和磁盘耗尽证据。