Files
qinglong/docs/adr/ADR-0173-durable-model-price-catalog-publication-and-activation.md
T

154 lines
7.8 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-0173:耐久 Model Price Catalog 发布、激活与撤销
- 状态:Accepted
- 日期:2026-07-27
- 关联:RFC D-12、D-13、D-156、D-160、D-161、D-162、D-163ADR-0167、ADR-0168、ADR-0170、ADR-0171、ADR-0172
## 背景
ADR-0172 已要求一次 Model invocation 在 provider I/O 前绑定 exact price revision
但当时的 `StaticModelPriceCatalog` 只是运行时消费契约。它没有回答:
- 谁能发布价格,以及 publication 如何绑定 User、mutation 与数据库时间;
- Cluster 多副本如何原子决定当前 revision,SQLite 重启后如何保留同一事实;
- activation 响应丢失、并发切换和 stale writer 如何精确收敛;
- 错误价格如何永久撤销,避免稍后被另一副本重新激活;
- runtime 为什么只能读价格,而不能成为价格管理 authority;
- 如何在不增加低配路由器常驻进程、package 或依赖的情况下提供双方言能力。
## 决策
### 1. Publication 是不可变事实
`qinglong/model-price-catalog-publish-command@v1` 固定 provider、model、
price revision、USD 整数 micro-rate、mutation ID、publisher User 与 command digest。
Repository 使用数据库时钟创建
`qinglong/model-price-catalog-publication@v1`,并绑定 catalog、command 与 publication
digest。
同一 provider/model/revision 只能存在一条 publication。同一 mutation 与完全相同
command 可返回 `existing`revision、mutation 或内容任一漂移都必须冲突,不能 update、
upsert 或 delete 历史价格。
### 2. 当前配置只由 append-only generation head 表达
`qinglong/model-price-catalog-transition-command@v1` 支持:
- `activate`:把一个已发布且未撤销的 revision 设为 current
- `deactivate`:明确进入无 active revision
- `revoke`:为目标 revision 追加永久 tombstone;若它正 active,同时清空 active。
每条 `qinglong/model-price-catalog-head@v1` 绑定 expected generation、previous head
digest、action、active/revoked 投影、User、mutation、数据库时间、command digest 与
head digest。写入只允许 `generation + 1`。同 command 精确重放返回 `existing`stale
generation、detached digest、no-op、同 generation 竞争或 mutation 漂移都失败。
revoked revision 在该 provider/model 下永久不能再次 activate。撤销不是删除
publication;历史 Quote 和审计仍能证明当时使用的 exact catalog digest。
### 3. Resolver 只读取最新 head 精确激活的 revision
运行时 lookup 必须同时匹配 provider、model、price revision 与最新 head 的 active
projection;其它已发布但未激活、旧 active、deactivated 或 revoked revision 一律返回
unavailable。不得 fallback 到最高 revision、最近 publication 或静态默认价格。
SQLite 与 PostgreSQL resolver 都使用单条 publication/head join,避免先读 head、再读
publication 时遇到 activation 切换而产生撕裂。持久化 JSON 与结构列必须重新规范化并
核对 digest;损坏数据失败关闭。AbortSignal 的非法形状被拒绝,真实 cancellation reason
原样传播,不包装为存储故障。
### 4. 双方言使用各自最小并发原语
新增且只新增:
- SQLite `9005-ai-model-price-catalog`
- PostgreSQL `pg-9005-ai-model-price-catalog`
- publication/head 两张双方言表及 current/revoked 索引。
SQLite writer 复用同一进程的 shared operation authority,并在 `BEGIN IMMEDIATE` 短事务
中执行 publication 或 head CAS。PostgreSQL 使用 SERIALIZABLE transaction,并按
canonical provider/model identity 获取 transaction advisory lock;数据库 constraint、
serialization、deadlock 与 stale CAS 统一暴露为 catalog conflict。
90019004 的 migration identity 和 checksum 不变。9005 checksum 固定为:
- SQLite
`20d5c288dfab65ac7ea75a96b7302f9d59cd1bfdf06af28f3868261f6e2e3013`
- PostgreSQL
`7db1a80fab1aa3dee3a4c4bcae5add53758418504f63f4b7d253b090506d7864`
### 5. Pricing authority 属于 Storage,不属于 Provider
AI Profile 的 Storage authority 必须同时提供 durable invocation repository 与
`ModelPriceCatalogResolver`。启用顺序保持:
1. load/validate Storage 与 pricing
2. bounded recovery
3. load provider credential/policy
4. active。
缺少 pricing 时在 provider credential loader 前失败关闭,并回收已取得的无效 storage
authorityprovider authority 不再携带价格。这样价格事实跟随 SQLite/PostgreSQL
durability 与 failover,而不是跟随短生命周期 credential adapter。
### 6. 数据库角色按读写职责分离
PostgreSQL
- `ql3_runtime` 对两表只有 `SELECT`
- `ql3_admin` 对两表只有 `SELECT, INSERT`
- 两者都没有 `UPDATE, DELETE`
- package-manager、package-executor、worker-ingress 与 PUBLIC 无目录权限;
- schema owner/migration role 保留受审迁移 authority。
管理 repository 只能由短生命周期 admin composition 注入;常驻 runtime 只构造 reader。
### 7. 不新增 package、依赖或后台服务
领域 contract 与 repository 留在现有 `@qinglong/ai`
- `price-catalog`
- `local-price-catalog-storage`
- `postgres-price-catalog-storage`
根入口只导出 profile-neutral catalog contract;存储 adapter 通过显式 subpath 到达。
没有新增 workspace package、第三方依赖、timer、watcher、轮询或独立价格服务。AI 禁用时
仍保持零 catalog loader、零迁移和零后台活动。
本 ADR 不开放 HTTP、MCP、UI 或默认 Cluster 管理进程。产品入口仍必须另行建立认证、
Project/Role Policy、rate limit、低敏 audit、发布预览与必要的双人/签名 ceremony。
## 被否决方案
1. 可变 current-price 单行:无法保存历史、精确重放或证明不确定提交的 winner。
2. 让 provider adapter 持有价格:把 credential 生命周期和计价权威错误合并。
3. 完成时读取最新价格:破坏 ADR-0172 的 exact Quote 绑定。
4. 先查 current head 再查 publication:并发切换时可能拼接两个 revision 的事实。
5. 撤销后允许重新激活:错误或泄露价格可被 stale writer 复活。
6. 用应用时钟:Cluster 副本时钟漂移会产生不同 publication/head 时间。
7. 给 runtime 写目录权限:外部调用面可改写计费配置,违反最小权限。
8. 新建 pricing package/service:没有独立部署收益,却增加 edge importer、制品和运维成本。
## 当前验证
- domain/SQLite/Profile 定向门覆盖 publication、activation/deactivation/revoke、exact
replay、并发单赢家、永久撤销、损坏 JSON、AbortSignal 与无效 authority 回收;
- migration contract 证明 90019004 checksum 不变、9005 双方言 DDL/history/ACL 固定;
- 真实 PostgreSQL 18 以 migration/admin/runtime 三个连接验证正式主迁移与 feature
migration、publication、activation race、active-only read、inactive/active revoke、
永久 reactivation conflict、runtime read-only 与 admin append-only
- PostgreSQL 18.4 arm64 physical HA 在 timeline 1→2 promotion 前后逐字段复验十张
`ql3_ai` 表、90019005 history/checksum、runtime catalog read-only、admin append-only
与其它业务角色 deny ACL;旧主 fencing、分区零确认丢失、`pg_rewind` 只读重入、双
fresh control 与总 `gates.passed=true`
- 没有新增 workspace package或第三方依赖。
## 后续门禁
1. 为 publish/transition 增加 COMMIT-response-loss 与 promotion 期间的数据行级 fault
2. 建立认证、Policy、rate limit、低敏 audit、preview/confirm 的本机 CLI 与 Cluster 管理
facade
3. 决定高风险价格是否要求签名、双人复核或 break-glass ceremony
4. 设计非 USD、汇率 revision、cached-input、batch 与 provider-specific 计价维度;
5. 在不可变 rollup/coverage receipt 前禁止删除 publication/head 历史。