# 价格目录与计价政策模块 ## 1. 定位 价格模块负责回答一个问题:**在某个不可变价格版本下,这次实际模型与速度层级应该使用什么计价政策**。 它拥有: - 模型价格来源、候选、草稿和已发布版本; - 模型别名到规范计费模型的显式映射; - 普通输入、缓存读取、缓存写入和输出四段单价; - 长上下文阈值与阶梯价格; - Fast/priority 等速度层级政策; - 价格预览、发布、回滚和只读运行时快照; - 无法定价、来源过期和规则冲突诊断。 它不拥有: - Provider Usage 的 Token 语义归一化,该职责属于 [collection.md](collection.md); - 最终金额结算、余额、套餐和账本,该职责属于 [billing.md](billing.md); - 模型调用、OAuth 账户健康和路由选择; - 按历史 Token 动态改写已经入账的金额; - 用户侧 Credits 或 Token 额度。用户核心单位始终是金额。 Billing 向本模块提供价格查询主题,本模块返回不可变 `ResolvedPricePolicy`。Billing 再使用实际规范化 Usage 计算金额并将所用政策完整快照写入账单。 ## 2. 不可违背的原则 ### 2.1 候选价格绝不是生产价格 外部目录、官方网页抓取结果和管理员输入先进入 candidate/draft,只有经过验证和显式发布后才成为生产价格。 生产请求只能读取 `published` 版本。以下行为禁止: - models.dev 或其他第三方目录刷新后自动改变生产计费; - 请求执行过程中读取网络价格; - 直接修改已发布版本; - 因价格源暂时不可用而把未知价格当成 0; - 用“当前最新价格”重算并覆盖历史账单。 ### 2.2 价格和倍率使用确定性表示 结算币种第一版固定为 USD。持久化和领域计算不使用 `float64` 表示最终费率或金额: ```go type Rate struct { MicrosPer1M int64 // 每 1,000,000 Token 的微美元数 } type Ratio struct { Numerator int64 Denominator int64 } ``` 例如 Fast 2.5× 保存为 `5/2`,不是二进制浮点数。计算使用溢出安全的整数/大整数中间值,每个 Execution 的完整 canonical Usage 只在最终总额处执行一次 `round-half-up` 到 micro USD。Usage revision 通过“新累计应收总额减已入账总额”生成差额,不能对每次增量分别舍入后累加。 ### 2.3 发布后不可变 回滚不是重新编辑旧版本,而是以旧版本内容创建一个新的发布版本。这样每笔账单都能永久引用原始 `price_version_id`,同时保留谁在何时恢复了哪一版。 ### 2.4 无价格必须显式失败 - 准入时无法解析允许模型的可信价格:`503 billing_price_unavailable`,不触达上游; - 执行后才发现实际模型/tier 与准入假设不同且无法定价:保存 `unpriced` 事实、告警并阻止该 Credential/模型的后续请求; - 明确发布的零价格是合法价格,必须通过 `is_explicit_free=true` 与“未解析”区分; - 管理员补发价格后只能追加 settlement/adjustment,不修改原始 Usage 或旧账本。 ## 3. 核心领域对象 ### 3.1 `PriceSource` 记录价格信息从哪里来: ```text PriceSource ├─ source_id ├─ kind # official/manual/external ├─ name ├─ source_uri ├─ retrieved_at ├─ effective_hint ├─ content_hash ├─ raw_reference # 受限引用或原始快照位置 └─ verification_status ``` `official` 只表示来源类型,不代表内容已自动获准发布。外部内容要保存抓取时间和哈希;来源正文不进入请求关键路径。 ### 3.2 `PriceCandidate` 候选是某个来源解析出的模型价格建议: ```text PriceCandidate ├─ candidate_id ├─ source_id ├─ source_model ├─ canonical_model_hint ├─ four_segment_rates ├─ long_context_policy ├─ tier_policy ├─ source_effective_at ├─ parse_warnings[] └─ status # new/reviewed/rejected/imported ``` 同一模型可以同时存在多个来源候选。冲突必须显示差异,不能用抓取先后顺序静默覆盖。 ### 3.3 `PriceVersion` ```text PriceVersion ├─ price_version_id ├─ sequence ├─ currency # USD ├─ state # draft/published/superseded/rejected ├─ parent_version_id ├─ content_hash ├─ change_summary ├─ created_at / created_by ├─ published_at / published_by └─ source_snapshot[] ``` 任一时刻只有一个运行时 active published version。数据库可以保存未来生效版本,但 MVP 不做定时自动切换;管理员在生效点明确发布,避免 c-shared 内依赖常驻 timer。 ### 3.4 `ModelPricePolicy` ```text ModelPricePolicy ├─ price_version_id ├─ canonical_model ├─ provider # MVP 为 openai/codex ├─ input_rate ├─ cache_read_rate ├─ cache_write_rate ├─ output_rate ├─ explicit_free ├─ long_context_policy? ├─ service_tier_policy ├─ source_refs[] └─ notes ``` 四段费率始终完整保存。来源没有缓存写价格时不能擅自沿用普通输入价格;只有管理员明确确认的 fallback 规则才能编译进版本。 ### 3.5 `ModelAlias` ```text ModelAlias ├─ alias ├─ canonical_model ├─ alias_kind # request/route/upstream ├─ provider ├─ valid_from_version └─ status ``` MVP 生产匹配只允许精确模型名和显式 alias,不支持任意 glob。key-billing 的 glob 实现可以参考算法,但模糊规则在新增模型后可能意外继承价格,不适合作为金额账本默认策略。 ### 3.6 `ResolvedPricePolicy` 这是 Pricing 给 Billing 的唯一运行时输出: ```text ResolvedPricePolicy ├─ price_version_id ├─ canonical_model ├─ matched_value ├─ matched_by # upstream_model/model_alias/route_alias ├─ four_segment_rates ├─ long_context_policy ├─ effective_service_tier ├─ tier_multiplier # Ratio ├─ source_refs └─ policy_hash ``` Billing 必须把这些字段复制为账单价格快照,不能只保存一个指向可变当前目录的模型名。 ## 4. 模型解析 一次请求至少可能出现: - `requested_model`:用户请求的名字; - `route_model` / `model_alias`:CPA 路由和别名; - `selected_model`:选择账户后准备执行的模型; - `upstream_model`:上游响应或 Usage 确认的实际模型。 MVP 解析顺序固定为: 1. 去除已被 CPA 证明只是选项的 thinking suffix,不改变真正模型 ID; 2. 若实际 `upstream_model` 精确命中已发布模型,使用它; 3. 否则按类型和 provider 查找显式 `ModelAlias`; 4. 若管理员明确把 route model 发布为独立计费模型,允许精确命中; 5. 多个候选同优先级冲突时返回 `ambiguous_price_match`; 6. 没有可信匹配时返回 unavailable,绝不按字符串相似度猜测。 准入时还没有最终 upstream model。此时必须锁定本次允许使用的 `price_version_id` 和候选政策。执行后若模型发生变化,在**同一个价格版本**内重新解析并结算;不能因调用期间发布了新版本而混用新价格。 GPT-5.6 Sol/Terra/Luna 必须作为不同 canonical model 分别发布。名字相近不代表价格相同,禁止通过 `gpt-5.6-*` 默认继承同一价格。 ## 5. 四段 Token 价格 价格政策只定义单价;Token 是否重叠由采集/归一化模块决定。Billing 输入必须已经形成四个互斥区段: ```text 普通输入 + 缓存读取 + 缓存写入 + 输出 ``` Reasoning Token 若已经包含在输出中,不增加第五段价格。后台可以展示 reasoning 明细,但不能重复收费。 费率验证至少包括: - 所有整数非负且不溢出 `int64` 支持范围; - 四段字段不得缺失; - 显式免费必须带确认标记和审计原因; - 最坏 Token × 费率 × 倍率的中间结果可安全计算; - 相同 canonical model 在同一版本中只能有一条 base policy。 ## 6. Fast / priority 政策 Fast 是金额倍率,不是独立余额单位。GPT-5.6 Fast/priority 的当前产品政策保存为版本化 `5/2`,只应用一次。请求可能使用 OpenAI/Codex 的 `service_tier=priority`,也可能使用 Anthropic 的 `speed=fast`。采集层必须把两种输入和最终上游请求中的 Fast 状态归一化为一个 `fast_requested` 事实。 首先合并请求和响应事实: ```text effective_service_tier = 响应明确确认的 tier 否则请求明确指定的 tier 否则 standard ``` `effective_service_tier=priority` 或请求 `speed=fast` 都表示 Fast。价格政策中的 `fast_pricing_enabled` 决定是否应用倍率,因此 Fast 请求可以按标准价格结算。禁止同时配置: - `service_tier=priority × 2.5`;以及 - `response_service_tier=priority × 2.5` 并让二者命中时变成 6.25×。 `cpa-usage-keeper` 的固定字段规则和不可变 Snapshot 值得复用,但其 resolver 会把每个命中的字段规则连续相乘;这对一般分析倍率合理,对 Fast 请求/响应两个同义观察不合理。本项目必须先归一化为唯一 `effective_service_tier`。 发布 GPT-5.6 价格版本时必须用当时的 OpenAI 官方资料重新确认:支持的具体模型、倍率、生效时间以及是否有额外限制。文档不硬编码可能过期的具体基础单价。 ## 7. 长上下文政策 ```text LongContextPolicy ├─ threshold_input_tokens ├─ comparison # gt/gte,必须显式 ├─ input_rate ├─ cache_read_rate ├─ cache_write_rate └─ output_rate ``` 判断输入是归一化后的总输入 Token,包含普通输入、缓存读取和缓存写入;阈值边界由 `comparison` 明确,不能隐藏在代码的 `>` 或 `>=` 中。 组合顺序固定为: 1. 根据总输入判断是否命中长上下文; 2. 选择完整四段 base rates; 3. 计算四段基础金额; 4. 对总基础金额应用一次 Fast/tier 倍率; 5. 最终舍入为 `amount_micros`。 导入 models.dev 时必须保留并核对 tiers。当前 usage-keeper 的自动价格同步忽略长上下文 tiers,不能原样复制。 ## 8. 来源优先级与发布流程 候选展示优先级: 1. 管理员手工输入并说明来源; 2. 项目维护的官方来源快照; 3. models.dev 等外部目录; 4. 无来源。 优先级只影响 review UI,不自动决定发布。标准发布流程: ```text fetch/import candidate → parse + hash → match canonical models → validate numeric/token/tier semantics → diff against active version → run golden price cases → administrator approve with reason → persist immutable version → atomically publish compiled Snapshot → emit audit + projection event ``` 发布事务必须先完整写入版本、明细、来源引用和审计,再更新 active pointer。运行时 `Catalog` 使用原子指针替换完整只读 Snapshot;请求不得看见半个新版本。 回滚流程创建新版本并重复相同验证。发布失败时继续使用最后有效版本,readiness 显示 `pricing_publish_failed`;不得清空当前价格。 ## 9. 持久化建议 | 表 | 用途 | | --- | --- | | `price_sources` | 来源与抓取元信息 | | `price_candidates` | 未发布候选和解析诊断 | | `price_versions` | 不可变版本头 | | `model_price_policies` | 每版模型四段价格 | | `model_aliases` | 版本化精确别名 | | `long_context_policies` | 阈值与阶梯价格 | | `service_tier_policies` | tier 与有理数倍率 | | `price_publications` | 发布、回滚和审批审计 | active version 可以存于 `app_settings`,但更新必须与版本发布事务一致。旧版本不能随普通归档删除;只要账本仍引用就永久保留。 ## 10. API 与 UI 输出 管理员需要看到: - active version、发布时间、操作者和来源; - 当前模型价格、alias、长上下文和 tier 政策; - candidate 与 active 的逐字段差异; - 使用中的未定价模型和受影响请求; - 发布前 golden case 结果; - 历史版本和回滚入口。 用户 API 只返回已结算金额、币种和必要说明,不返回 Token 单价、来源、规则或倍率。管理员接口路径和 DTO 见 [api.md](api.md)。 ## 11. 运行时与故障策略 - Resolver 只读取内存 Snapshot,不做 SQL、网络或 host callback; - Snapshot 编译在管理命令/安全 maintenance budget 中完成; - 外部目录失败只影响 candidate refresh,不影响 active version; - active version 缺失、损坏或 currency 不匹配时计费 readiness 失败,新请求 fail closed; - 实际 Usage 无法匹配价格时保存事实并阻断后续同范围请求,不能 panic 或返回会被 CPA 忽略的 interceptor RPC error; - 热配置不能直接改变历史或 active price content;价格发布走业务 API 和审计事务。 ## 12. 如何参考现有项目 ### 12.1 `cpa-plugin-key-billing` 优先参考: - `internal/billing/pricing.go`:四段 Token 价格、模型匹配和长上下文计算; - `internal/billing/catalog.go`:models.dev 下载、大小限制、缓存和候选解析; - `internal/billing/admin.go`:目录搜索、覆盖和价格校验; - `internal/billing/pricing_test.go`:四段不重复和阈值测试。 可以复用算法思路,不能复制其 `float64` 账务表示、自动把目录当 builtin production price、模糊 glob 默认匹配或缺少 Fast 政策的行为。 ### 12.2 `cpa-usage-keeper` 优先参考: - `internal/pricing/catalog.go`、`snapshot.go`:完整编译后原子发布只读快照; - `internal/pricing/fields.go`、`resolver.go`:固定枚举维度和条件倍率; - `internal/repository/pricing*.go`:价格与规则持久化; - `internal/service/pricing_metadata_sync.go`:同步预览; - `internal/helper/usage_cost.go`:四段费用拆分。 应吸收 Snapshot、固定字段和完整校验;不能继承动态查询重算历史费用、多条同义 tier 规则相乘、浮点账本和忽略长上下文 tiers。 参考版本: - `cpa-plugin-key-billing` `25b534ae386f830f537cca9215cff5586e630b3a`; - `cpa-usage-keeper` `d62cad3f345ae574089a14a4ac75cca023c7ead6`。 ## 13. 验收标准 - 没有已发布可信价格时请求不会免费穿透; - GPT-5.6 Sol/Terra/Luna 独立解析且来源可追溯; - Fast/priority 在请求、响应或两者同时报告时都只应用一次 2.5×; - 长上下文阈值前、边界和边界后结果符合显式 comparison; - 四段 Token 价格不重叠,Reasoning 不重复收费; - 所有费率、倍率和最终金额确定性计算,无浮点累计漂移; - 外部目录刷新不自动改变生产价格; - 发布原子可见,失败保持 LKG; - 回滚生成新版本,旧账单仍引用原版本; - alias 冲突和未知模型明确失败,不做模糊猜测; - 价格 Golden Tests、版本重放和溢出边界全部通过; - 用户侧只看到金额,管理员才能查看价格计算细节。