mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
fix(billing): 缓存命中按全价计 + 未定价流量静默记 0
部署前审计计费插件时自己找到的两个真缺陷,都会直接算错钱。
## ★ 缺陷 1:缓存命中按全价计(高估约 10 倍)
costFor 只看 prompt_tokens,不区分其中多少是缓存命中。实测(审计脚本,非推演):
1M prompt token 里 900k 是 cache_hit → **算出 10 USD**,而缓存读通常只要 1/10
价,正确值 ~1.9。agent 流量反复重放长前缀,正是缓存要让它便宜的那类流量,所以
这个偏差恰好落在最高频的流量上。
改为拆分:
fresh = prompt_tokens - cache_hit_tokens → 全价
cached = cache_hit_tokens → 全价 × cache_discount
cache_discount 默认 0.1(DeepSeek/Qwen/Kimi 的量级),可按条目覆盖——**折扣率是
每个 provider 的事实、不是自然常数**,所以 0.1 只是默认值而不是硬编码常量。
另外把 cache_hit 钳到 prompt 以内:适配器报出比 prompt 还大的缓存命中数时,
fresh 会变负数,凭空产生负计费 token。
## ★ 缺陷 2:未定价模型静默记 0(最危险)
没有任何价目覆盖的请求,成本记 0,而 **requests 和 token 数照常计入 total**。
于是账单看起来完全正常,只是 quietly 少报——没有任何报错,没有任何异常。
比多算危险得多:多算你会去查,少算你不会知道。
新增两个维度把这件事变成显式信号:
unpriced_reqs 未定价请求数
unpriced_models 按模型点名,直接告诉你价目表缺哪一行
仪表盘加一张 "Unpriced" 卡片,**这个数应该是 0**。
任何维度(source / model / key)覆盖了就算 priced。
## 修这两个时自己踩的坑
第一版把未定价统计块写在了 `local s = plugin.state` **之前十行**,
在一个全新插件上 hook 直接抛 "attempt to index global 's'",于是
**整条请求什么都没记**——计费插件能有的最坏失败方式。
是 TestBillingZeroPricesIsSafe 的 "requests = 0" 抓到的。
代价:一个计费插件静默失效,而网关日志里只有一行 hook error。
## 判据(351 个测试全绿,计费相关 16 个)
新增 5 个,全部是**具体金额**断言:
TestBillingCacheHitsAreDiscounted 1M/900k 命中 → 1.9
TestBillingCacheDiscountIsPerModel 覆盖为 0 / 1 两种极端
TestBillingCacheHitClampedToPrompt 荒谬的命中数不产生负费用
TestBillingCountsUnpricedTraffic 只数未定价的那个,且流量仍计入 total
TestBillingAnyDimensionCountsAsPriced 源维度定价也算 priced
This commit is contained in:
@ -337,6 +337,7 @@ PUT /api/plugins/{name}/state 替换状态(admin)
|
||||
|
||||
**token 价**优先级:`keys` > `models` > `default`。
|
||||
**`per_request` 固定价是叠加的**(不覆盖),所以一个生图模型可以既算 token 又收固定费。
|
||||
**`cache_discount`** 见 §7.7。
|
||||
|
||||
### 7.2 价格单位
|
||||
|
||||
@ -408,7 +409,49 @@ curl -H "Authorization: Bearer $ADMIN_KEY" \
|
||||
理由:两套独立的会计路径如果对不上,比一套功能略少的更糟。计费是**观察**,
|
||||
配额是**控制**,二者分开。
|
||||
|
||||
### 7.6 数据从哪来
|
||||
### 7.6 未定价流量(重要)
|
||||
|
||||
**任何维度都没配价的请求,成本记 0。** 这是最危险的失败模式:账单照样能加总,
|
||||
只是**悄悄少报**,而且没有任何报错。
|
||||
|
||||
所以插件单独统计它们:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|---|---|
|
||||
| `unpriced_reqs` | 没有任何价目覆盖的请求数 |
|
||||
| `unpriced_models` | 按模型点名(`{"MYSTERY-MODEL": 12}`)——直接告诉你价目表缺了哪一行 |
|
||||
|
||||
仪表盘上有 "Unpriced" 卡片。**这个数应该是 0**;不是 0 就去补价目。
|
||||
|
||||
注意"未定价"不等于"免费":这些请求的 `requests` / token 数**照常计入**
|
||||
`total` 与各维度,只有金额是 0。
|
||||
|
||||
### 7.7 提示缓存计价
|
||||
|
||||
**缓存命中的 prompt token 不按全价算。** 绝大多数 provider 对缓存读给很深的折扣
|
||||
(常见是 1/10),而 agent 流量会反复重放长前缀——正是缓存要让它便宜的那类流量。
|
||||
|
||||
```
|
||||
fresh = prompt_tokens - cache_hit_tokens → 全价
|
||||
cached = cache_hit_tokens → 全价 × cache_discount
|
||||
```
|
||||
|
||||
`cache_discount` 默认 **0.1**(10 倍折扣),因为 DeepSeek / Qwen / Kimi 等都是这个
|
||||
量级。它是**每个 provider 的事实、不是自然常数**,所以可以按条目覆盖:
|
||||
|
||||
```json
|
||||
"models": { "gpt-5.4": { "prompt": 1.25e-6, "completion": 1e-5, "cache_discount": 0.25 } }
|
||||
```
|
||||
|
||||
设成 `1` 恢复成旧的"prompt 一律全价"行为,设成 `0` 表示该 provider 不打折。
|
||||
|
||||
优先级与 token 价一致(`keys` > `models` > `default`)。
|
||||
|
||||
> **这一条改过行为。** 修复前缓存命中按全价算:100 万 prompt token 里 90 万是
|
||||
> 缓存命中,会算出 10 USD 而不是 ~1.9——**高估约 10 倍**,而且恰好发生在缓存
|
||||
> 最有价值的高频流量上。
|
||||
|
||||
### 7.8 数据从哪来
|
||||
|
||||
`request_end` 的 `prompt_tokens` / `completion_tokens` 优先取上游真实的
|
||||
`usage`;上游没报时网关用字节估算(`len/3+1`)。流式请求在流结束后用上游真实
|
||||
|
||||
Reference in New Issue
Block a user