Files
cpa-plugin/docs/modules/pricing.md
T
chuan 6a461bf1f3 feat: 添加持久化用量计费与管理面板
- 使用 SQLite 保存关联后的请求生命周期和用量记录
- 支持长上下文阶梯价格和可配置的 Fast 计费倍率
- 添加管理接口和可配置列的用量面板
- 保留失败、取消、重试和 compact 请求,便于计费核对
2026-08-14 21:08:02 +08:00

379 lines
15 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.
# 价格目录与计价政策模块
## 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、版本重放和溢出边界全部通过;
- 用户侧只看到金额,管理员才能查看价格计算细节。