- 使用 SQLite 保存关联后的请求生命周期和用量记录 - 支持长上下文阶梯价格和可配置的 Fast 计费倍率 - 添加管理接口和可配置列的用量面板 - 保留失败、取消、重试和 compact 请求,便于计费核对
379 lines
15 KiB
Markdown
379 lines
15 KiB
Markdown
# 价格目录与计价政策模块
|
||
|
||
## 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、版本重放和溢出边界全部通过;
|
||
- 用户侧只看到金额,管理员才能查看价格计算细节。
|