Files
litellm-config/docs/merge-auto-consume.md
T

352 lines
21 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.
# 火山免费额度自动消耗:auto_consume 合入 volc_limit_sync 设计方案
> 日期:2026-08-11
> 背景:`/root/workspace/huoshan-api-auto_consume/`(自动消耗脚本)与 `/root/workspace/litellm-config/`(LiteLLM 限流回调 + 火山余额同步)合并,使每日 23:00 自动消耗免费额度,防止资源包过期蒸发,避免真实扣费。
> 前置文档:《merge-volc-limit-into-newapi.md》(2026-08-05,方向相反:把 volc_limit_sync 功能迁往 new-api,未实施)。
---
## 一、火山免费额度业务模型(一切设计的事实基础)
### 1.1 日返还循环
```
每天任意时刻 真实用户 + 消耗任务 的消耗 = 当日采集量(火山实时统计,每模型单独计算)
每天 23:00 CST 消耗任务补齐缺口(可选,见 §4)
次日 00:00 起 火山按昨日采集量发放资源包(不定时,可能延迟到上午)
```
### 1.2 五个硬规则(用户确认)
| 规则 | 值 | 影响 |
|---|---|---|
| 采集上限 | **2M token/模型/天** | 消耗超过 2M 的部分**不返还**(白烧);所有模型统一 |
| 发放额度 | = 昨日采集量(≤2M) | 消耗 X → 次日返还 X,用多少补多少,延迟一天到账 |
| 发放时间 | **不定时** | 次日 0:00 至发放时点存在余额空窗 |
| 资源包有效期 | **30 天** | 过期额度实打实消失,失去"启动资金" |
| 消耗顺序 | **FIFO(先消耗获得时间早的包)** | 队列头部 = 最早获得 = 最早过期(效期统一 30 天时两者等价) |
| 余额耗尽 | **请求不失败,按真实价格扣费** | 全部防护的终极目标:**避免扣费** |
| 首次开通 | **每模型赠送 2M** | 启动资金的种子(无对应前日采集的免费午餐) |
### 1.3 关键账目:启动资金守恒
```
余额_{N+1} = 余额_N − 消耗_N + 返还_{N+1}
= 余额_N − 消耗_N + 采集_N
= 余额_N (消耗即采集,≤2M 内全额返还)
```
**推论**:
- **使用不损失额度**(用多少补多少,仅延迟一天)
- **过期才损失额度**(额度蒸发 = 启动资金永久减少)
- 消耗任务的价值 = 防止"启动资金"被 30 天过期侵蚀,把额度滚成新包永续循环
- 真实用量 ≥ 66.7k/天(= 2M/30 天)时,赠送包被自然消耗,脚本自动闲置;低于此值必须靠脚本在最后 9 天窗口洗掉
### 1.4 扣费风险链(限流系统的真正职责)
```
免费额度耗尽(余额 = 0) → 继续请求 → 真实扣费
```
触发扣费的两种路径:
1. **真实用户单日用量 > 当日可用余额**(含空窗期 0:00–发放时点余额不足)
2. **消耗任务烧穿余额**(脚本自身引入的扣费,必须结构性杜绝)
---
## 二、两项目现状与合并可行性
### 2.1 当前项目 `volc_limit_sync.py`(1536 行,LiteLLM 回调 + 常驻守护)
| 能力 | 实现 |
|---|---|
| 火山余额同步 | SigV4 签名调 `ListResourcePackages`(`Status: Effective` 过滤 + `seen_tokens` 防分页死循环),`normalize_volc_name` 归一化,按监控模型聚合 |
| 动态限额 | Peak-Buffer:`PeakPool = 当日用量 + 余额`;`stable_limit = PeakPool − 15% margin(下限 25w)`;与静态 `HARD_DAILY_LIMITS` 取小得 `effective_limit` |
| 当日用量计数 | LiteLLM 成功回调 `INCRBY total_tokens` → Redis `daily_usage:{北京日期}:{model}:{api_base_hash}`,北京时间 00:00 滚动,TTL 2 天 |
| 超限停用/恢复 | `soft_evict_from_router` 重命名软踢;北京时间 7–13 点每 30 分钟、其余 4 小时检查恢复;`startup_audit` 启动预踢 |
| 模型发现 | 解析 `config.yaml` 中 `volcengine/` 前缀 + `HUOSHAN_BASE_URL` 条目 → `MONITORED_MODEL_PAIRS`;版本号自动推导别名 + `MANUAL_MONITORED_MODEL_ALIASES` 手工覆盖 |
### 2.2 目标项目 `huoshan-api-auto_consume/`(420 行 + 4 辅助)
| 能力 | 实现 |
|---|---|
| 语料 | `/var/log` 系统日志分组 + 脱敏(IP/MAC/邮箱/路径)+ 不足时合成日志 |
| 计划 | `calculate_plan`:9 天窗口内包 `remaining/ceil(days_left)×1.1`,最后 24h 冲刺全量,多包相加 |
| 执行 | 直连 Ark API(`ark.cn-beijing.volces.com/api/v3/chat/completions`),速率伪装(15–40s 随机),指数退避,6h 上限,连续失败 5 次跳过 |
| 配置 | `config.py`:`MONITOR_TARGETS`(id/keyword/alias/extra_params) |
### 2.3 可复用资产与冲突点
**重叠(合并即消除重复)**:
- SigV4 签名(`volc_signer.py` ≈ `get_signed_headers`)——**同一账户同一组 AK/SK**
- `fetch_all_packages`(目标版缺 `seen_tokens` 防循环保护,保留当前版)
- 资源包→模型映射(当前版 `normalize_volc_name` + 别名体系更健壮,目标版 `keyword` 模糊匹配退役)
**关键事实:模型配置天然单一化**。`config.yaml` 的 volcengine 上游模型名(剥掉 `volcengine/` 前缀)就是 Ark endpoint id,与 `MONITOR_TARGETS` 的 id 一一对应(`deepseek-v4-flash` 的 `-ga-260731` vs `-0731` 差异已由 `MANUAL_MONITORED_MODEL_ALIASES` 覆盖)→ **`MONITOR_TARGETS` 退役,消耗任务直接由 config.yaml 驱动**。
**冲突点与解法**:
1. 限额 vs 消耗:消耗请求**必须直连 Ark,绝不改走 LiteLLM**(否则被 `quota_interceptor` 拦下自杀)
2. 消耗量**必须计入 `daily_usage`**(手动 INCR 同 key)——空窗期防扣费闭环的关键,见 §5.3
3. 脚本自身不能烧穿余额(结构性防扣费,见 §4.4)
---
## 三、消耗策略设计
### 3.1 为什么不做"烧满 2M"
API 请求的 token 消耗不可精确控制(输入按字符估算、输出不确定)。若目标 = 补满 2M:
- 烧超采集上限的部分不返还 → 白烧
- 烧到余额 0 仍继续 → **直接扣费**
- 精确校准需要逐请求调整 → 复杂度不可接受
### 3.2 目标:洗掉"即将过期的额度"(过期风险驱动)
```
消耗量 = Σ(该模型 ExpiryTime − now ≤ MONITOR_DAYS 的资源包剩余额度)
```
- 消耗量 ≤ 快过期包剩余 ≤ 当前可用余额 → **结构上不可能烧穿余额**(估算偏差只决定多烧/少烧几笔请求,多烧部分来自非过期包,次日照常返还,无害)
- FIFO 先烧最早获得 = 最早过期的包 → 脚本烧的恰好是最该洗的包,顺序自动正确
- 不追求 2M 精确值 → 无校准问题
### 3.3 均分逻辑(保留旧 `calculate_plan`,经真实使用验证)
```
对每个 9 天窗口内的包:
days_left ≤ 1.0 → plan = remaining # 24h 冲刺,全量兜底
days_left > 1.0 → plan = remaining / ceil(days_left) × 1.1 # 均分 + 10% 提前量
总 plan = Σ(各包 plan)
```
**为什么均分优于一次性洗光**(在新模型下):
1. **最小干扰真实用户**:只在额度即将死亡(进窗口)后才每天接管 1/D,其余留给真实用户 → 免费额度利用率最大化,空窗风险最小
2. **请求负载分摊**:每天 3–5 次请求(而非 25–40 次),速率伪装自然,风控友好
3. **余额下降平缓**:23:00 后余额仍保留大部分 → 扣费只发生在余额真正归零时,均分下脚本几乎不可能制造归零
4. **FIFO 自动一致**:效期统一 30 天 → 获得早 = 过期早 → 队列头部 = 窗口内包,计划与执行不打架(分包明细仅用于定总量)
**与日返还循环的契合**:旧脚本本就是"每天跑一次、分 D 天烧完"的设计;自动化后每天重算(余额/天数变化 → div 递减 → 当日份额递增),节奏天然吻合。9 天窗口 + 1.1 系数 + 24h 冲刺:只要每天触发成功,任何进窗口的包都赶在过期前清空。
### 3.4 三个约束(旧逻辑缺失,必须补)
```
plan = min(plan, 2M − daily_usage) # 采集上限:超出不返还,白烧
plan = min(plan, 余额快照 − 本次会话已烧 − BUFFER) # 防扣费:脚本不烧穿余额
```
**执行停止条件(每轮请求后检查,3 条闭环)**:
```
daily_usage ≥ 2M → 停(采集上限,再烧不返还)
余额快照 − 本次已烧 ≤ 0 → 停(防扣费)
本次已烧 ≥ plan → 停(任务完成)
```
### 3.5 消耗量估算
- 单次请求:输入 3–5.5 万字符 ≈ 5–11 万 token(中文 1 字 ≈ 1–2 token)+ 输出 ~1000
- 极值 2M / 8 万 ≈ 25 次请求 → 15–40s 间隔 ≈ 10–17 分钟,1 小时窗口绰绰有余
- 均分模式每天 3–5 次请求,更宽松
### 3.6 触发调度
- 北京时间 **23:00 ± 随机抖动**(防固定时间被风控识别),Redis `consume:last_run:{日期}` 去重,当天未跑过才触发
- 守护线程(`active_monitor_loop` 已有主循环)检测时点 → spawn daemon 线程执行,不阻塞主循环
- 23:00 的时点语义:距 00:00 采集结算 1 小时,此时窗口内包状态已定(真实用户当天用量已发生),洗包目标最准
---
## 四、限额设计(防扣费)——v2 简化逻辑(v5 修订见 §4.5,2026-08-12 已实施)
> ⚠️ 现行版本为 v6 Snapshot-Offset,见 `volc-quota-notes.md` §7.6。2026-08-13 实测修正了
> 其「B0 常数」论证缺陷:`limit = balance_at_sync + usage_at_sync − SMALL_BUFFER = B0 + L − SMALL_BUFFER`,
> L = 同步时点 API 结算滞后窗口内未结算消耗(实测 Δ≈14 分钟、L=149,616 > SMALL_BUFFER 5 万),
> 今日由静态 2M 墙兜底(B0=2,242,178 > 2M),B0 < 2M 时无兜底、有超扣风险。本节 v2/v5 仅作沿革参考。
### 4.1 为什么废弃 Peak-Buffer 峰值状态机(存量场景产物)
旧逻辑 `stable_limit = PeakPool × 0.85`(PeakPool = usage + balance,首锁/门控重锁/DROP 状态机维护)有两个问题:
1. **比例 margin 在 usage 接近 balance 时数学失效**(致命):
- `u=0.5b` 时限额允许再烧 `0.775b`,实际余额只剩 `0.5b`(超 55%)
- `u=0.8b` 时允许再烧 `0.73b`,实际只剩 `0.2b`(超 3.6 倍)→ **烧穿扣费**
- 存量场景没爆是因为 usage 远小于 balance;新模型"返还 = 消耗"使 usage 天天逼近 balance → 必炸
2. **峰值状态机防的是旧 bug**(存量包余额只减不增时,滞后虚高把 PeakPool 漂移、限额越算越多 399,836 → 599,511)。新模型 balance 每天真实跳涨(返还),锁峰值会把真实发放当异常压住——**跟随现值即可,状态机删除**
### 4.1b 1.7M/1.8M 保守值的来历:post-hoc 滞后时代的遗产
旧拦截机制是"请求成功后记账 + 踢模型"(`log_success_event`),**超限请求本身已放行**,存在"滞后一个请求"的漏网量。因此限额必须打折留安全垫:`HARD_DAILY_LIMITS` 默认 1.8M(2M 打 9 折)、`stable_limit` 打 8.5 折(≈1.7M 效应)。
**现状已改为请求前闸门**:`async_pre_call_hook` → `_check_request_block`(429 拒绝,不进 router,不触发 retries/fallback)——主拦截不再滞后,"滞后一个请求"的折扣失去存在必要。新公式精确到 2M;BUFFER 30 万保留,但它防的是**余额统计滞后**(实测 20 万内不动,固定量非比例),与请求滞后无关。
### 4.2 新公式
> ⚠️ 2026-08-12 修订:本节 v2 公式经实测确认**数学正确**(B 滞后收敛后守恒成立),
> 无需取代;v4 冻结快照尝试因误拦已回退(见 §4.5)。保留原文作论证。
```
effective_limit = min(HARD_DAILY_LIMITS, max(0, usage + balance − BUFFER))
HARD_DAILY_LIMITS → 统一 2M(采集上限;doubao-seed-code-preview-251028 480 万遗留一并改为 2M)
```
**`usage + balance` = 当日初始可用池,天然稳定**:
- 消耗中:usage ↑ / balance ↓ → 池恒定 → 限额恒定 ✓
- 返还:balance 跳涨 → 池涨 → 限额次日自动解锁(无需 RECHARGE)✓
- 过期:balance 跳跌 → 池跌 → 限额收紧(无需 DROP)✓
- 余额滞后(实测 20 万内不动):池虚高 ≤ 20 万 → BUFFER 30 万覆盖 ✓
- 消耗任务 23:00 烧(usage 计入):池不变 → 滞后窗口内限额偏松 ≤ BUFFER ✓
- 余额耗尽:池 − 30 万 ≤ 0 → 限额 0 → 全拦,扣费结构性杜绝 ✓
**fallback(stable_limit 缺失窗口)**:免费模型余额常态 < 2M(昨日采多少返多少),若 dynamic 缺失(首次同步前/新增模型/TTL 过期)时裸奔到静态 2M,余额不足的模型会被烧穿扣费。故 fallback 用保守值 `FALLBACK_LIMIT = min(static, 90万)`(env 可调),宁可短时收紧不可裸奔。
**与消耗任务同构**:限额放行条件 = `balance − BUFFER ≥ 0`;消耗任务停止条件 = `余额快照 − 已烧 ≤ 0`——同一把尺子。
### 4.3 简化后保留/删除清单
| 保留 | 删除(~100 行) |
|---|---|
| 同步频率控制(驱动改为 balance,余额越低同步越勤) | `volc:peak_pool:*` 状态机 |
| `remaining = balance − BUFFER` 写入 | RECHARGE / DROP / 首锁 / 门控重锁分支 |
| 软踢 / 恢复 / 审计 / pre-block / usage 记账 | `volc:last_seen_pool` / `volc:last_balance` |
| `get_usage_key` 北京时间滚动 | stable_limit 改直接写 `max(0, usage + balance − BUFFER)` |
### 4.4 空窗期防扣费闭环(消耗量计入 daily_usage 的理由)
23:00 消耗任务把额度洗掉后余额可能很低,此时真实用户请求必须被拦。拦截依据 = `daily_usage vs effective_limit`:
- **若消耗量不计入 daily_usage**:限额看到"真实用户才用 100k,未超限" → 放行 → **扣费**
- **若计入**:daily_usage ≈ 真实 + 消耗任务 ≈ 2M → 超限拦截 → 空窗期安全
同步线程检测 balance 归零 → limit 收紧有 2–60min 滞后;消耗量计入让拦截在**滞后窗口内也有效**,双保险。
### 4.5 v5 修订(2026-08-12 已实施):恢复 v2 骨架,实测修正论证
**v2 曾被误判失效**:2026-08-12 上午观察到「balance 快照 1,558,307 在 usage 1.24M→1.70M 期间
不动」,据此推断 v2 的 `usage + balance` 池膨胀失效,改为 v4 冻结快照 `balance − BUFFER`。
**v4 错误实证(15:09:51 生产日志)**:Bal=596,869(真实剩余 59.7 万),stable=296,869,
usage=1,696,135 → `[PRE-BLOCK] (1,696,135+16/296,869)` **误拦**——把「剩余余额」当
「从 0 累计的上限」,U>0 时低估初始池,有真实免费额度却全拦。
**守恒反推(14:59:51 同步)**:U=1,696,135 + B=596,869 = B0=2,293,004 ≈ 理论剩余 597,004
+ 已用 1,696,135 → **B 滞后收敛后满足 B = B0 − U,v2 公式数学正确**;
13:36 的 1,558,307 是滞后快照(高估真实剩余约 50 万),非余额不足。
今日消耗 1,696,135 < B0 2,293,004 → **当日无扣费**。
**v5 公式(= v2 原式)**:
```
stable_limit = max(0, usage + balance − BUFFER) # 同步时算一次
effective_limit = min(HARD_DAILY_LIMITS, stable_limit)
```
- `usage + balance` = 当日初始免费池(守恒),消耗中恒定(B 实时时);usage 累计撞线
= 用到剩 BUFFER 时停 —— 防扣费语义正确
- 残留风险仅「同步时点 B 滞后」→ stable 虚高 ≤ 滞后窗口消耗 → BUFFER + 低余额加密
同步(600s/300s)+ burst 触发覆盖;滞后收敛后池回守恒值
- 与消耗任务「余额快照 − 已烧 ≤ 0 停」同一把尺子(不变量:放行条件 = `balance − BUFFER ≥ 0`)
- 与昨日采集量**无关**(存量余额守恒;v3「昨日锚」因把单日流量当存量已被否决)
**BUFFER 语义**:防「同步时点余额滞后窗口内的消耗」。13:36 实测滞后高估 ~50 万 > 30 万,
滞后上限未实测 → 建议调大(如 50 万,env 可调)或实测后定。
### 4.3 限额与消耗共用数据、方向相反
| 数据 | 限额(拦真实用户) | 消耗任务(洗过期额度) |
|---|---|---|
| `daily_usage` | 超限踢出 | 停止条件(≥2M 停) |
| `remaining`(余额−缓冲) | 限额收紧 | 防烧穿上限 |
| `stable_limit` | effective_limit 组成 | 不适用(直连旁路) |
---
## 五、架构与实现
### 5.1 代码形态
```
litellm-config/
├── volc_limit_sync.py # 现状:签名/拉取/归一化/限额/守护线程
│ # 新增:23:00 调度钩子(检测时点 + spawn 消耗线程 + 去重)
├── volc_consume.py # 新增:消耗任务(从 auto_consume 移植 + 改造)
│ ├── load_base_logs / generate_dynamic_payload / _sanitize_line # 原样
│ ├── calculate_plan # 保留 + 两个 clamp
│ ├── run_stealthy_consume # 保留(停止条件 + 手动 INCR)
│ └── consume_once(model) # 新封装:查缺口 → 执行 → 记账
├── config.yaml # 模型入口不变(消耗任务读同一份监控发现)
└── .env # 密钥统一走 env(HUOSHAN_API_KEY 即 Ark key)
```
请求直连 Ark(`HUOSHAN_API_KEY`),复用 `MONITORED_MODEL_PAIRS` / `MONITORED_MODEL_ALIASES` / `fetch_all_packages` / 签名。`MONITOR_TARGETS` 与 `config.py` 不再需要。
### 5.2 Redis key 一览
| key | 读写方 | 说明 |
|---|---|---|
| `daily_usage:{日期}:{model}:{hash}` | LiteLLM 回调 INCR + 消耗任务 INCR | 当日用量,TTL 2 天 |
| `limit:dynamic:volcengine:remaining:{model}` | 守护线程写 | 余额 − BUFFER |
| `limit:dynamic:volcengine:stable_limit:{model}` | 守护线程写 | v4 冻结快照:余额 − BUFFER |
| `volc:peak_pool:{日期}:{model}` | 守护线程 | 当日峰池 |
| `consume:last_run:{日期}` | 消耗任务 | 当日去重,新增 |
### 5.3 消耗任务执行流程
```
23:00 触发(守护线程)
→ consume:last_run:{今天} 已存在? → 跳过
→ 遍历 MONITORED_MODELS(config.yaml 发现的 volcengine 模型)
→ fetch_all_packages(复用)→ 按模型过滤窗口内包
→ plan = calculate_plan 结果,应用两个 clamp
→ plan > 阈值(如 5 万)?
→ 读 remaining 快照 → spawn 消耗线程
→ 循环:生成语料 payload → 直连 Ark(速率伪装/退避/6h 上限)
→ 成功 → INCR daily_usage;每轮检查 3 停止条件
→ 失败 > 5 次 → 跳过该模型
→ 写 consume:last_run:{今天}
```
### 5.4 代码资产迁移清单
| 资产 | 来源 | 处理 |
|---|---|---|
| 签名 / `fetch_all_packages` / 归一化 / 模型发现 | volc_limit_sync | 原样复用 |
| `calculate_plan`(9 天窗口/均分×1.1/冲刺/相加) | auto_consume | 保留,加 2 个 clamp |
| 语料生成/脱敏/合成 | auto_consume | 原样移植 |
| 速率伪装/退避/失败上限/6h 上限 | auto_consume | 原样移植 |
| `_record_usage` 的 key 计算(`get_usage_key`) | volc_limit_sync | 消耗任务复用做 INCR |
| 调度钩子 | 新增 | 守护线程 23:00 检测 + 去重 |
---
## 六、风险与边界(已确认/已知)
| 项 | 结论 |
|---|---|
| 空窗期(0:00–次日发放不定时) | **用户确认可接受**;均分策略使空窗概率最小化(余额平缓下降) |
| 烧满 2M 的精确性 | 放弃该策略,改为过期风险驱动,结构性规避 |
| 物理上限 | 窗口内快过期额度 > 2M 时每天最多洗 2M,极少数过期蒸发不可避(返还上限硬约束) |
| 触发连续性 | 依赖每天 23:00 成功;连续失败 ~8 天耗尽窗口缓冲(冲刺兜底),守护线程常驻风险低 |
| 风控 | 23:00 ± 抖动 + 15–40s 随机间隔 + 每天 3–5 次请求,接近真实行为 |
| 容器语料 | LiteLLM 容器内无 `/var/log` 系统日志 → 合成语料兜底(5000 行够用) |
| 消耗任务自身扣费 | 结构性杜绝:plan ≤ 余额快照 − 缓冲,且每轮检查余额 |
| 余额 API 滞后 | v5 恢复 v2(usage+balance−BUFFER):滞后收敛后守恒成立;滞后窗口由 BUFFER + 低余额加密同步覆盖;v4 冻结快照(balance−BUFFER)因误拦已回退 |
---
## 七、待确认事项
1. `MONITOR_DAYS` 沿用 9 天(推荐:每天触发 + 冲刺兜底下足够,且与旧逻辑一致)
2. 实现落点:拆 `volc_consume.py` 模块(推荐,单文件将超 2000 行)还是严格并入 `volc_limit_sync.py`
3. 消耗触发阈值(plan 小于多少跳过):建议 5 万 token,避免无谓请求
4. 消耗任务与真实用户的额度优先级:均分策略已隐含"真实用户优先",无需额外开关
---
## 八、实施步骤(确认后执行)
1. 移植 `auto_consume.py` 的语料/执行逻辑到 `volc_consume.py`,请求层改为直连 Ark(env key)
2. `calculate_plan` 加两个 clamp + 停止条件;执行循环加 `daily_usage` INCR 与余额检查
3. `volc_limit_sync.py` 守护线程加 23:00 调度钩子 + `consume:last_run` 去重
4. `HARD_DAILY_LIMITS` 统一为 2M;`MONITOR_TARGETS`/`config.py` 退役
5. 用 `test_connection.py` 等价方式验证 Ark 连通 → 手动触发一次消耗任务(dry-run 模式验证 plan 计算与 clamp)→ 观察 Redis key 变化
6. 全量回归:守护线程限额逻辑不受影响(消耗线程独立 daemon)