- 使用 SQLite 保存关联后的请求生命周期和用量记录 - 支持长上下文阶梯价格和可配置的 Fast 计费倍率 - 添加管理接口和可配置列的用量面板 - 保留失败、取消、重试和 compact 请求,便于计费核对
619 lines
30 KiB
Markdown
619 lines
30 KiB
Markdown
# 计费模块
|
||
|
||
## 1. 模块目标
|
||
|
||
计费模块服务于一个核心场景:管理员把 CPA 的模型能力分享给其他人,为每位用户分发独立 API Key,并以**金额额度**控制其使用。
|
||
|
||
完整闭环:
|
||
|
||
```text
|
||
管理员创建或同步 API Key
|
||
→ 设置金额套餐与周期
|
||
→ 请求进入时检查剩余金额
|
||
→ CPA 执行模型请求
|
||
→ 按真实 Usage、价格和倍率计算金额
|
||
→ 原子记账并更新余额
|
||
→ 额度耗尽后拒绝新请求
|
||
```
|
||
|
||
本模块包含:
|
||
|
||
- API Key 身份识别和脱敏标识;
|
||
- 金额套餐、周期、额度和余额;
|
||
- 请求准入、并发状态和终态清理;
|
||
- Usage 归一化、价格解析和金额计算;
|
||
- Fast/priority、长上下文等计价规则;
|
||
- 不可重复结算的金额账本;
|
||
- 管理端的套餐、绑定、充值、重置、停用和账单查询;
|
||
- 向统计模块输出标准计费事件。
|
||
|
||
本模块不负责趋势图、排行榜、热力图、RPM/TPM、延迟分析等统计产品能力。
|
||
|
||
## 2. 不可违背的金额原则
|
||
|
||
### 2.1 唯一计费单位
|
||
|
||
系统对共享用户提供的计费单位有且只有**金额**。
|
||
|
||
用户获得、看到和理解的内容只能是:
|
||
|
||
- 套餐金额;
|
||
- 已消费金额;
|
||
- 剩余金额;
|
||
- 本次请求金额;
|
||
- 周期内金额明细。
|
||
|
||
用户侧不得出现以下额度概念:
|
||
|
||
- Credits、点数或积分;
|
||
- Token 配额或 Token 余额;
|
||
- 按请求次数折算的额度;
|
||
- 隐藏的第二套扣费单位。
|
||
|
||
Token、缓存、模型单价、倍率和价格版本是后台计算与审计信息。管理员可以查看,统计模块也可以分析,但它们不能成为用户的余额单位。
|
||
|
||
### 2.2 结算币种
|
||
|
||
一个部署实例只使用一种结算币种。第一版固定使用 USD;未来如支持其他币种,必须通过明确的汇率版本换算为该实例的唯一结算币种,不能让同一账户同时持有多种余额。
|
||
|
||
所有金额必须使用定点表示:
|
||
|
||
```go
|
||
type Money struct {
|
||
Currency string // v1: USD
|
||
Micros int64 // 1 USD = 1,000,000 micros
|
||
}
|
||
```
|
||
|
||
持久化、比较、扣减和额度判断全部使用整数 `Micros`。`float64` 只允许出现在外部价格解析边界,进入领域层前必须按统一规则转换并检查溢出,不能直接用于余额和账本。
|
||
|
||
### 2.3 用户视图与管理员视图
|
||
|
||
用户视图只返回金额和必要状态:
|
||
|
||
```json
|
||
{
|
||
"limit": "10.00",
|
||
"spent": "3.27",
|
||
"remaining": "6.73",
|
||
"currency": "USD",
|
||
"cycle_ends_at": "2026-09-01T00:00:00Z",
|
||
"enabled": true
|
||
}
|
||
```
|
||
|
||
管理员视图可以额外包含计价依据:模型、输入/缓存/输出 Token、基础价格、Fast 倍率、长上下文价格、最终金额和价格版本。
|
||
|
||
## 3. 模块边界
|
||
|
||
### 3.1 计费模块拥有
|
||
|
||
- 套餐和周期定义;
|
||
- API Key 与套餐绑定;
|
||
- 周期金额额度、已用金额和余额;
|
||
- 请求准入结果;
|
||
- 请求与最终 Usage 的关联;
|
||
- 每笔金额账本;
|
||
- 消费 [pricing.md](pricing.md) 返回的不可变 `ResolvedPricePolicy`,并把实际使用的价格政策快照固化到每笔账单;
|
||
- 充值、退款、人工调整和重置记录;
|
||
- 幂等结算键。
|
||
|
||
价格来源、候选、模型 alias、长上下文/tier 政策、版本发布和回滚属于 [价格目录与计价政策模块](pricing.md)。Billing 不能在结算时自行拉取价格或修改 active version。
|
||
|
||
### 3.2 统计模块拥有
|
||
|
||
- 原始事件的长期保存策略;
|
||
- 小时、天和实时聚合;
|
||
- 趋势、构成、排行、热力图;
|
||
- RPM、TPM、成功率、TTFT、延迟;
|
||
- 按 API Key、模型、Provider、Auth 等维度查询;
|
||
- 导出和只读分析页面。
|
||
|
||
统计模块只能消费计费模块已经确定的 `charged_amount`,不能在查询时重新计算历史账单。价格修改不得悄悄改变已经结算的金额。
|
||
|
||
### 3.3 CLIProxyAPI 能力
|
||
|
||
预期使用以下最小能力组合,最终以目标 CLIProxyAPI 源码为准:
|
||
|
||
| 能力 | 用途 |
|
||
| ----------------------------------------------------- | ------------------------------------------------------------------------- |
|
||
| `frontend_auth_provider` | 校验插件签发的下游 Key,并向 CPA 返回不含明文 Key 的稳定身份 |
|
||
| `request_interceptor` | 识别稳定下游身份、检查金额额度并拒绝请求 |
|
||
| `request_lifecycle_plugin` | 接收成功、失败、拒绝、取消等终态,释放并发并触发幂等结算 |
|
||
| `response_before_translator`、response interceptors | 取得带 RequestID 的上游 Usage,并关联非流式或流式响应 |
|
||
| `usage_plugin` | 补充 CPA 标准 Usage、上游身份、性能和失败信息;当前不能单独承担按请求结算 |
|
||
| `scheduler` | 按 Key 的账户绑定选择上游 AuthID |
|
||
| `management_api` | 暴露计费管理 API 和管理页面 |
|
||
|
||
主路径使用 CPA 的 `frontend_auth_provider`,由插件自己签发、校验、禁用和轮换下游 Key。插件只保存高熵 Key 的安全校验值,认证成功后把稳定 `credential_id` 作为 Principal 返回给 CPA;CPA 再基于该 Principal 产生不可逆 `caller_scope`。这样 Key 生命周期、金额账户和路由绑定可以在同一业务事务内维护,不依赖修改 CPA 配置文件。
|
||
|
||
生产模式必须声明 `frontend_auth_provider_exclusive: true`,并在真实 CPA 中验证本插件是唯一生效的认证路径,避免 CPA 原生 Key 绕过插件准入。CPA 原生 `api-keys` 只保留为显式兼容/迁移模式;该模式下任何不能映射到本地 Credential 的调用都必须按配置拒绝或标为受控例外,不能默认无限使用。
|
||
|
||
## 4. 核心领域模型
|
||
|
||
### 4.1 API Key 标识
|
||
|
||
插件不持久化明文 Key。签发时从明文生成或提取:
|
||
|
||
- public key ID 与 HMAC 校验摘要:用于认证 lookup;
|
||
- 脱敏 `key_preview`:用于管理员辨认;
|
||
- 可选用户标签。
|
||
|
||
一个逻辑 Credential 可以包含多个 `CredentialSecretVersion`。secret 轮换新增 version、保留同一 `credential_id` 和 BillingAccount,overlap 后撤销旧 version;只有独立新 Key 才创建新 Credential。认证成功后向 CPA 返回稳定 `credential_id` Principal;CPA 的 `caller_scope` 是该 Principal 的不可逆请求期标识。业务表使用 `credential_id/billing_account_id` 外键,不把 caller scope 当作唯一长期主键。
|
||
|
||
额度实际挂在 `BillingAccount`,每个 Credential 必须绑定一个 BillingAccount。第一版创建用户 Key 时默认一并创建独立 BillingAccount,因此产品表现仍是“每个 Key 可分配金额额度”;轮换 Key 时绑定同一 BillingAccount 即可保留余额和历史。未来多个 Key 需要共享额度时只调整绑定,不改变账本模型。
|
||
|
||
### 4.2 套餐
|
||
|
||
```text
|
||
BillingPlan
|
||
├─ id
|
||
├─ name
|
||
├─ amount_micros
|
||
├─ currency
|
||
├─ period_kind fixed / never
|
||
├─ period_seconds daily/weekly 等 UI 预设都换算为固定秒数
|
||
├─ enabled
|
||
└─ timestamps
|
||
```
|
||
|
||
周期在该 BillingAccount 第一次成功准入时开始,沿用 `cpa-plugin-key-billing` 的简单行为。日历月、统一账单日和时区对齐留给后续明确需求,第一版不暗中引入复杂规则。
|
||
|
||
### 4.3 周期账户
|
||
|
||
```text
|
||
BillingAccountCycle
|
||
├─ billing_account_id
|
||
├─ plan_id
|
||
├─ cycle_id
|
||
├─ starts_at
|
||
├─ ends_at
|
||
├─ limit_micros
|
||
├─ spent_micros # 可重建消费投影
|
||
├─ balance_micros # 可重建余额投影
|
||
└─ status
|
||
```
|
||
|
||
请求准入时必须记录其 `plan_id` 和 `cycle_id`。延迟结束的请求只能计入准入时所属周期,不能误扣到新周期或新套餐。
|
||
|
||
### 4.4 账本
|
||
|
||
账本是金额事实来源,不以可变聚合字段作为唯一依据:
|
||
|
||
```text
|
||
BillingLedgerEntry
|
||
├─ id
|
||
├─ idempotency_key
|
||
├─ billing_account_id
|
||
├─ credential_id # 请求归因;充值可为空
|
||
├─ request_id
|
||
├─ execution_id
|
||
├─ billing_record_id
|
||
├─ cycle_id
|
||
├─ kind charge / late_settlement / credit / refund / adjustment
|
||
├─ spend_delta_micros
|
||
├─ balance_delta_micros
|
||
├─ currency
|
||
├─ occurred_at
|
||
├─ booked_at
|
||
├─ pricing_version
|
||
├─ pricing_snapshot
|
||
└─ usage_snapshot
|
||
```
|
||
|
||
每次结算必须按 `(request_id, execution_id, settlement_kind, usage_revision)` 或等价稳定规则生成 `idempotency_key`。同一 Usage revision 的重复、迟到或重放不能产生第二次扣费;新的可靠 Usage revision 只能追加差额账本。
|
||
|
||
账本用两个带符号字段避免把“用户消费”和“账户余额变化”混成一个数字:
|
||
|
||
| kind | `spend_delta_micros` | `balance_delta_micros` |
|
||
| -------------------------------- | ---------------------: | -----------------------: |
|
||
| `charge` / `late_settlement` | 正数 | 负数 |
|
||
| 与 Usage 关联的`refund` | 负数 | 正数 |
|
||
| 充值`credit` | 0 | 正数 |
|
||
| 非 Usage 人工增减 | 0 | 按方向正/负 |
|
||
| 纠正历史费用的 adjustment | 按消费方向正/负 | 与其相反 |
|
||
|
||
用户消费趋势汇总 `spend_delta_micros`;余额从 `balance_delta_micros` 推导。充值和普通账户调整没有 model/Auth/endpoint 维度,不能塞进请求消费趋势。
|
||
|
||
## 5. 计价管线
|
||
|
||
计价管线必须是单向、可审计的:
|
||
|
||
```text
|
||
原始 Usage
|
||
→ Token 语义归一化
|
||
→ 确定计费模型
|
||
→ 解析版本化基础价格
|
||
→ 应用长上下文价格
|
||
→ 确定 effective service tier
|
||
→ 应用 Fast/priority 倍率
|
||
→ 转换为整数金额
|
||
→ 写入不可变账本
|
||
→ 发布 BillingEvent
|
||
```
|
||
|
||
### 5.1 Token 归一化
|
||
|
||
计价使用四个互不重叠的区段:
|
||
|
||
```text
|
||
普通输入 + 缓存读取 + 缓存写入 + 输出
|
||
```
|
||
|
||
必须识别供应商字段语义:
|
||
|
||
- 缓存 Token 是否已包含在 `input_tokens` 中;
|
||
- Reasoning Token 是否已包含在 `output_tokens` 中;
|
||
- `total_tokens` 与各区段是否一致;
|
||
- 重试和流式响应是否上报了重复 Usage。
|
||
|
||
信息不完整或矛盾时必须标记 `accounting_quality`,并采用以下固定策略:
|
||
|
||
- `complete` / `normalized`:正常结算;
|
||
- `partial`:只结算语义明确且未与其他字段重叠的已确认部分,状态保持 `partial`,后续 revision 只追加差额;
|
||
- `missing` / `unclassified` / `inconsistent`:不猜测金额,进入 `awaiting_usage` 或 `review_required`,当前展示金额为 0 但不标记为免费;
|
||
- 准入时找不到可信价格:在触达上游前返回 `503 billing_price_unavailable`;
|
||
- 实际模型/tier 与准入快照不同而在执行后才发现无价格:保存 `unpriced` 事实、告警并阻止该 credential/model 的后续新请求,管理员发布可追溯价格版本后通过追加结算处理。
|
||
|
||
不得用“按最高价保守扣费”或“把未知都当零”代替真实事实。
|
||
|
||
### 5.2 基础金额公式
|
||
|
||
```text
|
||
基础金额 =
|
||
普通输入 Token × 输入单价
|
||
+ 缓存读取 Token × 缓存读取单价
|
||
+ 缓存写入 Token × 缓存写入单价
|
||
+ 输出 Token × 输出单价
|
||
```
|
||
|
||
Reasoning Token 用于后台明细,但若上游语义表明其已经包含在输出中,不得再次相加收费。
|
||
|
||
### 5.3 Fast / priority 2.5×
|
||
|
||
Fast/priority 是金额倍率,不是另一种余额单位。OpenAI/Codex 通过 `service_tier=priority` 表达,Anthropic 通过 `speed=fast` 表达:
|
||
|
||
```text
|
||
Fast 最终金额 = 基础金额 × 2.5
|
||
```
|
||
|
||
不能同时对请求 `service_tier=priority` 和响应 `response_service_tier=priority` 各乘一次。应先得到唯一的有效速度层级:
|
||
|
||
```text
|
||
effective_service_tier =
|
||
响应明确确认的层级
|
||
否则请求指定的层级
|
||
否则 standard
|
||
```
|
||
|
||
两种表达先归一化为一个 `fast_requested` 事实,然后只应用一次倍率。价格配置可以关闭 Fast 加价;关闭后请求仍使用 Fast,但金额不乘倍率。倍率和是否实际应用必须保存在账本价格快照中。
|
||
|
||
Fast 规则必须是版本化价格政策的一部分,不能依赖管理员每次手工补规则。实现时需要用当前 OpenAI 官方资料再次核对支持模型和倍率。
|
||
|
||
### 5.4 长上下文
|
||
|
||
价格目录包含长上下文阈值时,使用归一化后的总输入 Token 判断是否进入阶梯价;阈值、命中的价格和判断输入必须写入价格快照。
|
||
|
||
Fast 倍率作用于长上下文基础金额之后:
|
||
|
||
```text
|
||
最终金额 = 长上下文规则计算出的基础金额 × Fast 倍率
|
||
```
|
||
|
||
### 5.5 价格来源与优先级
|
||
|
||
价格不能无条件信任第三方目录。当前已观察到 models.dev 的部分 GPT-5.6 Terra/Luna `openai` 条目与 OpenAI 官方价格不一致。
|
||
|
||
推荐优先级:
|
||
|
||
1. 管理员明确覆盖并确认的价格;
|
||
2. 项目维护的、带生效日期和来源链接的官方价格表;
|
||
3. models.dev 自动同步的候选价格;
|
||
4. 无法定价,进入显式待处理状态。
|
||
|
||
自动同步必须是“预览 → 确认 → 发布新价格版本”,不能在后台静默修改生产计费。
|
||
|
||
每笔账单至少保存:
|
||
|
||
- 实际计费模型和匹配方式;
|
||
- 四段 Token 数;
|
||
- 四段实际单价;
|
||
- 长上下文状态;
|
||
- effective service tier;
|
||
- 最终倍率;
|
||
- 价格来源和版本;
|
||
- 舍入前结果与最终 `amount_micros`。
|
||
|
||
### 5.6 Usage revision 与差额算法
|
||
|
||
`usage_revision` 由插件生成,不是 CPA/provider 传入:
|
||
|
||
1. 每个 Execution 保存 canonical cumulative vector(四段 Token)、source rank、response ID 与 canonical hash;
|
||
2. 同一 vector/hash 的重复 callback 不创建 revision;
|
||
3. 只有 SQLite 事务内 CAS 确认 canonical vector 改变后才递增 revision;
|
||
4. CPA usage、翻译前响应和翻译后响应是择优/交叉校验来源,绝不能三份相加;
|
||
5. 新应收额使用该 Execution 的原始 admission/price policy 重算,差额为“新 canonical total 应收 - 该 Execution 已入账 usage spend”;
|
||
6. 更小或矛盾 vector 标为 `inconsistent`,自动差额为 0;退款只能由显式 refund/adjustment 账本产生。
|
||
|
||
Request 级汇总不能参与单个 Execution 的差额,避免重试费用互相抵消。
|
||
|
||
## 6. 请求准入与结算
|
||
|
||
### 6.1 准入
|
||
|
||
请求进入时:
|
||
|
||
1. 使用 frontend auth 已确认的 `credential_id`,并校验 CPA `caller_scope` 映射;
|
||
2. 查询有效套餐和周期;
|
||
3. 结算已过期周期;
|
||
4. 检查账户启用状态和剩余金额;
|
||
5. 保存 pending request、周期和价格政策版本;
|
||
6. 放行或返回结构化错误。
|
||
|
||
未绑定套餐的策略必须可配置。开发/迁移时可以显式使用“记录金额但不限制”的 observe-only 模式;生产默认必须是“未绑定即拒绝”,避免新建或漏同步的 Key 意外获得无限额度。
|
||
|
||
额度耗尽时返回 HTTP `429` 和稳定错误码 `billing_quota_exhausted`。响应只描述金额余额,不泄露内部 Token、价格表或凭证信息。
|
||
|
||
### 6.2 并发透支
|
||
|
||
请求开始时无法知道最终金额。第一版沿用 `cpa-plugin-key-billing` 的务实策略:
|
||
|
||
- 余额大于零即可准入;
|
||
- 请求结束后按真实金额结算;
|
||
- 单次或并发请求可以造成有限负余额;
|
||
- 余额不再为正时拒绝新的请求。
|
||
|
||
这项行为必须在管理界面明确说明为“软金额额度/请求后结算”,不能承诺绝不超额。MVP 为每个 BillingAccount 设置默认并发上限 1,管理员可显式调高;这只能把最坏超额限制在少量在途请求,仍不是硬额度。严格预授权、输入成本估算和按允许最大输出预占属于后续增强,不能伪装成已经做到严格不透支。
|
||
|
||
### 6.3 终态
|
||
|
||
成功、失败、取消、客户端断开、流式中止和宿主关闭都必须结束运行中的并发占用。`request.complete` 是请求终态信号,但**终态类型本身不决定是否收费**;是否扣费只由上游实际产生且能够可靠确认的 Usage 决定。
|
||
|
||
统一规则:
|
||
|
||
| 场景 | 金额处理 | 记录处理 |
|
||
| ----------------------------------------- | ------------------------------- | ------------------------------------------ |
|
||
| 本地准入拒绝,未到达上游 | 不扣费 | `outcome=rejected`,无 Usage、无消费账本 |
|
||
| 请求在上游执行前取消 | 不扣费 | `outcome=canceled`,金额为 0 |
|
||
| 成功完成且有可靠 Usage | 按实际 Usage 扣费 | 正常结算 |
|
||
| 执行失败但上游报告了 Usage | 按实际 Usage 扣费 | `outcome=failed` 与金额同时保留 |
|
||
| 用户取消/断开,但取消前已经产生可靠 Usage | 按实际 Usage 扣费 | `outcome=canceled` 与金额同时保留 |
|
||
| 流式输出一部分后取消 | 按上游累计报告的实际 Usage 扣费 | 不能只按已发送到下游的 chunk 猜测 |
|
||
| 已触达上游但没有取得可靠 Usage | 不猜测、不虚构金额,暂记 0 | 标记`unmeasured` 并进入迟到 Usage 观察 |
|
||
|
||
取消自动免单会形成明显漏洞:调用方可以在最后一个流式事件前主动断开,从而反复使用已经由上游执行的计算。因此用户侧可以看到“已取消”和本次实际金额,但不能把取消理解为退款。
|
||
|
||
#### 6.3.1 取消时的处理顺序
|
||
|
||
1. 幂等接收 `request.complete`,保存 `outcome=canceled`;
|
||
2. 立即释放该请求的并发占用或预授权资源;
|
||
3. 封存此时已经取得的 Execution 与 Usage 快照;
|
||
4. 有可靠 Usage 时按同一价格快照正常结算;
|
||
5. 没有 Usage 时写入 `settlement_status=awaiting_usage`、`accounting_quality=unmeasured`,用户当前金额显示为 `$0`;
|
||
6. 保留有界的持久化关联窗口,等待可能迟到的 response/usage 事实;
|
||
7. 到期仍无 Usage 时转为 `unmeasured_final`,保留诊断和风险计数,绝不能伪造 Token。
|
||
|
||
第一版 `late_usage_ttl` 默认 24 小时,与持久化 pending/recovery 机制配合;到期只清理内存关联并把状态转为 `unmeasured_final`,不是“24 小时后永远免费”的边界。只要以后到达的 Usage 仍能用 RequestID/ExecutionID 可靠关联,仍可通过追加账本进行补记。
|
||
|
||
#### 6.3.2 迟到 Usage 与补记
|
||
|
||
回调不能假定同步、严格有序或只调用一次。终态后到达的新 Usage 不得直接修改已经提交的 BillingRecord 或 LedgerEntry,而应:
|
||
|
||
1. 以 `request_id + execution_id + usage_revision` 去重;
|
||
2. 对新的累计 Usage 生成新的计费版本;
|
||
3. 计算“新确认应收金额 - 已入账金额”的差额;
|
||
4. 差额为正时追加 `late_settlement` 账本;
|
||
5. 出现更小或矛盾的 Usage 时不自动退款,标记 `inconsistent` 等待管理员复核;
|
||
6. 原请求的展示投影汇总原账与补记账,账本历史保持不可变。
|
||
|
||
部分可确认 Usage 可以先按确认部分结算并标记 `partial`,迟到的新增部分仍走差额补记。重复回调、相同 revision 或相同幂等键必须产生零次新扣费。
|
||
|
||
#### 6.3.3 防止取消逃费
|
||
|
||
系统按 Key/账户统计以下诊断:
|
||
|
||
- canceled 请求数量与占比;
|
||
- `canceled + unmeasured` 数量与连续次数;
|
||
- 迟到补记金额;
|
||
- 取消发生时是否已经选择上游账户、收到首字或输出 chunk。
|
||
|
||
偶发无 Usage 不惩罚用户;高频、持续的 `canceled + unmeasured` 可以触发并发收紧、暂时限流或管理员告警。风控动作属于 Core 的准入策略,计费模块只提供事实和计数,不能凭猜测生成金额。
|
||
|
||
## 7. 提供给统计模块的数据
|
||
|
||
计费模块不承担详细分析,只发布足以支撑未来统计的标准事件。建议字段:
|
||
|
||
```go
|
||
type BillingEvent struct {
|
||
EventID string
|
||
IdempotencyKey string
|
||
RequestID string
|
||
ExecutionID string
|
||
CredentialID string
|
||
KeyPreview string
|
||
BillingAccountID string
|
||
PlanID string
|
||
CycleID string
|
||
Kind string // charge/late_settlement/refund/credit/adjustment
|
||
|
||
Provider string
|
||
ExecutorType string
|
||
Model string
|
||
ModelAlias string
|
||
AuthID string
|
||
AuthIndex string
|
||
Source string
|
||
Endpoint string
|
||
ReasoningEffort string
|
||
RequestedServiceTier string
|
||
ResponseServiceTier string
|
||
EffectiveServiceTier string
|
||
|
||
RequestedAt time.Time
|
||
CompletedAt time.Time
|
||
OccurredAt time.Time
|
||
BookedAt time.Time
|
||
Latency time.Duration
|
||
TTFT time.Duration
|
||
Failed bool
|
||
FailureStatusCode int
|
||
Outcome string // succeeded/failed/rejected/canceled
|
||
|
||
InputTokens int64
|
||
OutputTokens int64
|
||
ReasoningTokens int64
|
||
CacheReadTokens int64
|
||
CacheCreationTokens int64
|
||
TotalTokens int64
|
||
AccountingQuality string
|
||
|
||
Currency string
|
||
BaseAmountMicros int64
|
||
AppliedMultiplier string // 十进制定点文本,例如 "2.5"
|
||
BillingAmountMicros int64 // 当前 BillingRecord 的完整应收快照
|
||
SpendDeltaMicros int64 // 本事件对消费趋势的带符号变化
|
||
BalanceDeltaMicros int64 // 本事件对余额的带符号变化
|
||
PricingVersion string
|
||
PricingSource string
|
||
LongContext bool
|
||
SettlementStatus string // settled/awaiting_usage/unmeasured_final/corrected
|
||
UsageRevision int64
|
||
CorrectionOfEventID string
|
||
}
|
||
```
|
||
|
||
安全要求:统计事件不得包含明文 API Key、Bearer Token、上游凭证、原始请求体或失败响应中的敏感内容。
|
||
|
||
持久化层以 `EventID/IdempotencyKey` 去重,并在同一领域事务中把事件转换为带单调 `event_seq` 的 `ProjectionEvent`。统计模块聚合 `SpendDeltaMicros`;余额投影聚合 `BalanceDeltaMicros`。它可以展示 Token 和倍率,但不能根据 Token 重新计算或覆盖已经入账的金额。
|
||
|
||
## 8. 如何参考现有项目
|
||
|
||
### 8.1 `cpa-plugin-key-billing`
|
||
|
||
定位:实时金额额度控制和插件生命周期的首要参考。
|
||
|
||
优先阅读:
|
||
|
||
| 主题 | 源码位置 |
|
||
| ------------------- | ----------------------------------------------------------------- |
|
||
| 插件装配与 RPC 分发 | `cpa-plugin-key-billing/internal/plugin/app.go` |
|
||
| 请求准入与 Key 识别 | `internal/plugin/intercept.go`、`internal/billing/enforce.go` |
|
||
| Request/Usage 关联 | `internal/plugin/usage_tracker.go` |
|
||
| 上游 Usage 归一化 | `internal/plugin/upstream_usage.go` |
|
||
| 四段计价与长上下文 | `internal/billing/pricing.go` |
|
||
| 套餐、周期与绑定 | `internal/billing/plan.go`、`keys.go`、`account.go` |
|
||
| 并发安全与持久化 | `internal/billing/store.go`、`pending.go` |
|
||
| 管理 API 与 UI | `internal/plugin/management.go`、`admin.go`、`ui.html` |
|
||
| C ABI 入口 | `cmd/cpa-key-billing/main.go` |
|
||
|
||
适合直接吸收的设计:
|
||
|
||
- Key 摘要和脱敏,不保存明文;
|
||
- 准入周期快照,避免跨周期误扣;
|
||
- 请求、Usage 和终态关联;
|
||
- 先可靠结算能用 RequestID/response ID 关联的最终 Usage;其覆盖失败尝试的限制不能作为长期完整计费模型;
|
||
- 429 拒绝流程;
|
||
- 原子热重配、幂等关闭和无后台 goroutine 的插件约束;
|
||
- 长上下文阶梯价与不确定 Usage 的保守处理。
|
||
|
||
不能直接照搬:
|
||
|
||
- `float64` 作为余额和累计金额,必须改为整数定点金额;
|
||
- JSON 状态文件作为长期账本,目标实现应使用 SQLite 和不可变账本;
|
||
- 缺少 Fast/priority 计价;
|
||
- models.dev 自动价格不能直接视为权威;
|
||
- 当前简单累计统计不能成为统计模块基础;
|
||
- 固定 `SchemaVersion=2` 且忽略宿主 lifecycle `schema_version` 的实现;
|
||
- 无条件同时声明 response-before 与 stream chunk hook:这是为弥补 Usage 缺少 RequestID/WebSocket 同协议透传的过渡关联方案,会把 JSON/base64/C ABI 成本放到每个流式帧;
|
||
- 仅把 schema 改成 3 就宣称性能问题解决:schema 3 不会消除 response-before 请求体和 `HistoryChunks` 的重复传输。
|
||
|
||
### 8.2 `cpa-usage-keeper`
|
||
|
||
定位:持久化、Token 语义修正、价格快照、条件倍率以及未来统计模块的首要参考。
|
||
|
||
计费模块优先阅读:
|
||
|
||
| 主题 | 源码位置 |
|
||
| ---------------- | -------------------------------------------------------- |
|
||
| 四段费用计算 | `cpa-usage-keeper/internal/helper/usage_cost.go` |
|
||
| Token 语义修正 | `internal/service/tokenprocessor/` |
|
||
| 价格快照与校验 | `internal/pricing/snapshot.go`、`catalog.go` |
|
||
| 条件倍率 | `internal/pricing/fields.go`、`resolver.go` |
|
||
| 价格持久化与规则 | `internal/repository/pricing.go`、`pricing_rules.go` |
|
||
| models.dev 同步 | `internal/service/pricing_metadata_sync.go` |
|
||
| Usage 事件实体 | `internal/entities/usage_event.go` |
|
||
|
||
未来统计模块重点参考:
|
||
|
||
- `internal/repository/usage_*`:SQLite 事件、实时/小时/天聚合;
|
||
- `internal/service/usage.go`:Overview、Activity 和 Analysis 服务;
|
||
- `internal/api/usage_*`:查询接口;
|
||
- `web/src/components/usage/`:请求、Token、费用、健康和延迟展示;
|
||
- `web/src/features/ranking/`:排名能力,是否纳入产品需另行决定。
|
||
|
||
适合吸收的设计:
|
||
|
||
- SQLite 和迁移体系;
|
||
- Provider 感知的 Token 归一化;
|
||
- 原子发布的只读价格快照;
|
||
- `service_tier` 等条件倍率;
|
||
- 价格修改后各查询路径使用一致快照;
|
||
- 完整的统计维度和前端组件体系。
|
||
|
||
不能直接照搬:
|
||
|
||
- 价格规则同时命中多个字段时全部相乘,Fast 请求/响应 tier 必须先合并成唯一有效 tier;
|
||
- 当前自动价格同步忽略长上下文 tiers;
|
||
- models.dev 候选价格必须经过官方表或管理员确认;
|
||
- Keeper 是独立服务,后台 worker、定时器和运行模型不能原样搬入 c-shared 插件;
|
||
- 统计查询时动态重算费用的行为不能改变已经落账的历史金额。
|
||
|
||
### 8.3 复用规则
|
||
|
||
两个参考项目均使用 MIT License。复制或修改核心代码时必须:
|
||
|
||
- 保留相应版权与许可证声明;
|
||
- 在仓库 NOTICE/THIRD_PARTY 文档中记录来源文件和上游提交;
|
||
- 优先按领域模块移植并补测试,不做无法追踪来源的大段拼贴;
|
||
- CLIProxyAPI ABI、RPC DTO 和能力名称始终以目标 `CLIProxyAPI` 源码为准,两个下游项目不能覆盖宿主契约。
|
||
|
||
当前检查基线:
|
||
|
||
- `cpa-plugin-key-billing`: `25b534ae386f830f537cca9215cff5586e630b3a`
|
||
- `cpa-usage-keeper`: `d62cad3f345ae574089a14a4ac75cca023c7ead6`
|
||
|
||
## 9. 验收标准
|
||
|
||
计费模块完成必须至少证明:
|
||
|
||
- 用户所有额度、余额、扣费和账单均以金额显示;
|
||
- 余额与账本使用整数定点金额,无浮点累计误差;
|
||
- 普通输入、缓存读、缓存写和输出不会重复计价;
|
||
- Reasoning 不会因字段语义误判而重复计价;
|
||
- GPT-5.6 Sol/Terra/Luna 使用经过确认、带版本的价格;
|
||
- Fast/priority 只应用一次 2.5×;
|
||
- 长上下文与 Fast 可以正确组合;
|
||
- 本地拒绝不收费,失败或取消只按可确认的实际上游 Usage 收费;
|
||
- 取消但没有可靠 Usage 时不猜费,并能记录 `unmeasured`;
|
||
- 迟到 Usage 通过幂等追加账本补记,重复 Usage 不会重复扣费;
|
||
- 重试产生的每个**可可靠关联**上游 Execution 用量都能独立去重和结算;当前 CPA 无 RequestID 的 attempt usage 必须标为限制/`unmeasured`,不能猜配;
|
||
- 延迟完成不会扣到错误周期;
|
||
- 额度耗尽时稳定拒绝新请求;
|
||
- 明确为软额度,默认 BillingAccount 并发 1,最后一个在途请求可能形成有限负余额;
|
||
- Credential secret 轮换不创建新 BillingAccount/cycle,也不刷新额度;
|
||
- 相同 canonical Usage 重放不增加 revision,多来源不会重复相加;
|
||
- 重启后账本、周期和余额一致;
|
||
- 统计事件足以支持后续统计模块,且不含敏感信息;
|
||
- 实际动态库通过单元测试、竞态测试和 CLIProxyAPI 端到端加载测试。
|
||
|
||
## 10. MVP 产品默认值
|
||
|
||
为避免实现阶段再次产生不同口径,第一版固定:
|
||
|
||
1. 生产未绑定账户/套餐/价格一律拒绝;observe-only 只允许显式开发/迁移模式;
|
||
2. 周期从第一次成功准入开始,按 plan 的固定时长推进;不实现自然月/统一账单日;
|
||
3. 允许管理员充值或扣减,但必须追加 credit/adjustment 账本和审计原因;
|
||
4. 用户可以查看自己的逐请求时间、模型、状态和最终金额,也可以看周期汇总;
|
||
5. 负余额按真实负金额展示,并说明软额度语义,不伪装成 `0.00`;
|
||
6. 结算币种只支持 USD。
|