Files
litellm-config/docs/volc-quota-notes.md
T
vibe-code 23778a8f7c feat: Snapshot-Offset 限额策略替换 BUFFER 盲猜
核心改动:不再用 BUFFER 猜滞后窗口消耗,而是用同步时点快照
 直接计算累计限额。

- BUFFER=300000 → SMALL_BUFFER=50000(仅覆盖非消耗性跳降)
- get_effective_limit: limit = B0 - SMALL_BUFFER,不依赖 current_usage
- 同步循环:写 balance_at_sync + usage_at_sync 替代 remaining/stable_limit
- 突发检测/低额度判断:读 balance_at_sync 替代 remaining
- volc_consume.py: vls.BUFFER → vls.SMALL_BUFFER
- debug/debug_volc_sync.py: 同步更新
- docs/volc-quota-notes.md: 新增 §7.6 v6 设计文档,更新各节口径
2026-08-12 12:22:22 +00:00

280 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.
# 火山免费额度机制速查(VolcEngine Quota Notes)
> 日期:2026-08-12
> 用途:后续限额/防扣费设计、排障、消耗任务的事实基础,避免重复推导
> 来源:
> - `merge-auto-consume.md` §1(2026-08-11 用户确认的业务细则)
> - 2026-08-12 对话澄清(扣费口径、余额 API 滞后、限额方案演进)
> - 2026-08-12 生产日志观察(deepseek-v4-flash-ga-260731)
>
> 代码现状:v6 = Snapshot-Offset(`limit = balance_at_sync + usage_at_sync − SMALL_BUFFER`),
> 2026-08-12 修改;BUFFER 盲猜法已废弃,见 §7.6。
---
## 1. 业务模型(细则,用户确认)
### 1.1 日返还循环
```
每天任意时刻 真实用户 + 消耗任务 的消耗 = 当日采集量(火山实时统计,每模型单独计算)
每天 23:00 CST 消耗任务补齐缺口(可选)
次日 00:00 起 火山按昨日采集量发放资源包(不定时,可能延迟到上午)
```
### 1.2 硬规则
| 规则 | 值 | 影响 |
|---|---|---|
| 采集上限 | 2M token/模型/天 | 消耗超过 2M 的部分**不返还**(白烧);所有模型统一 |
| 发放额度 | = 昨日采集量(≤2M) | 消耗 X → 次日返还 X,用多少补多少,延迟一天到账 |
| 发放时间 | 不定时 | 次日 0:00 至发放时点存在余额空窗 |
| 资源包有效期 | 30 天 | 过期额度实打实消失,失去"启动资金" |
| 消耗顺序 | FIFO(先消耗获得时间早的包) | 队列头部 = 最早获得 = 最早过期 |
| 余额耗尽 | 请求不失败,按真实价格扣费 | 全部防护的终极目标:避免扣费 |
| 首次开通 | 每模型赠送 2M | 启动资金的种子(无对应前日采集的免费午餐) |
### 1.3 账目守恒
```
余额_{N+1} = 余额_N − 消耗_N + 返还_{N+1}
= 余额_N − 消耗_N + 采集_N
= 余额_N (消耗即采集,≤2M 内全额返还)
```
推论:
- **使用不损失额度**(用多少补多少,仅延迟一天)
- **过期才损失额度**(额度蒸发 = 启动资金永久减少)
- 消耗任务的价值 = 防止"启动资金"被 30 天过期侵蚀
- 真实用量 ≥ 66.7k/天(= 2M/30 天)时,赠送包被自然消耗;低于此值必须靠消耗任务在最后 9 天窗口洗掉
---
## 2. 扣费判定(2026-08-12 用户确认口径,最重要)
> 用户原话:**是否扣费只取决于当下有多少免费额度。超出免费额度的请求,都会被扣费。采集量只决定明日发放,与今日是否扣费无关。**
```
扣费判定:消耗 ≤ 当下余额(免费额度)→ 免费,余额扣减
消耗 > 当下余额 → 超出部分按真实价格扣费(无返还、不退)
今日采集量(≤2M)→ 只决定明日返还的包大小,与今日是否扣费无关
```
推论(2026-08-12 案例):
- 当日余额 1,558,307,今日已采 1,696,135 → 超出 **137,828 tokens 已扣费**,不可挽回
- 明日返还 = 今日采集量 1,696,135 的包(免费额度回正),但今天扣的钱不退
- 因此「先采后返」**不构成透支许可**:消耗超余额的部分当场计费
---
## 3. 关键概念区分(反复踩坑处)
| 概念 | 含义 | 量级特征 | 与扣费的关系 |
|---|---|---|---|
| 采集量(今日) | 今日实时消耗统计(火山网页"今日采集量") | 单日流量,0~2M | 只决定明日返还 |
| 余额(免费额度) | 资源包 AvailableAmount 存量之和 | 累计存量,守恒 ≈2M(种子) | **扣费唯一依据** |
| 采集配额 2M | 今日最大采集量 | 每模型固定 | 超了白烧(不返还),未必当场扣费 |
| 返还(T+1) | 昨日采集量 → 新资源包 | 单日流量延迟一天 | 补回余额,不抵今日扣费 |
**核心:余额是累计存量(守恒),采集量是单日流量。限额设计必须用存量(余额),
绝不能用单日流量(昨日采集量)当上限——那会把可用性锁死在水位以下(见 §7.2 v3)。**
---
## 4. 余额 API 的滞后问题(2026-08-12)
- **非实时,延迟未知**(用户明确)。实测:2026-08-12 13:36:01 同步快照 Bal=1,558,307,
此后 usage 从 1,241,821 涨到 1,696,135(13:36→13:38)期间 Bal 纹丝不动。
- 观察结论:AvailableAmount 不随今日消耗实时递减(至少滞后 20 万级,上限未知)。
- **后果**:一切「余额随消耗实时递减」的公式(v2 的 `usage + balance` 池恒定假设)在
滞后下失效——usage 涨而 balance 不动 → 池膨胀 → 限额虚高 → 放行到扣费区。
- 最终对策:Snapshot-Offset(§7.6),不再依赖 `usage + balance` 守恒,而是用同步时点
的快照 `balance_at_sync + usage_at_sync = B0` 直接计算累计限额,两次同步间常数,
实时 usage 自然抵消滞后窗口内的消耗。
---
## 5. 症状对照:Today > Rem(2026-08-12 案例)
日志示例(13:36-13:38,deepseek-v4-flash-ga-260731):
```
DEBUG: Checking deepseek-v4-flash-ga-260731 (Bal: 1,558,307, Rem: 1,258,307) (余额/剩余)
DEBUG: deepseek-v4-flash-ga-260731 Pool=2,800,128 | Usage=1,241,821 | StaticLimit=2,000,000 | StableLimit=2,500,128 | EffectiveLimit=2,000,000
Set Redis limit:dynamic:volcengine:remaining:deepseek-v4-flash-ga-260731 = 1,258,307
📊 [Usage] deepseek-v4-flash-ga-260731:ba0383d3: +38083 (Today: 1,279,904 | Rem: 1,558,307 | Limit: 2,000,000)
```
字段口径:
| 字段 | 含义 | 计算 |
|---|---|---|
| Bal | 火山 API 余额(ListResourcePackages AvailableAmount 之和) | 同步快照 |
| BalAtSync | 同步时写入 Redis 的余额快照 | = Bal |
| UsageAtSync | 同步时写入 Redis 的累计消耗 | Redis `daily_usage` 求和 |
| Today | 本地今日采集量 | Redis `daily_usage` 逐请求累加 |
| Limit | 生效限额 | min(2M, balance_at_sync + usage_at_sync − SMALL_BUFFER) |
**Today > BalAtSync 的原因**:Today 是单日流量,BalAtSync 是存量余额快照,两套口径本就不该比较。
火山网页「今日采集量 1,696,135/2,000,000」对应 Today 与 2M 配额,与余额无关。
**扣费结论(2026-08-12 15:09 实测修正)**:14:59:51 同步 U=1,696,135 + B=596,869,
守恒反推今日初始免费池 B0 = 2,293,004 → 今日消耗 1,696,135 < B0 → **当日无扣费**。
此前「超余额 13.8 万已扣费」的推断基于 13:36 滞后快照(B=1,558,307 高估真实剩余
约 50 万),不成立。判定扣费要用「守恒初始池」(U+B 反推)而非任意时点快照。
---
## 6. Redis key 与日志口径(速查)
### 6.1 Redis key
| key | 读写方 | 说明 |
|---|---|---|
| `daily_usage:{北京日期}:{model}:{hash}` | LiteLLM 成功回调 INCR + 消耗任务 INCR | 当日采集量,TTL 48h,北京 00:00 滚动 |
| `limit:dynamic:volcengine:balance_at_sync:{model}` | 守护线程写 | 同步时点 API AvailableAmount 快照 |
| `limit:dynamic:volcengine:usage_at_sync:{model}` | 守护线程写 | 同步时点 Redis 累计消耗快照 |
| `consume:last_run:{日期}` | 消耗任务 | 23:00 当日去重,TTL 48h |
### 6.2 关键日志
- `🚫 [PRE-BLOCK] {model} daily limit reached ({usage}+{reserve}/{limit})`:请求前闸门 429
- `📊 [Usage] {model}:{hash}: +{tokens} (Today: X | BalAtSync: Y | Limit: Z)`:成功记账
---
## 7. 限额方案演进
### 7.1 v2(HEAD c6c1224 原式,v5 已恢复):`stable_limit = usage + balance − BUFFER`
- 意图:`usage + balance` = 当日初始免费池(守恒 B0),消耗中恒定 → 限额恒定
- **数学正确性(2026-08-12 15:09 实测确认)**:B 滞后收敛后确实满足 B = B0 − U
(14:59:51:U=1,696,135 + B=596,869 = B0=2,293,004,与理论剩余 597,004 几乎一致)。
同步时 stable = B0 − BUFFER,usage 累计撞线 = 用到剩 BUFFER 停止 —— 语义正确
- **残留风险(非公式错误,是快照时点问题)**:同步时 B 若滞后(13:36 高估真实剩余
约 50 万)→ stable 虚高 → 多放行滞后窗口内消耗。由 BUFFER + 低余额加密同步
(600s/300s)+ burst 触发覆盖;收敛后池回到守恒值
- 案例:13:36 快照 B=1,558,307 为滞后值(真实剩余 ≈1,051,183),并非「余额不足」;
今日无扣费(见 §5)
### 7.2 v3(尝试后否决,已回滚):昨日锚 `effective = min(2M, 昨日采集量 × 系数)`
- 思路:把「返还 = 昨日采集量」当作「今日可采上限」
- **否决理由(用户反例)**:余额 2M、昨日用量 10K → 今日最多 10K?荒谬。
错误在于把**单日流量(昨日采集量)当存量(余额)** 用;昨日少用不损余额(守恒),
今日可用 = min(2M, 余额) ≈ 2M,与昨日无关
- 附:系数 <1.0 还会引发「今日少采 → 明日返还更少」的日复一日递减陷阱(0.9 系数 5 日降至 66%)
### 7.3 v4(2026-08-12 实施后当天发现错误,已回退):冻结快照
```
stable_limit = max(0, balance − BUFFER) # ❌ 错误:把「剩余余额」当「从 0 累计的上限」
```
- 设计初衷:余额 API 滞后 → 冻结快照防池膨胀
- **错误实证(15:09:51 生产日志)**:Bal=596,869(真实剩余 59.7 万),
stable = 296,869,usage=1,696,135 ≥ 296,869 → `[PRE-BLOCK] … (1,696,135+16/296,869)` 误拦。
账上有真实免费额度却全拦 —— 因为 stable 应是「今日累计上限」(= B0 − BUFFER),
而 balance 是「剩余」(= B0 − U);U>0 时两者相差 U,直接替换导致低估、误拦
- 仅在同步时点 U=0(日初/刚返还)巧合正确(doubao 场景 stable=2.2M 无碍)
- 教训:**「剩余额度」不能当「累计限额」用**;两者通过 `usage + balance` 桥接(v5)
### 7.4 待观察项(2026-08-12)
- **BUFFER(现 30 万)**:防「同步时点 B 滞后窗口内的消耗」。13:36 实测高估约 50 万
> 30 万 BUFFER → 若滞后窗口内流量大,v2 会多放行到虚高 stable;可调大(如 50 万,env)
- **部署**:v5 已改(工作区),需重启 litellm 容器加载;重启在生产主机由用户执行
### 7.5 v5(2026-08-12 修正,已废弃):恢复 v2 骨架
```
stable_limit = max(0, usage + balance − BUFFER)
```
- 与 v2(§7.1)公式相同,区别在**论证与认识**:v2 数学正确(B 收敛后 = B0 − U),
不需要冻结快照;v4 的「剩余当上限」误拦由「usage + balance 桥接」天然避免
- 实测验证(14:59:51 同步 + 15:09:51 生产日志):U=1,696,135、B=596,869 →
stable=1,993,004 → 今日剩余可用 296,869(账上 59.7 万用到剩 30 万 BUFFER)✓
- 防扣费语义不变:usage 撞 stable = 剩余恰为 BUFFER 时停,扣费结构性杜绝;
balance < BUFFER(U=0)→ stable=0 → 全拦
- **已废弃原因**:BUFFER 盲猜滞后窗口消耗,高强度击穿低强度浪费。被 §7.6 Snapshot-Offset 替代。
### 7.6 v6(Snapshot-Offset,2026-08-12 当前):宝塔防扣费
```
limit = balance_at_sync + usage_at_sync − SMALL_BUFFER(5万)
= B0 − SMALL_BUFFER
```
#### 核心思路
不再猜滞后消耗,而是利用一个事实:**同步时点的 balance + usage = 当日初始免费池 B0(B0 是常数,一天内不变)**。
**limit 是累计限额(从 0 到上限),不是剩余余额**。`_check_request_block` 用 `current_usage + reserve >= limit` 撞线,`current_usage` 自然抵消了滞后窗口内的增量消耗。
#### 为什么不需要 BUFFER
旧方案用 BUFFER 猜「滞后窗口内消耗了多少」——猜不准。新方案直接记录同步时点的 `balance_at_sync` 和 `usage_at_sync`,闸门读实时 `current_usage` 去减,精确抵消:
```
# 旧:盲猜
limit = usage + balance - BUFFER(300k)
# 新:精确抵消
limit = balance_at_sync + usage_at_sync - SMALL_BUFFER(5k)
```
**SMALL_BUFFER(5 万)只覆盖非消耗性余额跳降**(资源包过期、API 未结算残差),不是消耗量。
#### 不变量
**永远不扣费**:`usage + reserve >= B0 - SMALL_BUFFER` 撞线时,剩余实时余额 = `B0 - usage` = `B0 - (B0 - SMALL_BUFFER - reserve)` = `SMALL_BUFFER + reserve` > 0。扣费在余额归零时才发生,结构性杜绝。
#### 执行流程
**同步线程**(每 5~30min / 突发触发):
1. 拉 API → 取到 balance
2. 读 Redis 取 current_usage
3. 写入 `balance_at_sync` = balance, `usage_at_sync` = current_usage (TTL 24h)
4. 不再写入 `remaining`/`stable_limit` 旧键
**请求闸门**:
1. 读 `balance_at_sync` + `usage_at_sync`
2. `limit = balance_at_sync + usage_at_sync - SMALL_BUFFER`
3. `usage + reserve >= limit` → 429
#### 与旧方案对比
| 场景 | 旧(BUFFER 30万) | 新(Snapshot-Offset) |
|---|---|---|
| 高强度消耗 | 滞后窗口内消耗 > 30 万 → 击穿扣费 | 实时 usage 撞 B0 线,不放行 |
| 低强度消耗 | 浪费 30 万额度(实际只用了 5 万) | 只留 5 万 SMALL_BUFFER |
| 新包到账 | 下次同步 stable limit 跳升 | 下次同步 B0 跳升,一致 |
| 包过期 | 余额跳降,limit 偏高至下次同步 | 同左,但 SMALL_BUFFER=5 万风险窗口小 |
#### 遗留风险
1. **同步时点 B0 本身滞后**:API 的 AvailableAmount 在同步时点就已经滞后了 L0。这个量无法通过抵消消除。唯一缓解:同步频率(低余额 5min) + 突发检测。
2. **多包同时过期 > SMALL_BUFFER**:概率低,无法绝对防御。可调大 SMALL_BUFFER 或监控包过期时间。
---
## 8. 排障速查(今日结论汇总)
1. 看到 Today > BalAtSync 不是异常,先分清口径(§5);判扣费用「守恒初始池」(BalAtSync+UsageAtSync 反推),
不要用任意时点快照(13:36 滞后快照曾误判「超余额扣费」,实际无扣费)
2. 余额显示不动 ≠ 余额充足,是 API 滞后;Snapshot-Offset 不再依赖 BUFFER 覆盖,而是直接用
`balance_at_sync + usage_at_sync - current_usage` 实时抵消
3. 判断「还能免费采多少」:`balance_at_sync + usage_at_sync - SMALL_BUFFER - 今日已采`(v6)
4. 消耗任务 23:00 直连 Ark 绕过 LiteLLM 闸门(防自杀),其自身 clamp:`plan ≤ 2M − usage`、
`plan ≤ 余额快照 − SMALL_BUFFER`,停止条件三闭环
5. 余额滞后不改变 B0 守恒;30 天过期蒸发是余额下跌的另一来源,
与消耗扣减区分(如 08-12 余额 1,558,307 → 596,869,其中约 50 万为滞后修正 + 过期)## 9. 待确认/风险清单
- [ ] 余额 API 滞后上限实测(决定是否需要进一步压缩 SMALL_BUFFER 或保留 5 万)
- [x] v4 误拦发现与回退(2026-08-12 15:09 生产日志实证,§7.3)
- [x] v5 废弃(2026-08-12,§7.5;被 v6 Snapshot-Offset 替代)
- [x] v6 实施(2026-08-12,§7.6;待生产主机重启生效)
- [ ] 消耗任务口径同步收紧(已改为 vls.SMALL_BUFFER,确认与闸门一致)
- [ ] 空窗期(0:00–返还到账)行为观察:v6 下 B0 自动收紧,确认无异常误杀
- [ ] 数据库泄漏:512 亿 token 消耗记录(08-12 一天 deepseek-v4 烧了 512 亿)导致 Redis 内存膨胀;触发监控/清理机制