7.8 KiB
ADR-0173:耐久 Model Price Catalog 发布、激活与撤销
- 状态:Accepted
- 日期:2026-07-27
- 关联:RFC D-12、D-13、D-156、D-160、D-161、D-162、D-163;ADR-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。
9001–9004 的 migration identity 和 checksum 不变。9005 checksum 固定为:
- SQLite:
20d5c288dfab65ac7ea75a96b7302f9d59cd1bfdf06af28f3868261f6e2e3013; - PostgreSQL:
7db1a80fab1aa3dee3a4c4bcae5add53758418504f63f4b7d253b090506d7864。
5. Pricing authority 属于 Storage,不属于 Provider
AI Profile 的 Storage authority 必须同时提供 durable invocation repository 与
ModelPriceCatalogResolver。启用顺序保持:
- load/validate Storage 与 pricing;
- bounded recovery;
- load provider credential/policy;
- active。
缺少 pricing 时在 provider credential loader 前失败关闭,并回收已取得的无效 storage authority;provider 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。
被否决方案
- 可变 current-price 单行:无法保存历史、精确重放或证明不确定提交的 winner。
- 让 provider adapter 持有价格:把 credential 生命周期和计价权威错误合并。
- 完成时读取最新价格:破坏 ADR-0172 的 exact Quote 绑定。
- 先查 current head 再查 publication:并发切换时可能拼接两个 revision 的事实。
- 撤销后允许重新激活:错误或泄露价格可被 stale writer 复活。
- 用应用时钟:Cluster 副本时钟漂移会产生不同 publication/head 时间。
- 给 runtime 写目录权限:外部调用面可改写计费配置,违反最小权限。
- 新建 pricing package/service:没有独立部署收益,却增加 edge importer、制品和运维成本。
当前验证
- domain/SQLite/Profile 定向门覆盖 publication、activation/deactivation/revoke、exact replay、并发单赢家、永久撤销、损坏 JSON、AbortSignal 与无效 authority 回收;
- migration contract 证明 9001–9004 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表、9001–9005 history/checksum、runtime catalog read-only、admin append-only 与其它业务角色 deny ACL;旧主 fencing、分区零确认丢失、pg_rewind只读重入、双 fresh control 与总gates.passed=true; - 没有新增 workspace package或第三方依赖。
后续门禁
- 为 publish/transition 增加 COMMIT-response-loss 与 promotion 期间的数据行级 fault;
- 建立认证、Policy、rate limit、低敏 audit、preview/confirm 的本机 CLI 与 Cluster 管理 facade;
- 决定高风险价格是否要求签名、双人复核或 break-glass ceremony;
- 设计非 USD、汇率 revision、cached-input、batch 与 provider-specific 计价维度;
- 在不可变 rollup/coverage receipt 前禁止删除 publication/head 历史。