Files
litellm-config/docs/merge-auto-consume.md
T
vibe-code 6a2760bd03 feat: 每日消耗任务触发时点 23:00 改为 23:20
- volc_limit_sync:_maybe_trigger_daily_consume 改分钟级判断(23:20 起触发),
  线程内 0-10 分钟随机抖动保留,实际执行窗口 23:20~23:30
- volc_consume:main 手动运行门卫同步 23:20 窗口(--force/--dry-run 不受限)
- docs/volc-quota-notes §9:consume clamp2 余额快照滞后在最新思路(§4.2 量化
  L=149,616)下确认为必改项,方向待定;消耗任务口径同步收紧标记 [x]
- 全仓文档/注释 23:00 统一同步为 23:20(AGENTS/README/docker-compose/两 docs)
2026-08-13 06:08:26 +00:00

21 KiB
Raw Blame History

火山免费额度自动消耗:auto_consume 合入 volc_limit_sync 设计方案

日期:2026-08-11 背景:/root/workspace/huoshan-api-auto_consume/(自动消耗脚本)与 /root/workspace/litellm-config/(LiteLLM 限流回调 + 火山余额同步)合并,使每日 23:20 自动消耗免费额度,防止资源包过期蒸发,避免真实扣费。 前置文档:《merge-volc-limit-into-newapi.md》(2026-08-05,方向相反:把 volc_limit_sync 功能迁往 new-api,未实施)。


一、火山免费额度业务模型(一切设计的事实基础)

1.1 日返还循环

每天任意时刻    真实用户 + 消耗任务 的消耗 = 当日采集量(火山实时统计,每模型单独计算)
每天 23:20 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:20 后余额仍保留大部分 → 扣费只发生在余额真正归零时,均分下脚本几乎不可能制造归零
  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:20 ± 随机抖动(防固定时间被风控识别),Redis consume:last_run:{日期} 去重,当天未跑过才触发
  • 守护线程(active_monitor_loop 已有主循环)检测时点 → spawn daemon 线程执行,不阻塞主循环
  • 23:20 的时点语义:距 00:00 采集结算 40 分钟,此时窗口内包状态已定(真实用户当天用量已发生),洗包目标最准

四、限额设计(防扣费)——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:20 烧(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:20 消耗任务把额度洗掉后余额可能很低,此时真实用户请求必须被拦。拦截依据 = 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:20 调度钩子(检测时点 + 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:20 触发(守护线程)
 → 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:20 检测 + 去重

六、风险与边界(已确认/已知)

项 结论
空窗期(0:00–次日发放不定时) 用户确认可接受;均分策略使空窗概率最小化(余额平缓下降)
烧满 2M 的精确性 放弃该策略,改为过期风险驱动,结构性规避
物理上限 窗口内快过期额度 > 2M 时每天最多洗 2M,极少数过期蒸发不可避(返还上限硬约束)
触发连续性 依赖每天 23:20 成功;连续失败 ~8 天耗尽窗口缓冲(冲刺兜底),守护线程常驻风险低
风控 23:20 ± 抖动 + 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:20 调度钩子 + 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)