Compare commits

...

47 Commits

Author SHA1 Message Date
vibe-code 10d3c7f88c fix(config): 为火山渠道白名单 reasoning_effort,修复思考强度被静默丢弃
LiteLLM 的 VolcEngineChatConfig.get_supported_openai_params() 白名单不含
reasoning_effort,客户端一传就抛 UnsupportedParamsError;叠加 drop_params
后更糟 —— Router._deployment_params_with_request_reasoning_override 会先剥掉
部署侧 extra_body/thinking,drop_params 再把客户端参数丢掉,两者同归于尽,
上游 body 皆无该字段,方舟回落默认 high(静默失效)。

改动:17 个 volcengine 条目全部加 allowed_openai_params: [reasoning_effort]。
- 透传组(huoshan/deepseek-v4-*、doubao-seed-2-1-*、glm-5.2):客户端显式指定生效
- 档位别名组(doubao-seed-nothink/-minimal/-low/-medium/-high、fanyi):
  extra_body 钉死值为默认档位,客户端显式传参可覆盖;不传则走钉死档位

语义为「默认档位 + 客户端可覆盖」,非硬锁:LiteLLM 无纯配置手段实现强制覆盖。
实测依据见 config.yaml 注释(本地 echo 抓上游请求体)。
2026-09-24 15:37:56 +08:00
vibe-code fafcf6e732 fix: DATABASE_URL 加 client_encoding=UTF8,修复 SQL_ASCII 库启动崩溃
新镜像 main-latest 自 2026-09-20 起默认启用 v2 migration resolver
(上游 fac518dbf7),v2 启动时调用 _warn_if_db_ahead_of_head() 读
_prisma_migrations。本库 litellm 为 SQL_ASCII(server/client_encoding
均 SQL_ASCII,datcollate=C),psycopg 的 TextLoader 对此直接返回 bytes
而非 str(psycopg/types/string.py:110-120),导致 utils.py:174 的
_MIGRATION_TS_RE.match(bytes) 抛 TypeError,容器启动即崩。

同一缺陷还潜伏在 repair_invalid_indexes/_index_repair
(utils.py:760,_TRANSIENT_INDEX_SUFFIX_RE.search(index.name))与
_failed_migration_logs(utils.py:1023,str in bytes),二者 v1/v2 双路径
均会经过,只在对应数据非空时触发(CREATE INDEX CONCURRENTLY 中断或
迁移死锁)。

根因修复:DATABASE_URL 追加 client_encoding=UTF8。libpq 将其作为
启动参数下发(fe-protocol3.c:2503),psycopg 按服务端确认值决定解码
(pq_ctypes.py:229-232),恢复返回 str。Prisma 侧无影响:quaint 丢弃
未知参数(url.rs:459),其驱动 tokio-postgres 本就固定发
client_encoding=UTF8。已全库校验 450 个文本列/43548 行/14.5MB 全部
为合法 UTF-8,无解码失败风险。

另保留 USE_V2_MIGRATION_RESOLVER=false 作独立兜底,沿用更新前的 v1
路径;根因已除后该项可移除以启用 v2。
2026-09-24 12:53:19 +08:00
root f0aa78c904 删除下线的模型 2026-09-12 15:26:49 +08:00
vibe-code 6dfaa3f34c fix: 幂等检查提前到 token_counter 之前,第二次 pre-hook 不打印 Pre-reserve 2026-08-18 11:05:48 +00:00
vibe-code 84828646bf docs: 幂等检查注释标注 data 引用假设与失效条件 2026-08-18 11:01:48 +00:00
vibe-code 5869d311c9 fix: pre-hook 幂等防重,litellm 双调用不重复预扣
litellm 对同一请求调用两次 async_pre_call_hook(proxy 级 +
deployment 级),第二次重复预扣 inflight 但结算只释放一次,
残留累积。已在日志确认(18:46:07 每个请求 Pre-reserve 打印两次)。

修复:pre-hook 入口检查 data["metadata"] 是否已有
PREBLOCK_RESERVE_META_KEY,有则跳过预扣直接放行(幂等)。

验证:新增场景7(幂等防重),7 场景 ALL PASS。
2026-08-18 11:00:18 +00:00
vibe-code 69d144ba34 fix: _release_inflight 改用 Lua 原子释放,floor 0 防幽灵误删
TTL 过期后迟到结算的 DECRBY 会把 inflight 打到负数,旧代码 DEL 整个
key → 误删别的请求的预留。改用 RELEASE_SCRIPT 在 Redis 内原子
DECRBY + floor 0:结果 <=0 则 DEL,否则 SET new_val + EXPIRE 保留 TTL。

_release_inflight eval 补第三个参数 PREBLOCK_INFLIGHT_TTL,与
RELEASE_SCRIPT 的 ARGV[2] 对应。

验证:新增场景6(幽灵释放),6 场景 ALL PASS。
2026-08-18 10:42:21 +00:00
vibe-code 2502a6b507 fix: 软踢用量墙从 1.95M 降到 1.9M 增加容错缓冲 2026-08-18 09:33:18 +00:00
vibe-code 4479e179bb fix: Lua 原子闸门堵死并发 TOCTOU 击穿
根因:_check_request_block 的"读 usage + 比较 + 放行"是纯读检查,
与结算 incrby 之间无原子性。并发请求在同一 usage 快照上各自判定
"未超限"集体放行,叠加消耗击穿限额(08-18 13:14:41-50,5 个并发
请求从 1.49M 冲到 2.11M,超支 11.2 万 token)。

修复:Lua 原子闸门把"读 usage + 读 inflight + 判 limit + INCRBY
预扣"合并为单个 Redis 原子操作,并发请求串行看到彼此的在途预留:
  - 放行:INCRBY inflight reserve,返回 granted
  - 拦截:不预扣,返回 blocked

配对释放:pre-hook 放行时把 reserve 写入 data["metadata"]
(PREBLOCK_RESERVE_META_KEY),litellm 透传到 log_success_event
的 kwargs["litellm_params"]["metadata"],结算时 _release_inflight
配对 DECRBY,usage 只计实际消耗。

兜底:inflight key 设 PREBLOCK_INFLIGHT_TTL(1h),失败/丢失的
预扣由 TTL 自动归零,不永久占额度。

验证:打桩 redis/litellm 跑 5 场景,含复现 08-18 击穿的并发测试
(5 并发 reserve,旧代码全放行击穿,新代码仅放行 2 个且
usage+inflight < 2M)。ALL PASS ✅
2026-08-18 09:30:10 +00:00
vibe-code 9eaafdec6b feat: 新增 huoshan/deepseek-v4-pro 火山模型
- config.yaml 增加 deepseek-v4-pro-ga-260813 模型条目
- volc_limit_sync.py MANUAL_MONITORED_MODEL_ALIASES 增加
  deepseek-v4-pro-0813 -> deepseek-v4-pro-ga-260813 映射
  (资源包名 MMDD 与上游模型名 -ga-YYMMDD 不一致,不映射则余额不同步)
2026-08-18 07:22:58 +00:00
vibe-code 9423adeea6 feat: manual_evict 自动解除(余额增量阈值)
- 首次命中 manual_evict key 时惰性记录 balance_at_sync 快照
  (manual_evict:snapshot:{raw_model}),后续每次请求比较当前余额,
  上升 ≥ AUTO_RELEASE_BALANCE_MARGIN(默认 50 万)时自动 DEL key 恢复
- 小额波动不误放,余额大幅上升(=免费额度已返还)才触发
- 幂等、可 env 覆盖(AUTO_RELEASE_BALANCE_MARGIN=500000)
- 自测 9/9 passed
2026-08-13 10:24:05 +00:00
vibe-code 0d1776c8a1 docs: README 补充手动软踢 manual_evict 用法 2026-08-13 10:08:53 +00:00
vibe-code 9338070562 fix: manual_evict key 改用 config model 字段全匹配防误伤
此前 key 用 clean 名(model 去渠道前缀), 同 clean 不同渠道(如
openai/ 前缀同名模型)会被误伤。改为 manual_evict:{raw_model}
(config.yaml 完整 model 字段, 含渠道前缀)全匹配, monitored pairs
过滤后仍精确到具体条目。用法:
  SET manual_evict:volcengine/<model> 1
2026-08-13 10:07:38 +00:00
vibe-code e91e2d1d5e feat: 手动软踢 Redis key (manual_evict) + redis 卷固定名称
- docker-compose: redis_data 卷加 name: litellm_redis_data, 卷名不再随
  compose 项目名(目录名)漂移, 目录迁移不会新建空卷丢数据
- _check_request_block: 监控条目命中后检查 Redis manual_evict:{clean} key,
  存在即返回 (clean,-2,-2) 恒 429; key 落持久卷, 重启后仍生效,
  显式 DEL 才恢复, 幂等
- async_pre_call_hook: 识别 -2 标记, 返回 manually soft-evicted 429
- 用法: SET/DEL/KEYS manual_evict:* (docker exec litellm_redis redis-cli)
2026-08-13 10:05:02 +00:00
vibe-code 5fddea126e fix: redis 挂载 redis_data 持久化卷, down/up 不再丢今日用量
此前 volumes 段仅注释未挂载, docker compose down 重建容器时
RDB/AOF 随容器层删除, daily_usage 与量快照全丢, 重启后闸门
以余额重算限额(limit=Bal-BUFFER)虽不烧穿, 但当日用量统计归零
且 soft-evict 状态丢失
2026-08-13 09:34:49 +00:00
vibe-code 9d46fd4aa5 fix: 闸门 fail-closed 堵重启/Redis 故障窗口漏放
- _check_request_block: Redis 不可用/无限额/读取异常时监控条目恒 429(标记 -1,-1),非监控渠道不受影响;原实现任何异常 fail-open
- async_pre_call_hook: 识别 fail-closed 标记返回 quota unavailable 429
- FALLBACK_LIMIT 900k→200k(免费池 2M 的 10%),收窄 0 余额模型兜底泄露
- docker-compose: redis 开启 AOF(--appendonly yes everysec),防重启丢快照/用量
- active_monitor_loop: 首轮跳过 CHECK_INTERVAL,额度未同步空窗 12-20s 缩至 2-5s
2026-08-13 09:18:23 +00:00
vibe-code 4e98c14920 perf: 已软踢模型短路跳过 token_counter 预估
- _soft_evicted_block: 扫描 router 中 {model}-limit-reached 条目返回
  (usage, limit), 供 async_pre_call_hook 直接 429
- 省去注定被拒请求的 token_counter 调用(含多模态图片 URL 下载等昂贵路径)
- 与 _check_request_block 的 limit-reached 恒 429 双保险
- stub 验证: 已踢/未踢/不存在三场景符合预期
2026-08-13 09:04:30 +00:00
vibe-code 795cc99faa feat: 软踢预警线提前至 1.95M 用量墙与 10万余额墙
- should_soft_evict: usage ≥ min(effective_limit, 1.95M) 或
  (balance_at_sync < 10万 且 usage>0)——usage>0 防小额初始池(B0<10万)
  模型误杀永久死锁
- should_restore_model 恢复对称: usage < min(额度,1.95M) 且
  (余额≥10万 或 usage==0),否则踢了立即被同步恢复成抖动
- _check_request_block 对已软踢条目(*-limit-reached)恒 429,
  防踢后 1.95M~2M 区间小请求放行→路由原名缺失→400
- startup_audit / restore_blocked_models / restore_model_if_healthy
  统一接入判定; SOFT_EVICT_USAGE_LIMIT/SOFT_EVICT_BALANCE_LIMIT env 可调
- docs §7.6 新增软踢预警线小节(两墙条件/恢复对称/前置联动/2.5% 采集代价)
- 数字用例 9/9 验证(踢/不踢/死锁防护/防抖动/动态墙取 min)
2026-08-13 08:56:26 +00:00
vibe-code f15b64e31f fix: 消耗任务 clamp2 改守恒口径,消除余额快照滞后超烧风险
- b0_est = 拉包时刻 余额+usage (= B0+L,与 v6 闸门同构)
- clamp2: plan ≤ b0_est − SMALL_BUFFER − usage_now; 停止条件 2 改
  usage ≥ b0_est − SMALL_BUFFER,每轮实时读共享 key,真实用户消耗
  结构性计入,两方合计超烧杜绝(残余风险收敛为 §7.5 L 同款)
- 数字用例验证: L=0 撞墙剩 SMALL_BUFFER(旧方案执行中用户耗 100k 超扣 100k)
- docs: §8 排障 4/§9 条目翻转为已实施;新增视觉图片 reserve 估算
  口径校准待确认项(LiteLLM 85/765 OpenAI 口径 vs 火山未公开公式)
2026-08-13 08:31:37 +00:00
vibe-code 660efdfbeb docs: 明确 consume clamp2 余额滞后修复方向
推荐 C 守恒口径(与 v6 闸门同构: b0_est = 拉包时刻 balance+usage,
clamp2/停止条件均改 usage 实时撞 b0_est-SMALL_BUFFER,零 API 成本,
两方不同步结构性消除);A' 单次收敛重取可选叠加;否决 B 扣减 L 预估
(v5 BUFFER 盲猜教训, L 实测浮动 15 倍)
2026-08-13 06:14:11 +00:00
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
vibe-code f86aa59af1 docs: 采纳独立复核精化 v6 注释与不变量(reserve 阈值/consume 快照口径) 2026-08-13 05:51:41 +00:00
vibe-code 729b1a3987 chore: 移除撞墙验证临时 80w 覆盖,恢复统一 2M 默认限额 2026-08-13 05:19:33 +00:00
vibe-code b246393be4 docs: 通读整理 volc-quota-notes.md
- §2 扣费案例标注已被 §5 推翻,消除误导
- §5 日志示例标注为 v2/v5 历史,补充 v6 当前格式
- §7 重编号: v6 Snapshot-Offset 从 §7.6→§7.5, 2M 击穿从 §7.7→§7.6
- §7.1 标题修正(不再写 'v5 已恢复')
- 删除过时的 §7.4 待观察项(BUFFER 已不存在)
- 修正 §7.5 SMALL_BUFFER(5k) 笔误为 5 万
- 修正全章节交叉引用编号
- 统一半角标点风格
2026-08-13 04:26:44 +00:00
vibe-code 65c52442c8 docs: 补充 §7.7 2M 击穿事件 + reserve 改进 + 排障速查更新
- 新增 §7.7: 2M 采集上限击穿事件(20:53 实测)与 reserve 改进
  (token_counter + max_tokens + 后置 soft_evict)
- §8 排障速查: 补充 2M 击穿(§7.7)、B0 守恒前提(§4.2)
- §9 待确认: 新增 reserve 精度验证项,标记 2M 击穿修复完成
- 文件头日期更新
2026-08-13 04:16:52 +00:00
vibe-code 9f8c6dc44c docs: 修正 v6 限额「B0 常数」论证缺陷,记录 08-13 余额滞后实测与 512 亿笔误 2026-08-13 03:55:40 +00:00
vibe-code 6edb40d01e chore: 临时 80w 限额覆盖用于撞墙验证(验证后移除) 2026-08-13 03:21:22 +00:00
vibe-code 8a6416f28f feat: 前置闸门预计算输入 token 增加 DEBUG 日志 2026-08-13 03:21:22 +00:00
vibe-code 82502dda05 fix: 前置闸门 reserve 改用 token_counter 精确估算输入 token
reserve 从单凭 max_tokens fallback 50k 改为:
  reserve = litellm.token_counter(messages) + max_tokens(声明值)

- 输入 token 精确计数(未知模型 fallback cl100k_base,对中文偏高更安全)
- 输出用请求声明的 max_tokens,未声明时 fallback PREBLOCK_RESERVE_TOKENS
- 修复 2M 采集上限被单个大请求(176k output)击穿的问题
2026-08-12 14:58:04 +00:00
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
vibe-code 97484cb7f8 fix: v5 恢复 v2 骨架,修正 v4 误拦
v4(stable = balance - BUFFER)把「剩余余额」当「从 0 累计的上限」,
U>0 时低估初始池:15:09:51 实测 Bal=596,869(剩 59.7 万)却被
usage 1,696,135 >= 296,869 全拦。守恒反推(U+B=2,293,004)证明
v2 公式数学正确——B 滞后收敛后满足 B = B0 - U,今日消耗 1.696M <
B0 2.293M 无扣费(此前「超余额 13.8 万扣费」基于滞后快照的推断
不成立)。v5 = 恢复 v2(stable = usage + balance - BUFFER),
残留风险仅同步时点 B 滞后(13:36 高估 ~50 万),由 BUFFER + 低余额
加密同步覆盖。文档同步:volc-quota-notes §7.3/§7.5、merge-auto-consume §4.5。
2026-08-12 07:21:13 +00:00
vibe-code d9c7b1406d refactor: 目录整理配套引用同步
README/AGENTS 文件清单与命令更新为新结构(generate_keys -> scripts/、
daily_limit -> archive/、文档 -> docs/);config.yaml 与 volc_consume.py
注释中废弃脚本/文档路径同步;README 过期限额示例更新为 2M 配置。
2026-08-12 06:58:16 +00:00
vibe-code dd349b64cf fix: 限额改 v4 冻结快照,余额红线防扣费
v2 的 stable_limit = usage + balance - BUFFER 依赖余额 API 随消耗实时递减,
实测滞后(08-12 快照 1,558,307 在 usage 涨 45 万期间不动)导致池膨胀、限额
虚高,放行到扣费区(当日超余额 13.8 万已扣费)。
扣费口径(用户确认):是否扣费只取决于当下免费额度,超出即扣费,采集量只
决定明日发放。v4:stable_limit = balance - BUFFER 冻结快照,本地 usage 实时
撞线即拦,等价于本地实时扣减余额;effective = min(2M 配额, stable_limit)。
新增 docs/volc-quota-notes.md 额度机制速查;merge-auto-consume.md 补 §4.5。
2026-08-12 06:58:03 +00:00
vibe-code c6c122426a fix: 同步间隔打印重算 + 恢复/审计口径对齐,清理死代码
- 同步后按新 Min Rem 重算'下一次同步'间隔(compute_sync_interval 唯一实现),
  修复'旧 interval + 新 Min Rem'混打误导;删除过时的五档注释
- restore_blocked_models / startup_audit 补 MONITORED hash 过滤,
  与 soft_evict/请求闸门/即时恢复口径一致,防误恢复/误踢同名非监控条目
- 删除 THRESHOLD 死常量
2026-08-12 03:53:02 +00:00
vibe-code 505a55987f fix: 限额恢复即时解锁 + 前置预留闸门,修复有额度却被禁
- 同步守护写健康限额后立即恢复被软踢模型(restore_model_if_healthy),
  不等 restore_blocked_models 的 30min/4h 闸门,修复日返还额度到账后
  模型带额度空转被禁(no healthy deployments)
- 前置闸门改为 usage + reserve >= limit 即拦截,预留优先取请求
  max_tokens(零采集损失),堵住并发请求集体撞线的超限窗口
2026-08-12 03:40:44 +00:00
vibe-code ee1245621d feat: 消耗语料优化:宿主真实日志挂载 + 合成语料扩容 + 游标去重
- docker-compose 只读挂载 /var/log:/host-var-log,LOG_SOURCES 追加宿主路径(真实日志优先)
- 合成语料 5000→30000 行、模板 8→28 种、主机 6→12、覆盖 24h(行间隔 ≈86400/count)
- generate_dynamic_payload 随机起点改游标连续窗口:单次运行内零重复(时间轴随请求推进),
  耗尽后环绕(跨请求重复可接受,跨天语料重建)
- 验证:模板特征 20/20 命中、12 主机、6 窗口零重叠、payload 长度合规
2026-08-11 15:17:52 +00:00
vibe-code 56b46652f2 feat: 日志统一加北京时间戳(_tagged_print 一处生效,实测 1.33us/次) 2026-08-11 15:10:24 +00:00
vibe-code b82fe3aa7e refactor: 余额分档退化为低余额加密两档,常规统一 30 分钟
按上一轮分析彻底落地:余额驱动的慢速档无意义(日返还下余额高=常态,
返还/过期/消耗随时发生)。仅保留 <10万 5min / <30万 10min(返还解锁、
扣费检测要紧),≥30万 统一 30min;慢同步完全交给 is_idle(×4 上限 60min)。
模式打印随分档简化并补充空闲状态。
2026-08-11 15:07:55 +00:00
vibe-code a93f806b33 fix: 同步模式打印图标改为余额风险语义(极速→中风险等,与档位自洽) 2026-08-11 15:06:26 +00:00
vibe-code 068f2ee658 fix: 取消高水位 60 分钟同步档(日返还模型下余额≥80万为常态)
- 高水位 base_interval 3600 → 1800:返还解锁/余额扣减检测延迟从 ≤60min 降到 ≤30min,
  消除每日返还后的额度空转窗口与余额虚高扣费窗口;真正的慢同步条件 = is_idle(已独立实现)
- All Dead 非晨间 1000000 → 300000:返还可能下午到账,不能睡 60 分钟
- 修复模式打印数值与 base_interval 全部错位的遗留 bug,改为输出实际同步间隔
2026-08-11 14:16:27 +00:00
vibe-code 5e40a311d1 fix: 消耗任务四类遗漏修复 + 清理死变量
volc_consume.py:
- payload 模型参数路由:doubao 系 max_completion_tokens+reasoning_effort minimal,
  其他模型 max_tokens(旧 MONITOR_TARGETS 实测配置,否则 doubao 消耗会 400 失效)
- _send_ark URL 单源:直接拼 HUOSHAN_BASE_URL(与 config.yaml 同 env),删 ARK_HOST
- 速率三档对齐旧 auto_consume(正常 15-40s / 极速 / 动态),避免 plan 大时 60s/次拖过 00:00
- run_consume_all 单次拉包复用,消除 6 次重复分页

volc_limit_sync.py:
- 修复 DEBUG 打印残留 current_peak 引用(v2 简化后已删变量,DEBUG 模式会 NameError)
- 清理旧遗留死变量 SYNC_INTERVAL / mode_str / display_limit,去重 limit_str
2026-08-11 13:58:58 +00:00
vibe-code b7df36bb4e fix: dynamic 限额缺失时 fallback 保守兜底(FALLBACK_LIMIT 90万)
免费模型余额常态 <2M(昨日采多少返多少),stable_limit 缺失窗口
(首次同步前/新增模型/TTL 过期)此前 fallback 裸奔到静态 2M,
余额不足的模型会被烧穿扣费。改为 min(static, FALLBACK_LIMIT=90万)。
2026-08-11 13:38:32 +00:00
vibe-code 96b81d52c4 feat: 合入火山免费额度自动消耗任务(每日23点洗过期包防扣费)
- 新增 volc_consume.py:消耗任务模块,直连 Ark 洗 9 天内过期资源包
  (旧 calculate_plan 均分×1.1/24h冲刺保留 + 双 clamp:2M 采集上限、余额−BUFFER 防扣费)
- volc_limit_sync.py:限额 v2 简化(stable_limit = usage+balance−BUFFER,删除峰值状态机 ~90 行),
  HARD_DAILY_LIMITS 统一 2M(pre-block 不滞后,1.8M 折扣作废),
  新增 _maybe_trigger_daily_consume(北京时间 23:00 ± 抖动,SETNX 去重,延迟导入)
- docker-compose 挂载 volc_consume.py;AGENTS.md/文档同步
2026-08-11 13:15:24 +00:00
vibe-code 2cc7c7d08b docs: 火山免费额度自动消耗合并设计方案 2026-08-11 12:53:51 +00:00
root 8feb38db37 fix: 资源包查询修复翻页死循环并加 Status 过滤
- debug_volc_sync.py 缺少 seen_tokens 防循环保护,火山 API 翻页到头后
  NextToken 循环回起始 token,导致无限翻页(实测 900s 拉 1893 页假象,
  真实数据仅 8 页 137 个包)
- 两个 fetch_all_packages 请求加 Status=Effective,服务端直接过滤
  UsedUp/Expired/Refunded 等余额为 0 的无用包(实测过滤后 0 余额包为 0)
- 主脚本循环检测日志从 DEBUG 提升为 WARNING,循环收尾不再静默
2026-08-11 03:39:57 +00:00
root e29d94f9e8 feat: 日志统一加 [VolcSync] 前缀便于筛选
模块级重绑定 print(一处覆盖全部 52 处调用, 含 log()/dprint/裸print),
traceback.print_exc 改走带前缀通道, 去除 log() 内嵌标识避免双前缀。

筛选: docker logs litellm 2>&1 | grep VolcSync
2026-08-11 02:53:33 +00:00
root 729e5c7bc3 fix: 限额峰值改为首锁+余额门控重锁,修复越算越多
原 Smart Peak 逻辑把每次用量增长误判为余额恢复(Jump)锁更高峰值,
叠加火山 AvailableAmount 更新滞后,stable_limit 当天一路抬升
(399,836 -> 599,511)且闸门永不触发,额度实际被烧穿。

新策略(volc:peak_pool 当日键):
- 首次同步锁定当日峰值(usage + balance)
- 仅 balance 自身净上涨(新资源包/对账回补 >=50k)按发放增量抬峰
- pool 回落时 Drop 收紧,永不回升
- usage 驱动的 pool 上涨一律不得抬高限额
新增 volc:last_balance 键记录上次余额快照
2026-08-11 02:53:18 +00:00
14 changed files with 2396 additions and 508 deletions
+16 -5
View File
@@ -5,13 +5,16 @@
## 1. 项目上下文
此仓库包含用于 **LiteLLM** 的自定义回调脚本,核心功能是限制模型用量(特别是火山引擎/VolcEngine)。
- **核心逻辑**:通过 `quota_interceptor` 拦截请求,并通过 `DailyLimitHandler` 记录用量到 Redis。后台 `active_monitor_loop` 守护线程定期从火山引擎 API 同步配额到 Redis。
- **架构**:所有逻辑统一在 `volc_limit_sync.py` 单文件中。LiteLLM 通过 `config.yaml` 中 `callbacks: ["volc_limit_sync.proxy_handler_instance"]` 加载回调,模块加载时自动启动后台同步线程。
- **架构**:主逻辑在 `volc_limit_sync.py`(限额 + 同步守护 + 每日消耗调度),消耗任务拆分在 `volc_consume.py`(每日北京时间 23:20 由守护线程延迟导入触发;见 `merge-auto-consume.md`)。LiteLLM 通过 `config.yaml` 中 `callbacks: ["volc_limit_sync.proxy_handler_instance"]` 加载回调,模块加载时自动启动后台同步线程。
- **关键文件**:
- `volc_limit_sync.py`: **唯一核心文件**,包含同步守护线程(`active_monitor_loop`)、LiteLLM 回调(`DailyLimitHandler` + `quota_interceptor`)、限额计算、模型屏蔽/恢复等全部逻辑。
- `volc_limit_sync.py`: **核心文件**,包含同步守护线程(`active_monitor_loop`)、LiteLLM 回调(`DailyLimitHandler` + `quota_interceptor`)、限额计算(v5:`stable_limit = usage + balance − BUFFER`,`effective_limit = min(2M 采集配额, stable_limit)`;当日初始免费池守恒,扣费判定见 `docs/volc-quota-notes.md`)、模型屏蔽/恢复、以及每日 23:20 消耗任务调度(`_maybe_trigger_daily_consume`,SETNX 去重)。
- `volc_consume.py`: 消耗任务模块(每日 23:20 触发;手动 `python volc_consume.py --dry-run` / `--force`)。复用 volc_limit_sync 的拉包/归一化/用量记账设施,直连 Ark API 洗即将过期的免费资源包(防 30 天过期蒸发)。
- `config.yaml`: LiteLLM 配置文件,包含模型定义和回调配置。
- `docker-compose.yml`: 容器编排,仅挂载 `config.yaml` 和 `volc_limit_sync.py`。
- `debug_volc_sync.py` / `debug_volc_sync_manual.py`: 调试脚本,用于手动验证 API 和 Redis 状态。
- `daily_limit.py.deprecated`: 已废弃,功能已全部合并到 `volc_limit_sync.py`,仅保留作历史参考。
- `docker-compose.yml`: 容器编排,仅挂载 `config.yaml`、`volc_limit_sync.py`、`volc_consume.py`。
- `docs/`: 设计文档与额度机制速查(`merge-auto-consume.md` 消耗任务/限额设计、`volc-quota-notes.md` 火山额度机制事实基础、`merge-volc-limit-into-newapi.md` 未实施的迁移方案)。
- `debug/`: 调试脚本(`debug_volc_sync.py` / `debug_volc_sync_manual.py`),手动验证 API 和 Redis 状态。
- `scripts/`: 工具脚本(`generate_keys.py` 监控模型审计入口)。
- `archive/`: 已废弃(`daily_limit.py.deprecated` 历史参考、`recover.py` 一次性恢复工具)。
## 2. 环境与运行 (Build & Run)
@@ -38,6 +41,14 @@ pip install litellm redis requests
- **调试模式**:
设置环境变量 `DAILY_LIMIT_LOG_LEVEL=DEBUG` 可启用详细日志。
- **消耗任务(手动触发)**:
```bash
# 需要 HUOSHAN_API_KEY(Ark API Key)与 HUOSHAN_BASE_URL
python volc_consume.py --dry-run # 只计算每个模型的消耗计划,不发送请求
python volc_consume.py --force # 立即执行(绕过 23:20 时间限制)
```
每日 23:20 由 `volc_limit_sync.py` 守护线程自动触发(`consume:last_run:{日期}` 去重)。
## 3. 代码风格指南 (Code Style)
### 3.1 基础规范
+64 -21
View File
@@ -10,7 +10,7 @@ callbacks: ["volc_limit_sync.proxy_handler_instance"]
这意味着当前 Docker 容器里真正生效的是 `volc_limit_sync.py`。
`daily_limit.py` 目前 **未启用**,只保留作历史参考。除非你把 `callbacks` 改回 `daily_limit.py`,否则改它里面的 `MODEL_QUOTAS` 不会影响当前运行行为。
旧脚本 `archive/daily_limit.py.deprecated` 目前 **未启用**,只保留作历史参考。除非你把 `callbacks` 改回旧模块,否则改它里面的 `MODEL_QUOTAS` 不会影响当前运行行为。
---
@@ -62,7 +62,7 @@ glm-4-7-251222 -> glm-4-7
每次改完 `config.yaml` 后,执行:
```bash
python generate_keys.py
python scripts/generate_keys.py
```
这个脚本现在不是单纯“生成 key”,而是正式的 **监控模型审计入口**。它会输出:
@@ -106,7 +106,7 @@ python generate_keys.py
### 新增监控模型
1. 在 `config.yaml` 的 `model_list` 里加入新的火山模型条目。
2. 运行 `python generate_keys.py`。
2. 运行 `python scripts/generate_keys.py`。
3. 先确认脚本输出中:
- 该模型已经出现在“监控模型审计”里。
- “监控别名”正确。
@@ -119,7 +119,7 @@ python generate_keys.py
### 删除监控模型
1. 从 `config.yaml` 里删掉对应模型条目。
2. 运行 `python generate_keys.py`,确认它已经不在“监控模型审计”里。
2. 运行 `python scripts/generate_keys.py`,确认它已经不在“监控模型审计”里。
3. 如果该模型曾有单独的 `HARD_DAILY_LIMITS` 项,可以按需清理。
4. 如果该模型对应的是特殊手工别名,且已经没有任何上游模型再用它,可以再清理 `MANUAL_MONITORED_MODEL_ALIASES`。
5. 重启 LiteLLM 容器。
@@ -160,14 +160,14 @@ python generate_keys.py
3. 模糊规则
4. `__DEFAULT__`
例如:
例如(当前值,2026-08):
```python
HARD_DAILY_LIMITS = {
"doubao-seed-code-preview-251028": 4_800_000,
"doubao-seed-2-0-pro-260215:ba0383d3": 1_900_000,
"doubao-seed-2-0-lite-260215:ba0383d3": 1_900_000,
"__DEFAULT__": 1_800_000,
# 火山免费额度规则:每模型每日采集上限 2M,次日按采集量发放资源包。
# 采集上限封顶;真正决定今日可采的是 v5 动态层:
# effective = min(2M, stable_limit),stable_limit = 余额快照 − BUFFER(见 docs/volc-quota-notes.md)
"__DEFAULT__": 2_000_000,
}
```
@@ -175,13 +175,13 @@ HARD_DAILY_LIMITS = {
## 常见坑
### 1. 改了 `daily_limit.py` 但不生效
### 1. 改了旧脚本(`archive/daily_limit.py.deprecated`)但不生效
这是最容易踩的坑。
当前 `config.yaml` 没有启用 `daily_limit.py`,所以:
当前 `config.yaml` 没有启用旧模块 `daily_limit`,所以:
- 改 `daily_limit.py`
- 改 `archive/daily_limit.py.deprecated`
- 改 `MODEL_QUOTAS`
都不会影响当前 LiteLLM 容器的启动和监控行为。
@@ -204,7 +204,7 @@ xxx-251201 -> xxx
xxx-260215 -> xxx
```
所以普通版本升级时,通常只需要改 `config.yaml`,再跑一遍 `python generate_keys.py` 审计确认。
所以普通版本升级时,通常只需要改 `config.yaml`,再跑一遍 `python scripts/generate_keys.py` 审计确认。
---
@@ -213,7 +213,7 @@ xxx-260215 -> xxx
### 审计当前纳入监控模型
```bash
python generate_keys.py
python scripts/generate_keys.py
```
### 直接运行同步监控
@@ -228,6 +228,37 @@ python volc_limit_sync.py
DAILY_LIMIT_LOG_LEVEL=DEBUG python volc_limit_sync.py
```
### 手动软踢某个模型(运维应急,立即 429)
模型踩采集上限/异常时,无需等脚本同步阈值,直接往 Redis 写一个 `manual_evict` key 即可将该模型**立即、持续地**软踢(恒 429,直到显式解除)。
key 格式:`manual_evict:<config.yaml 中该条目的完整 model 字段>`(**含渠道前缀,全匹配**,如 `volcengine/deepseek-v4-flash-ga-260731`;同一 clean 名但不同渠道的模型不会被误伤)。
```bash
# 软踢:立即对该模型返回 429(key 落 Redis 持久卷,重启 litellm 后仍生效)
docker exec litellm_redis redis-cli SET manual_evict:volcengine/<model> 1
# 解除软踢
docker exec litellm_redis redis-cli DEL manual_evict:volcengine/<model>
# 查看当前手动软踢的模型
docker exec litellm_redis redis-cli KEYS manual_evict:*
```
示例(软踢 deepseek-v4-flash-ga-260731):
```bash
docker exec litellm_redis redis-cli SET manual_evict:volcengine/deepseek-v4-flash-ga-260731 1
```
注意:
- 软踢只作用于**纳入监控**的火山模型(`MONITORED_MODEL_PAIRS` 内);非监控渠道不受影响。
- 幂等:key 在期间每个请求都 429,重复 SET 无副作用。
- 解除后立即恢复按正常限额判定。
- **自动解除**:当脚本检测到当前 `balance_at_sync` 比踢除时记录的余额快照**上升 ≥ 50 万**(`AUTO_RELEASE_BALANCE_MARGIN`,可调),即视为免费额度已返还,自动 DEL 该 key 并恢复请求。日常运维无需手动 DEL;仅当模型已被自动解除但你想立即恢复软踢时,重新 SET 即可。
- 自动解除阈值默认 50 万,可在 `.env` 中覆盖:`AUTO_RELEASE_BALANCE_MARGIN=1000000`。
---
## 文件职责
@@ -235,18 +266,30 @@ DAILY_LIMIT_LOG_LEVEL=DEBUG python volc_limit_sync.py
```text
litellm-config/
├── config.yaml # 纳入监控模型的主入口
├── volc_limit_sync.py # 当前生效的监控、同步、限额脚本
├── generate_keys.py # 监控模型审计入口
├── daily_limit.py # 旧脚本,当前 config 未启用
├── debug_volc_sync.py # 资源包调试脚本
└── debug_volc_sync_manual.py # 手动调试脚本
├── docker-compose.yml # 容器编排(挂载 config.yaml / volc_limit_sync.py / volc_consume.py)
├── volc_limit_sync.py # 当前生效的监控、同步、限额脚本(v4 冻结快照限额)
├── volc_consume.py # 每日 23:20 自动消耗任务(防免费资源包过期蒸发)
├── AGENTS.md # 仓库 Agent 协议(代码风格/运行/提交规范)
├── README.md
├── docs/
│ ├── merge-auto-consume.md # 消耗任务 + 限额设计(v2 历史 / v4 修订)
│ ├── volc-quota-notes.md # 火山额度机制速查(扣费口径/方案演进/排障)
│ └── merge-volc-limit-into-newapi.md # 未实施的迁移方案(历史)
├── scripts/
│ └── generate_keys.py # 监控模型审计入口
├── debug/
│ ├── debug_volc_sync.py # 资源包调试脚本
│ └── debug_volc_sync_manual.py # 手动调试脚本
└── archive/
├── daily_limit.py.deprecated # 旧脚本,当前 config 未启用(历史参考)
└── recover.py # 一次性恢复工具(历史)
```
---
## 维护原则
- 先改 `config.yaml`,不要先改 `daily_limit.py`
- 先跑 `python generate_keys.py` 审计,再决定要不要补 `HARD_DAILY_LIMITS`
- 先改 `config.yaml`,不要先改旧脚本 `archive/daily_limit.py.deprecated`
- 先跑 `python scripts/generate_keys.py` 审计,再决定要不要补 `HARD_DAILY_LIMITS`
- 普通版本号模型优先依赖自动推导,不要手工补一堆重复映射
- 只有特殊别名或一对多场景,才改 `MANUAL_MONITORED_MODEL_ALIASES`
View File
+69 -213
View File
@@ -36,40 +36,6 @@ model_list:
###
#日常使用ds v3.2 ,强制think
# - model_name: deepseek-v3.2
# litellm_params:
# <<: *token_pricing
# model: volcengine/deepseek-v3-2-251201
# api_base: os.environ/HUOSHAN_BASE_URL
# api_key: os.environ/HUOSHAN_API_KEY
# 说明中建议设置30分钟(1800秒)以上,避免深度思考过程中断连接
# timeout: 1800
# 关键配置2:启用思考模式
# extra_body 中的参数会被直接合并到请求的 JSON body 中
# extra_headers:
# Connection: "close"
# extra_body:
# thinking:
# type: "enabled"
# model_info:
# max_input_tokens: 128000 # 允许的最大输入长度(单位:token)
# max_context_length: 128000 # 上下文窗口上限
# - model_name: deepseek-v3.2-nothink
# litellm_params:
# <<: *token_pricing
# model: volcengine/deepseek-v3-2-251201
# api_base: os.environ/HUOSHAN_BASE_URL
# api_key: os.environ/HUOSHAN_API_KEY
# extra_headers:
# Connection: "close"
# extra_body:
# thinking:
# type: "disabled"
- model_name: huoshan/deepseek-v4-flash
@@ -78,102 +44,20 @@ model_list:
model: volcengine/deepseek-v4-flash-ga-260731
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
- model_name: huoshan/deepseek-v4-pro
litellm_params:
<<: *token_pricing
model: volcengine/deepseek-v4-pro-ga-260813
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
- model_name: doubao-seed-1-8
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-1-8-251228
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
- model_name: doubao-seed-1-8-nothink
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-1-8-251228
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
extra_headers:
Connection: "close"
drop_params: true
extra_body:
reasoning_effort: "minimal"
# - model_name: doubao-seed-2-0-pro
# litellm_params:
# <<: *token_pricing
# model: volcengine/doubao-seed-2-0-pro-260215
# api_base: os.environ/HUOSHAN_BASE_URL
# api_key: os.environ/HUOSHAN_API_KEY
# timeout: 1800
# drop_params: true
# extra_headers:
# Connection: "close"
# - model_name: doubao-seed-2-0-lite
# litellm_params:
# <<: *token_pricing
# model: volcengine/doubao-seed-2-0-lite-260215
# api_base: os.environ/HUOSHAN_BASE_URL
# api_key: os.environ/HUOSHAN_API_KEY
# drop_params: true
# extra_headers:
# Connection: "close"
- model_name: doubao-seed-2-0-code
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
model_info:
max_input_tokens: 224000 # 允许的最大输入长度(单位:token)
max_context_length: 256000 # 上下文窗口上限
#禁用思考,用于标题等
- model_name: doubao-seed-2-0-code-nothink
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
# thinking:
# type: "disable"
reasoning_effort: "minimal"
model_info:
max_input_tokens: 224000 # 允许的最大输入长度(单位:token)
max_context_length: 256000 # 上下文窗口上限
# doubao-seed-2-0-code 强制中等思考,用作回退
- model_name: doubao-seed-2-0-code-medium
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
timeout: 1800
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "medium"
model_info:
max_input_tokens: 224000 # 允许的最大输入长度(单位:token)
max_context_length: 256000 # 上下文窗口上限
- model_name: doubao-seed-2-1-turbo
litellm_params:
@@ -181,6 +65,7 @@ model_list:
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -194,6 +79,7 @@ model_list:
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -208,6 +94,17 @@ model_list:
model: volcengine/glm-5-2-260617
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
# 方舟 glm-5-2-260617 支持 reasoning_effort(none/minimal/high/max,默认 high;
# 官方参数页 + 实测 curl 均确认),但 LiteLLM 的 VolcEngineChatConfig
# get_supported_openai_params() 白名单里没有它 —— 它是 openai-like 的固定
# 参数表,不含 reasoning_effort —— 客户端一传就抛 UnsupportedParamsError。
# allowed_openai_params 是官方逃生口:get_optional_params 用它在校验前
# extend supported_params,随后 _apply_openai_param_overrides 把该参数从
# non_default_params 原样写回 optional_params,最终透传到请求体。
# 不用 drop_params:那是把该模型所有不支持参数静默丢弃,会掩盖其它真错。
# 注意 thinking.type 无需此项,VolcEngineChatConfig 已原生白名单。
allowed_openai_params:
- reasoning_effort
extra_headers:
Connection: "close"
model_info:
@@ -215,18 +112,22 @@ model_list:
max_context_length: 1024000 # 上下文窗口上限
#=======================豆包汇总nothink
# 【思考档位别名组】doubao-seed-nothink / -minimal / -low / -medium / -high / fanyi
# 语义:别名后缀即【默认】档位,非硬锁。
# - 客户端不传 reasoning_effort:按 extra_body 钉的档位发给方舟。
# - 客户端显式传 reasoning_effort:以客户端为准(覆盖默认档位)。
# 为什么钉不成硬锁(实测,本地 echo 抓上游 body):
# 请求带 reasoning_effort 时,Router._deployment_params_with_request_reasoning_override
# 会【无条件】剥掉部署侧 extra_body.reasoning_effort / thinking;
# 此时若没有 allowed_openai_params 放行,drop_params 又把客户端这个参数丢掉,
# 两者同归于尽 ⇒ 上游 body 两者都没有 ⇒ 方舟回落默认 high(静默失效)。
# 故必须加 allowed_openai_params:让客户端值能通过校验,使结果至少是
# 「客户端说了算」而不是「悄悄变回 high」。
# LiteLLM 当前无纯配置手段做到「不论客户端传什么一律覆盖」;要硬锁只能加
# pre-call hook(本仓库当前不采用)。
# 注:方舟 doubao 上 minimal 等价于关闭思考(实测 reasoning_tokens=0),
# 故 nothink/minimal/fanyi 三个别名默认档位统一用 minimal。
- model_name: doubao-seed-nothink
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "minimal"
- model_name: doubao-seed-nothink
litellm_params:
@@ -234,6 +135,7 @@ model_list:
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -246,6 +148,7 @@ model_list:
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -256,17 +159,6 @@ model_list:
#=======================豆包汇总思考等级minimal
- model_name: doubao-seed-minimal
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "minimal"
- model_name: doubao-seed-minimal
litellm_params:
@@ -274,6 +166,7 @@ model_list:
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -286,6 +179,7 @@ model_list:
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -295,29 +189,7 @@ model_list:
#=======================豆包汇总思考等级low
- model_name: doubao-seed-1-8
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-1-8-251228
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "high"
- model_name: doubao-seed-low
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "low"
- model_name: doubao-seed-low
litellm_params:
@@ -325,6 +197,7 @@ model_list:
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -337,6 +210,7 @@ model_list:
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -346,17 +220,6 @@ model_list:
#=======================豆包汇总思考等级medium
- model_name: doubao-seed-medium
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "medium"
- model_name: doubao-seed-medium
litellm_params:
@@ -364,6 +227,7 @@ model_list:
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -376,6 +240,7 @@ model_list:
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -385,17 +250,7 @@ model_list:
#=======================豆包汇总思考等级high
- model_name: doubao-seed-high
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-0-code-preview-260215
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "high"
- model_name: doubao-seed-high
litellm_params:
@@ -403,6 +258,7 @@ model_list:
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -415,6 +271,7 @@ model_list:
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
@@ -427,30 +284,29 @@ model_list:
- model_name: fanyi
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-1-8-251228
model: volcengine/doubao-seed-2-1-pro-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
timeout: 120
stream: false
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
Connection: "close"
extra_body:
reasoning_effort: "minimal"
reasoning_effort: "minimal"
- model_name: fanyi
litellm_params:
<<: *token_pricing
model: volcengine/doubao-seed-2-1-turbo-260628
api_base: os.environ/HUOSHAN_BASE_URL
api_key: os.environ/HUOSHAN_API_KEY
allowed_openai_params: ["reasoning_effort"]
drop_params: true
extra_headers:
Connection: "close"
extra_body:
reasoning_effort: "minimal"
# - model_name: fanyi
# litellm_params:
# <<: *token_pricing
# model: volcengine/doubao-seed-2-0-lite-260215
# api_base: os.environ/HUOSHAN_BASE_URL
# api_key: os.environ/HUOSHAN_API_KEY
# timeout: 120
# stream: false
# drop_params: true
# extra_headers:
# Connection: "close"
# extra_body:
# reasoning_effort: "minimal"
@@ -462,7 +318,7 @@ litellm_settings:
#drop_params: true #启用drop_params,这样LiteLLM会忽略不支持的参数,但参数仍然会传递给后端API
num_retries: 3 # retry call 3 times on each model_name (e.g. zephyr-beta)
fallbacks: [
{"doubao-seed-high": ["deepseek-v4-pro"]}
#{"doubao-seed-high": ["deepseek-v4-pro"]}
]
#default_fallbacks: ["deepseek-v4-flash"]
request_timeout: 120 # raise Timeout error if call takes longer than 10s. Sets litellm.request_timeout
@@ -473,7 +329,7 @@ litellm_settings:
allowed_fails: 3 # cooldown model if it fails > 1 call in a minute.
# 当前生效入口:volc_limit_sync.py
# 监控模型发现、监控别名推导、资源包同步、HARD_DAILY_LIMITS 都在这个脚本里。
# daily_limit.py 当前未启用,仅保留作历史参考。
# 旧脚本 archive/daily_limit.py.deprecated 当前未启用,仅保留作历史参考。
callbacks: ["volc_limit_sync.proxy_handler_instance"]
router_settings: # router_settings are optional
@@ -20,7 +20,7 @@ SECRET_KEY = os.getenv(
)
HOST = "open.volcengineapi.com"
REGION = "cn-beijing"
BUFFER = 300000
SMALL_BUFFER = 50000
# Suffixes to clean from Volcengine response
SUFFIXES_TO_REMOVE = [
@@ -116,12 +116,15 @@ def fetch_all_packages():
version = "2022-01-01"
all_packages = []
current_token = ""
seen_tokens = set()
while True:
try:
payload = {
"ResourceType": "Package",
"MaxResults": "20",
# 只拉生效中的数据包,跳过 UsedUp/Expired/Refunded 等余额为 0 的无用包
"Status": "Effective",
"NextToken": current_token,
}
headers, body = get_signed_headers(action, version, payload)
@@ -134,8 +137,19 @@ def fetch_all_packages():
if items:
all_packages.extend(items)
next_token = data.get("Result", {}).get("NextToken", "")
if not next_token or next_token == current_token:
# Infinite Loop Protection: VolcEngine API 翻页到头后 NextToken 会
# 循环回起始 token(实测:数据只有 ~8 页,第 9 页绕回第一个 token),
# 没有该保护会无限翻页。
if not next_token:
break
if next_token == current_token:
break
if next_token in seen_tokens:
print(f"WARNING: Detected pagination cycle with token {next_token}. Stopping.")
break
seen_tokens.add(next_token)
current_token = next_token
time.sleep(0.2)
except Exception as e:
@@ -189,10 +203,10 @@ if __name__ == "__main__":
print("\n--- Final Model Balances ---")
for model, balance in model_balances.items():
remaining = balance - BUFFER
remaining = balance - SMALL_BUFFER
print(f"Model: {model}")
print(f" Balance : {balance:,}")
print(f" Buffer : {BUFFER:,}")
print(f" SmallBuffer: {SMALL_BUFFER:,}")
print(f" Remaining: {remaining:,}")
# Simulate Redis Keys
+36 -5
View File
@@ -8,7 +8,11 @@ services:
volumes:
- ./config.yaml:/app/config.yaml
# 【关键】挂载自定义脚本到容器根目录
- ./volc_limit_sync.py:/app/volc_limit_sync.py
- ./volc_limit_sync.py:/app/volc_limit_sync.py
# 自动消耗任务(每日 23:20 由 volc_limit_sync 调度触发)
- ./volc_consume.py:/app/volc_consume.py
# 消耗任务语料:宿主真实系统日志(只读)
- /var/log:/host-var-log:ro
command:
- "--config=/app/config.yaml"
ports:
@@ -17,8 +21,28 @@ services:
TZ: Asia/Shanghai
# ⚠️ 必须设置这个环境变量,否则 Python 找不到你的脚本
PYTHONPATH: "/app"
DATABASE_URL: "postgresql://litellm_app:shriek-sixfold-ethanol@192.168.1.34:5432/litellm"
# ⚠️ 必须保留 client_encoding=UTF8。
# 本库 litellm 是 SQL_ASCII 编码(server/client_encoding 均为 SQL_ASCII,
# datcollate=C)。psycopg 对 SQL_ASCII 连接的 TextLoader 直接返回 bytes
# 而非 str(psycopg/types/string.py:110-120),于是 litellm_proxy_extras
# 里 _MIGRATION_TS_RE.match(migration_name)、
# _TRANSIENT_INDEX_SUFFIX_RE.search(index.name) 等对 bytes 用字符串正则,
# 抛 TypeError: cannot use a string pattern on a bytes-like object。
# 该参数经 libpq 作为启动参数下发(fe-protocol3.c:2503),使连接按 UTF8
# 协商;psycopg 读服务端确认值决定解码(pq_ctypes.py:229-232),
# 从而正常返回 str。Prisma 侧无影响:quaint 丢弃未知参数
# (prisma-engines quaint/src/connector/postgres/url.rs:459),
# 且其驱动 tokio-postgres 本就固定发送 client_encoding=UTF8。
# 已全库校验:450 个文本列 / 43548 行 / 14.5MB 全部是合法 UTF-8,
# 不存在解码失败风险。
DATABASE_URL: "postgresql://litellm_app:shriek-sixfold-ethanol@192.168.1.34:5432/litellm?client_encoding=UTF8"
STORE_MODEL_IN_DB: "True"
# v2 migration resolver 的额外兜底(可去掉以启用 v2):
# main-latest 自 2026-09-20 起默认启用 v2(上游 commit fac518dbf7),
# v2 会在启动时调用 _warn_if_db_ahead_of_head() —— 即上面那个 TypeError
# 的触发点。上面的 client_encoding 修复已从根因上解除该崩溃,
# 但 v2 还引入迁移协调锁等新机制;保守起见先沿用更新前的 v1 路径。
USE_V2_MIGRATION_RESOLVER: "false"
# --- Modified: 添加 Redis 配置 ---
REDIS_HOST: "redis" # 对应下方 redis 服务的名字
REDIS_PORT: "6379"
@@ -46,15 +70,19 @@ services:
# --maxmemory-policy allkeys-lru : 内存满了之后,自动删除最久未使用的缓存(无论是否过期)
# --save "" : 如果你不需要持久化缓存到硬盘(重启后清空),可以把 save 设为空,减少磁盘IO
# command: redis-server --save 60 1 --maxmemory 128mb --maxmemory-policy allkeys-lru --loglevel warning
command: redis-server --maxmemory 32mb --maxmemory-policy allkeys-lru --save 300 1 --loglevel warning
# --appendonly yes: 开启 AOF,防止 Redis 崩溃/重启丢失快照与用量 key →
# 避免"额度未同步"fallback 窗口(首次同步前)被大请求烧穿免费额度
command: redis-server --maxmemory 32mb --maxmemory-policy allkeys-lru --save 300 1 --appendonly yes --appendfsync everysec --loglevel warning
environment:
TZ: Asia/Shanghai
# 配置详解:
# --maxmemory 32mb: 严格限制内存使用 32MB
# --maxmemory-policy allkeys-lru: 内存满时挤掉旧数据(安全兜底)
# --save 300 1: 每300秒(5分钟)如果至少有1个key变化,就保存快照到磁盘 (降低IO)
volumes:
# 映射数据目录,确保重启容器后,今天的计数还在
volumes:
# 映射数据目录:不挂载则 down/up 重建容器时 RDB+AOF 随容器删除,
# 当日用量统计与快照全丢(此前每次重启都丢)。挂载后 down 不删卷,
# 数据跨重启保留。
- redis_data:/data
# ---------------------------
@@ -81,3 +109,6 @@ volumes:
name: litellm_postgres_data
redis_data: # 如果开启了 redis 持久化,这里也要打开
driver: local
# 固定卷名:卷名不再由 compose 项目名(目录名)派生,
# 目录改名/迁移后 up 不会新建空卷导致数据"丢失"
name: litellm_redis_data
+351
View File
@@ -0,0 +1,351 @@
# 火山免费额度自动消耗: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, 20万)`(env 可调;2M 免费池的 10%,0 余额模型最大泄露收窄到 20 万),宁可短时收紧不可裸奔。另:Redis 不可用/读取异常时请求闸门 fail-closed(直接 429),不再放行。
**与消耗任务同构**:限额放行条件 = `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 行够用) |
| 消耗任务自身扣费 | 结构性杜绝:有效墙 min(2M, b0_est − SMALL_BUFFER)(b0_est = 拉包时刻 余额+usage,守恒口径与 v6 闸门同构,2026-08-13 实施);usage 每轮实时含真实用户 → 两方合计不可超烧 |
| 余额 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)
+475
View File
@@ -0,0 +1,475 @@
# 火山免费额度机制速查(VolcEngine Quota Notes)
> 日期:2026-08-12 ~ 2026-08-13
> 用途:后续限额/防扣费设计、排障、消耗任务的事实基础,避免重复推导
> 来源:
> - `merge-auto-consume.md` §1(2026-08-11 用户确认的业务细则)
> - 2026-08-12 ~ 13 对话澄清 + 生产日志观察(deepseek-v4-flash-ga-260731)
> - 2026-08-13 网页口径校准(真实初始余额 B0=2,242,178;API 结算滞后 Δ≈14 分钟)
>
> 代码现状:v6 = Snapshot-Offset(`limit = balance_at_sync + usage_at_sync − SMALL_BUFFER`),2026-08-12 实施。
> v2/v5 (`usage + balance − BUFFER`) 已废弃,见 §7.4/§7.5。
> v6 reserve 改进(`token_counter + max_tokens`)2026-08-13 实施,见 §7.6。
> ⚠️ v6 的「B0 常数/永远不扣费」论证有前提缺陷,见 §4.2 与 §7.5 修正。
---
## 1. 业务模型(用户确认)
### 1.1 日返还循环
```
每天任意时刻 真实用户 + 消耗任务 的消耗 = 当日采集量(火山实时统计,每模型单独计算)
每天 23:20 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. 扣费判定(用户确认口径,最重要)
> 用户原话:**是否扣费只取决于当下有多少免费额度。超出免费额度的请求,都会被扣费。采集量只决定明日发放,与今日是否扣费无关。**
```
扣费判定:消耗 ≤ 当下余额(免费额度) → 免费,余额扣减
消耗 > 当下余额 → 超出部分按真实价格扣费(无返还、不退)
今日采集量(≤2M) → 只决定明日返还的包大小,与今日是否扣费无关
```
> 注:2026-08-12 §5 曾基于滞后快照推断"当日超余额 137,828 tokens 已扣费",
> 后经 §4.2 网页口径校准推翻(滞后快照高估真实剩余约 50 万,实际无扣费)。
> 判扣费必须用**网页真实初始余额 B0** 或滞后收敛时点的 U+B 反推,不能用任意时点快照。
---
## 3. 关键概念区分(反复踩坑处)
| 概念 | 含义 | 量级特征 | 与扣费的关系 |
|---|---|---|---|
| 采集量(今日) | 今日实时消耗统计(火山网页"今日采集量") | 单日流量,0~2M | 只决定明日返还 |
| 余额(免费额度) | 资源包 AvailableAmount 存量之和 | 累计存量,守恒 ≈2M(种子) | **扣费唯一依据** |
| 采集配额 2M | 今日最大采集量 | 每模型固定 | 超了白烧(不返还),未必当场扣费 |
| 返还(T+1) | 昨日采集量 → 新资源包 | 单日流量延迟一天 | 补回余额,不抵今日扣费 |
**核心:余额是累计存量(守恒),采集量是单日流量。限额设计必须用存量(余额),
绝不能用单日流量(昨日采集量)当上限——那会把可用性锁死在水位以下(见 §7.2 v3)。**
---
## 4. 余额 API 的滞后问题
### 4.1 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 万级)。
### 4.2 2026-08-13 网页口径校准(Δ≈14 分钟)
- 网页确认今日真实初始余额 **B0 = 2,242,178**(采集量 0 时点读数,可信;次日 0:00 前无新包入账)
- 11:08 同步:Bal = 2,242,063 ≈ B0(API 尚未扣减当日用量,差 115 为残差)
- 11:22 同步:Bal = 1,726,526 = **B0 − 515,652**(= 11:08 时点累计用量,**精确吻合**)
- 结论:**结算滞后 Δ ≈ 14 分钟**,即 `Bal(t) ≈ B0 − Usage(t−Δ)`;滞后是**周期收敛**的
(2026-08-12 14:59:51 曾收敛:B=596,869 = 2,293,004 − 1,696,135 分毫不差)
- 滞后量(未结算消耗)随消费速度增长:实测 149,616(14 分钟突发消费),全天平均口径仅 ~1 万
#### 核心修正:`balance_at_sync + usage_at_sync ≠ B0 常数`
```
设 Δ_settled = 同步时点 API 已结算的今日消耗(= B0 − Bal_sync)
L = 滞后窗口内未结算消耗 = Usage_at_sync − Δ_settled ≥ 0
limit(v6) = Bal_sync + Usage_at_sync − SMALL_BUFFER
= B0 + L − SMALL_BUFFER
```
- 滞后收敛时(L=0):反推值 = B0 ✓(08-12 14:59:51 场景)
- 滞后未收敛时(L>0):反推值 = B0 + L,**虚高随消费增长**(08-13 11:22:2,391,794 vs
真实 2,242,178,虚高 149,616)
- 「守恒池」只在滞后收敛时成立;日常撞墙/恢复判定用的全是未收敛快照,故 v6 限额常态虚高 L
---
## 5. 症状对照:Today > BalAtSync
> 以下日志为 v2/v5 时代历史示例(字段 Rem/StableLimit 已不存在于 v6 代码),
> 保留用于说明「Today > 余额」这一常见误判。
历史日志示例(13:36-13:38,deepseek-v4-flash-ga-260731):
```
DEBUG: Checking deepseek-v4-flash-ga-260731 (Bal: 1,558,307, Rem: 1,258,307) (余额/剩余)
📊 [Usage] deepseek-v4-flash-ga-260731:ba0383d3: +38083 (Today: 1,279,904 | Rem: 1,558,307 | Limit: 2,000,000)
```
v6 当前日志格式:
```
📊 [Usage] deepseek-v4-flash-ga-260731:ba0383d3: +98 (Today: 1,696,233 | BalAtSync: 596,869 | Limit: 2,000,000)
```
字段口径(v6):
| 字段 | 含义 | 计算 |
|---|---|---|
| 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-13 修正)**: 判扣费必须用**网页真实初始余额 B0** 或滞后收敛时点的 U+B 反推。
13:36 滞后快照(B=1,558,307)高估真实剩余约 50 万,不能用于判扣费。
14:59:51 恰好收敛(B0=2,293,004 可信),今日消耗 1,696,135 < B0 → 当日无扣费。
---
## 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:20 当日去重,TTL 48h |
### 6.2 关键日志
- `🚫 [PRE-BLOCK] {model} daily limit reached ({usage}+{reserve}/{limit})`:请求前闸门 429
- `📊 [Usage] {model}:{hash}: +{tokens} (Today: X | BalAtSync: Y | Limit: Z)`:成功记账
- `🚫 [BLOCK] {model} Limit Hit: {current}/{limit}`:后置闸门触发,soft_evict
---
## 7. 限额方案演进
### 7.1 v2(原式,已被 v5/v6 继承和替代):`stable_limit = usage + balance − BUFFER`
- 意图:`usage + balance` = 当日初始免费池(守恒 B0),消耗中恒定 → 限额恒定
- **数学正确性**: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 触发覆盖;收敛后池回到守恒值
- **已废弃**:BUFFER 盲猜无法解决高强度击穿/低强度浪费(§7.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(日初/刚返还)巧合正确
- 教训:**「剩余额度」不能当「累计限额」用**;两者通过 `usage + balance` 桥接
### 7.4 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.5 v6 Snapshot-Offset 替代。
### 7.5 v6(Snapshot-Offset,当前方案)
```
limit = balance_at_sync + usage_at_sync − SMALL_BUFFER(5 万)
= B0 + L − SMALL_BUFFER # L = 同步时点滞后窗口未结算消耗(§4.2)
```
> ⚠️ 2026-08-13 修正:原论证称 `= B0 − SMALL_BUFFER`(B0 常数、精确抵消),前提「同步时点
> balance + usage = B0」在滞后未收敛时**不成立**——实际 = B0 + L(§4.2 实测 L=149,616)。
#### 核心思路
用同步时点快照 `balance_at_sync + usage_at_sync` 计算累计限额:**滞后收敛时(L=0)恰为
当日初始免费池 B0**;滞后未收敛时虚高 L。两次同步间快照不变,实时 `current_usage` 撞线
只抵消同步**之后**窗口内的增量消耗,**不抵消同步时点已存在的滞后 L**。
**limit 是累计限额(从 0 到上限),不是剩余余额**。`_check_request_block` 用 `current_usage + reserve >= limit` 撞线。
#### 与旧方案的核心区别
```
# 旧 v5: 盲猜滞后窗口消耗
limit = usage + balance - BUFFER(30 万)
# 新 v6: 快照 + 实时 usage 撞固定线
limit = balance_at_sync + usage_at_sync - SMALL_BUFFER(5 万) = B0 + L - SMALL_BUFFER
```
- v5 的 BUFFER 猜的是「两次同步之间 + 滞后窗口内的消耗」——猜不准,高强度击穿低强度浪费
- v6 的 SMALL_BUFFER(5 万)只覆盖**非消耗性余额跳降**(资源包过期) + 少量未结算残差,
不覆盖滞后窗口消耗 L。**L > 5 万时限额虚高进入扣费区**(08-13 实测 L=149,616 > 5 万)。
#### 不变量(带 L 的精确形式)
撞线 `usage + reserve >= B0 + L - SMALL_BUFFER` 时:usage = `B0 + L − SMALL_BUFFER − reserve`,
真实剩余 = `B0 − usage` = **`SMALL_BUFFER + reserve − L`**。
- L=0(滞后收敛):剩余 = SMALL_BUFFER + reserve > 0 → **永远不扣费** ✓(原论证仅此情形成立)
- L > SMALL_BUFFER + reserve:剩余 < 0 → **已扣费**(超扣下界,取决于 reserve;保守写 L > SMALL_BUFFER)
- **今日为何安全(08-13)**:真实 B0=2,242,178 > 静态 2M → `min(2M, B0+L−S)` 被静态 2M 压住,
撞线发生在 usage=2M,真实剩余 242,178 > 0——是**静态墙**兜底,不是 v6 不变量。
**若某日 B0 < 2M(包变小),静态墙失效,L > SMALL_BUFFER 即可能超扣。**
#### 执行流程
**同步线程**(每 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
#### 与旧方案对比
| 场景 | 旧 v5(BUFFER 30 万) | 新 v6(Snapshot-Offset) |
|---|---|---|
| 高强度消耗 | 滞后窗口内消耗 > 30 万 → 击穿扣费 | 撞 B0+L 线:L ≤ 5 万安全,L > 5 万虚高 |
| 低强度消耗 | 浪费 30 万额度(实际只用了 5 万) | 只留 5 万 SMALL_BUFFER |
| 新包到账 | 下次同步 stable limit 跳升 | 下次同步 B0 跳升,一致 |
| 包过期 | 余额跳降,limit 偏高至下次同步 | 同左,但 SMALL_BUFFER=5 万风险窗口小 |
#### 遗留风险
1. **同步时点滞后 L 无法抵消(当前首要风险)**:API 结算滞后 Δ≈14 分钟,
L = 窗口内消耗,随消费速度增长(实测 149,616 > SMALL_BUFFER 5 万)。v6 限额常态虚高 L,
仅被静态 2M 墙 + `min()` 兜住(今日 B0=2,242,178 > 2M)。若 B0 < 2M 或 L 放大,有
放行进扣费区风险。缓解方向:SMALL_BUFFER 与最近消费联动 / 缩短低余额同步 + 突发检测 /
直接以网页口径 B0 校验限额。
2. **多包同时过期 > SMALL_BUFFER**:概率低,无法绝对防御。可调大 SMALL_BUFFER 或监控包过期时间。
### 7.6 2M 采集上限击穿事件与 reserve 改进
#### 事件回顾(2026-08-12 20:53)
```
20:38:26 Today: 1,696,233 (delta +98)
20:53:13 Today: 1,872,804 (delta +176,571) ← 单请求消耗 176k
20:53:18 突发触发 (+176,669 > 100,000)
20:53:35 同步完成: Usage=2,050,826 ← 已超 2M!
23:18:59 下次同步: Bal=242,178 (API 滞后修正,B0 回归 2,293,004)
火山网页: 今日采集量 2,000,000/2,000,000 ← 2M 撑满
```
**2M 采集上限确实撞了**,usage 超出 50,826 token。但 B0=2,293,004 > usage,未扣费。
#### 根因:reserve 估算不足 + 并发击穿
旧 reserve 逻辑:
```python
reserve = data.get("max_tokens") or PREBLOCK_RESERVE_TOKENS(50000)
# 闸门: usage + reserve >= effective_limit → 429
```
deepseek-v4-flash 单请求实际消耗 176,571 token(远超 reserve 50k):
```
请求放行时: usage=1,800,000, reserve=50,000 → 1,850,000 < 2M → PASS
请求完成后: usage=1,976,571 (单请求吃了 176k)
下一个请求: usage=1,976,571, reserve=50,000 → 2,026,571 >= 2M → BLOCK
# 但此时已经超了 2M
```
**并发请求放大问题**:多个请求同时通过闸门(都读到 usage < 2M),各自实际消耗叠加,
放行时 usage 相加不超限,完成后集体超限。
#### B0 守恒假设的再次验证
```
20:33 B0 = 596,869 + 1,696,135 = 2,293,004
20:53 B0 = 596,771 + 2,050,826 = 2,647,597 ← 虚高(API 未结算 17.6 万消耗)
23:18 B0 = 242,178 + 2,050,826 = 2,293,004 ← API 修正后回归初值
```
B0 守恒 **在 API 滞后收敛后成立**,但同步快照期间虚高(§4.2 的 L 量)。
#### reserve 改进:token_counter 精确估算输入
```python
# 旧: 盲猜
reserve = data.get("max_tokens") or 50000
# 新: 精确输入 + 声明输出
input_tokens = litellm.token_counter(model=data["model"], messages=data["messages"])
# 未知模型 fallback cl100k_base,对中文偏高(更安全)
max_out = data.get("max_tokens") or PREBLOCK_RESERVE_TOKENS
reserve = input_tokens + max_out
```
**为什么中文偏高更安全**:DeepSeek 自有 tokenizer 对中文 ~1.5 token/字,
cl100k_base ~2-3 token/字。估算偏高 = 预留更多 = 提前撞线 = 更保守。
**效果**:输入 200k 的请求,旧 reserve 只有 50k,新 reserve 250k+,在 usage=1.8M 时拦住
(而非放行后超 2M)。⚠️ 该阈值仅对 `input ≥ 150k` 成立(由 `1.8M + input + 50k ≥ 2M` 反推);
小输入请求(如 126k)仍会在 usage<2M 时放行,封口实际依赖后置 `soft_evict`(见下节)。
#### 后置闸门兜底
```python
# _record_usage 里,写完 usage 后:
if current_total >= effective_limit:
soft_evict_from_router(raw_model, api_base) # 改名踢出,后续全 429
```
前置闸门挡不住已放行的请求,但 `_record_usage` 完成记账后检查超限,
立即 `soft_evict`,后续请求全部 429。
#### 双重防线总结
| 防线 | 机制 | 挡什么 | 挡不住什么 |
|---|---|---|---|
| **前置** | `reserve = token_counter(messages) + max_tokens` | 预估超限的请求 | 实际消耗 > reserve 的请求 |
| **后置** | `current_total >= effective_limit → soft_evict` | 超限后的后续请求 | 已发出正在执行的请求 |
**遗留解决(2026-08-13 短文本实证)**:`max_tokens` 未声明时前置仍可能预估过低;更隐蔽的是
**思考模型** —— deepseek-v4-flash(ga-260731) 默认思考模式,usage.total_tokens 含 reasoning tokens
且不受 `max_tokens` 限制(max_tokens 只约束正文 completion)。实测「彩虹有几种颜色」:
reserve = 18输入 + 16声明 = 34,实际消耗 +167/+150(正文 ~16 + 思考 ~120-130)。
修复:输出侧取 `max(声明 max_tokens, THINKING_RESERVE_FLOOR=4096)` 兜底思考开销。
大输入请求仍偏保守(cl100k_base 中文 2-3 token/字高估),方向安全。
#### 软踢预警线(2026-08-13)
后置 soft_evict 触发点从"撞有效墙"提前为两个预警线(`volc_limit_sync.py` `should_soft_evict`,
env `SOFT_EVICT_USAGE_LIMIT=1950000` / `SOFT_EVICT_BALANCE_LIMIT=100000` 可调):
| 预警线 | 触发条件 | 目的 |
|---|---|---|
| 用量墙 | usage ≥ min(effective_limit, 1.95M) | 避开贴 2M 边的灰色地带(大请求 429/小请求放行 + reserve 低估/并发击穿余量) |
| 余额墙 | balance_at_sync < 10万 且 usage > 0 | 免费额度见底提前摘除;usage>0 防小额初始池(B0<10万)模型误杀永久死锁 |
**恢复对称(`should_restore_model`)**:usage < min(effective_limit, 1.95M) 且
(余额 ≥ 10万 或 usage == 0) 才恢复——否则踢了立即被下一轮同步恢复成踢/恢复抖动。
恢复函数读 balance_at_sync 与 usage 同源(快照口径)。`restore_blocked_models` /
`restore_model_if_healthy` / `startup_audit` 同口径。
**前置联动**:`_check_request_block` 对已软踢条目(`*-limit-reached`)恒 429,防踢后
1.95M~2M 区间小请求放行 → 路由原名缺失 → 400 路由错误。
**短路优化**:`async_pre_call_hook` 在 `token_counter` 之前先查 `_soft_evicted_block`——
已软踢模型直接 429,跳过 token 预估(含多模态图片 URL 下载等昂贵路径),与
`_check_request_block` 的 limit-reached 恒 429 双保险。
**代价**:用量墙让 1.95M~2M 的 5 万采集放弃(明日返还少 5 万 ≈2.5%),换取贴边期容错;
余额墙对正常大池模型无影响(余额 ≥10万 不触发)。数字用例验证 2026-08-13:9/9 场景
(踢/不踢/死锁防护/防抖动/动态墙取 min)全部符合预期。
---
## 8. 排障速查
1. 看到 Today > BalAtSync 不是异常,先分清口径(§5);判扣费优先看**网页真实初始余额 B0**(或滞后
收敛时点的 U+B 反推,§4.2),不要用任意时点快照(13:36 滞后快照曾误判「超余额扣费」,实际无扣费)
2. 余额显示不动 ≠ 余额充足,是 API 滞后(Δ≈14 分钟,§4.2);v6 的 `balance_at_sync + usage_at_sync`
在滞后未收敛时 = B0 + L(虚高),「实时抵消」仅覆盖同步后窗口,L 不抵消(§7.5)
3. 判断「还能免费采多少」:保守用 `网页 B0 − 今日已采`;v6 公式在滞后收敛时等价,未收敛时偏高 L
4. 消耗任务 23:20 直连 Ark 绕过 LiteLLM 闸门(防自杀),其自身 clamp:`plan ≤ 2M − usage`、
`plan ≤ b0_est − SMALL_BUFFER − usage`(b0_est = 拉包时刻 余额+usage,守恒口径与 v6 闸门同构,
2026-08-13 已实施),停止条件三闭环
5. 余额滞后不改变 B0 守恒;30 天过期蒸发是余额下跌的另一来源,
与消耗扣减区分(如 08-12 余额 1,558,307 → 596,869,其中约 50 万为滞后修正 + 过期)
6. 2M 采集上限可被单请求 + 并发击穿(§7.6):reserve 估算不足时已放行的请求继续消耗,
`soft_evict` 后置兜底只能挡后续。修复:reserve = `litellm.token_counter(messages)` + `max_tokens`
(输出侧另加 `THINKING_RESERVE_FLOOR` 兜底思考模型 reasoning tokens,§7.6)
7. B0 守恒仅在 API 滞后收敛后成立;同步快照期间 `balance + usage = B0 + L`(虚高,§4.2),
不是真实 B0。判扣费用网页真实初始余额或收敛时点反推
---
## 9. 待确认/风险清单
- [~] 余额 API 滞后实测(2026-08-13:Δ≈14 分钟,滞后量 = 窗口内消费,实测 L=149,616 > SMALL_BUFFER
5 万;上限随消费速度变化,建议 SMALL_BUFFER 与消费联动或改用网页 B0 校验,§7.5 遗留风险 1)
- [x] v4 误拦发现与回退(2026-08-12 15:09 生产日志实证,§7.3)
- [x] v5 废弃(2026-08-12,§7.4;被 v6 Snapshot-Offset 替代)
- [x] v6 实施(2026-08-12,§7.5;2026-08-13 生产验证)
- [x] 2M 采集上限击穿修复(2026-08-12 20:53 实测 §7.6:reserve 改用 token_counter + max_tokens,
后置 soft_evict 兜底;23:18 重启后 PRE-BLOCK 生效)
- [x] 消耗任务口径同步收紧(2026-08-13 代码确认:clamp2 用 `vls.SMALL_BUFFER`,与闸门同源一致)
- [ ] consume clamp2 余额快照口径修复待观测(2026-08-13 已实施守恒口径,待 1-2 个结算周期观测
🔴 深夜 L 实际是否 < SMALL_BUFFER;若触发"守恒墙已耗尽"且当日有扣费疑点 → 叠加 A' 收敛重取)
- [ ] 视觉图片请求 reserve 估算口径未校准(2026-08-13 调查):LiteLLM `token_counter` 对 OpenAI 格式
image_url 按 OpenAI 口径计(detail low/auto = 85 token/图,high = 下载图按 tile 85+170×tiles,
失败 fallback ~255),**与火山 Ark doubao 视觉模型实际计费公式不一致**;官方仅提供 Token 计算器
工具(未公开公式),FAQ 只说"图片计入输入 token"。闸门调用未传 use_default_image_token_count →
high-detail 图会在 hook 内同步下载(阻塞)。偏差量级 = 每图几十~几百 token,相对
THINKING_RESERVE_FLOOR(4096) 小;方向待实测(Token 计算器对照或发图看 usage),有图流量时关注
- [x] 软踢预警线(2026-08-13 实施,§7.6):后置踢出提前为 用量墙 1.95M / 余额墙 10万(usage>0),
恢复对称 + 前置 limit-reached 恒 429;9/9 数字用例验证
- [ ] 空窗期(0:00~返还到账)行为观察:v6 下 B0 自动收紧,确认无异常误杀
- [ ] 数据库泄漏:08-12 一天 deepseek-v4 消耗记录量级约 5 万条导致 Redis 内存膨胀;触发监控/清理机制
- [ ] v6 无静态 2M 兜底时的安全:08-13 幸由 B0(2,242,178) > 2M 静态墙兜底;若某日 B0 < 2M,
滞后 L > SMALL_BUFFER 时 v6 限额虚高 → 可能放行到余额 0 → 扣费(§7.5 遗留风险 1)
- [x] reserve 精度验证(2026-08-13 短文本实证:估算 34 vs 实际 167 的根因 =
思考模型 reasoning tokens 不计入 reserve,已用 THINKING_RESERVE_FLOOR 兜底;
token_counter cl100k_base 对中文偏高对输入侧仍是安全方向)
- [x] consume clamp2 余额快照口径不同步(2026-08-13 已实施方向 C 守恒口径):原 `plan ≤ balance −
SMALL_BUFFER` 用拉包时刻余额快照 (volc_consume.py:455),**不含 23:20→00:00 真实用户消耗**;
clamp1 用共享 daily_usage 实时精准,但余额维度两方不同步。§4.2 已实测结算滞后 Δ≈14 分钟 →
快照相对真实剩余虚高 L(随消费速度增长,实测 149,616 > SMALL_BUFFER 5 万),深夜真实消耗大时
两方合计可超烧余额。
实施内容(2026-08-13):`_record_consumption` 已将自身 INCR 进共享 daily_usage,每轮
`_current_daily_usage` 即"真实用户+自身"总消耗 → `b0_est = balance + usage_now`(= B0+L,与
v6 闸门同构),clamp2 改 `plan ≤ b0_est − SMALL_BUFFER − usage_now`、停止条件改
`usage ≥ b0_est − SMALL_BUFFER`(与 clamp1 同构)。零 API 成本,两方不同步结构性消除。
数字用例验证:L=0 时撞墙剩 SMALL_BUFFER(旧方案执行中用户消耗 100k → 超扣 100k);
L>0 残余风险 = §7.5 遗留风险 1 同款(L > SMALL_BUFFER 时 b0_est 虚高,与闸门共享,非消耗任务独有)。
- 🅰 A' 可选叠加:执行前睡 ~15 分钟再拉一次取收敛 balance 算 b0_est(防 L 大,成本 8 次 API)
- ❌ B 扣减 L 预估:v5 BUFFER 盲猜教训(L 实测浮动 15 倍),否决
+619
View File
@@ -0,0 +1,619 @@
# ==========================================
# 🚀 VolcConsume — 火山免费额度自动消耗任务
# ==========================================
# 与 volc_limit_sync.py 合并方案的一部分(见 docs/merge-auto-consume.md):
# - 每日北京时间 23:20 由 volc_limit_sync 守护线程触发(可手动 --force)
# - 洗掉 CONSUME_MONITOR_DAYS 天内过期的资源包额度(FIFO 下即队列头部),
# 防止 30 天过期蒸发导致"启动资金"缩水(免费额度规则:消耗 X → 次日返还 X)
# - 直连 Ark API(Bearer token,绕过 LiteLLM 限额,避免消耗任务自杀)
# - 消耗量手动 INCR 进 daily_usage(与 LiteLLM 回调同 key):
# ① 算准采集上限缺口,② 空窗期限额能正确拦截真实用户(防扣费)
# - 三条停止条件:daily_usage ≥ 2M / 总用量 ≥ 守恒墙(b0_est−SMALL_BUFFER,与 v6 闸门同构) / 已烧 ≥ plan
#
# 手动运行:
# python volc_consume.py --dry-run # 只算计划不消耗
# python volc_consume.py --force # 忽略 23:20 时间限制立即执行
import os
import sys
import time
import math
import random
import uuid
import re
import traceback
from datetime import datetime, timezone
import requests
# 复用 volc_limit_sync 的拉包/归一化/Redis/日志/用量 key
import volc_limit_sync as vls
# ==========================================
# 🛠️ Configuration
# ==========================================
# Ark endpoint 单源:URL 直接拼自 HUOSHAN_BASE_URL(与 config.yaml 同一 env)
HUOSHAN_BASE_URL = os.getenv(
"HUOSHAN_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"
)
ARK_API_KEY = os.getenv("HUOSHAN_API_KEY", "")
# 采集上限:火山免费额度规则,每模型每天最多返还 2M(2026-08 确认所有模型统一)
CONSUME_DAILY_CAP = int(os.getenv("CONSUME_DAILY_CAP", "2000000"))
# 计划低于该阈值不消耗(避免无谓请求)
CONSUME_MIN_PLAN = int(os.getenv("CONSUME_MIN_PLAN", "50000"))
# 洗包窗口:ExpiryTime - now <= N 天的包视为"快过期"
CONSUME_MONITOR_DAYS = int(os.getenv("CONSUME_MONITOR_DAYS", "9"))
# 计划超额系数(旧脚本逻辑:多烧 10% 防积压)
PLAN_OVERAGE = 1.1
# 请求伪装参数(移植自 auto_consume.py)
MIN_INPUT_CHARS = 30000
MAX_INPUT_CHARS = 55000
MIN_SLEEP_BASE = 15
MAX_SLEEP_BASE = 40
MAX_RUNTIME_SECONDS = 21600 # 单模型任务最长 6h
MAX_CONSEC_FAILS = 5
# 任务提示词池(移植自 auto_consume config.py)
TASK_PROMPTS = [
{"sys": "你是资深运维专家。", "user": "请分析以下日志数据,找出潜在的风险点。"},
{"sys": "你是数据结构化助手。", "user": "请忽略乱码,提取以下文本中的关键信息。"},
{"sys": "你是安全审计员。", "user": "扫描以下文本,查找是否存在敏感操作记录。"},
{
"sys": "你是系统可靠性工程师。",
"user": "请判断以下日志片段中是否出现服务降级或异常重启的迹象。",
},
{"sys": "你是日志分析工具。", "user": "汇总以下日志中的错误级别事件,并给出时间分布。"},
{"sys": "你是性能优化顾问。", "user": "根据以下日志评估系统负载特征,指出瓶颈迹象。"},
]
# 日志源候选:宿主挂载(/host-var-log,docker-compose 只读挂载 /var/log)优先,
# 容器内 /var/log 兜底;均不存在时由合成语料兜底
LOG_SOURCES = [
"/host-var-log/syslog",
"/host-var-log/messages",
"/host-var-log/kern.log",
"/host-var-log/auth.log",
"/host-var-log/journal.log",
"/var/log/syslog",
"/var/log/messages",
"/var/log/kern.log",
"/var/log/auth.log",
]
# 单次运行内的语料游标(来源 -> 下一窗口起点),避免连续请求取到重复片段
_CURSOR = {}
# 敏感信息脱敏规则:(正则, 替换值)
_SENSITIVE_PATTERNS = [
(re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b"), "x.x.x.x"),
(re.compile(r"([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}"), "xx:xx:xx:xx:xx:xx"),
(re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"), "user@example.com"),
(re.compile(r"(/home/|/Users/)[^\s\"':]+"), r"\1user"),
]
# ==========================================
# 📄 语料生成(移植自 auto_consume.py,原样保留)
# ==========================================
def _sanitize_line(line):
"""过滤日志行中的敏感信息(IP、MAC、邮箱、用户目录等)"""
for pattern, repl in _SENSITIVE_PATTERNS:
line = pattern.sub(repl, line)
return line
def _synthetic_lines(count=30000):
"""
生成时间线连续、格式多样的合成日志(语料不足时的兜底)。
覆盖最近 24 小时(count 行均匀分布),多主机多风格,模拟真实系统日志流。
"""
hosts = [
"web-01",
"web-02",
"web-03",
"api-01",
"api-02",
"db-01",
"cache-01",
"gateway-01",
"worker-01",
"k8s-node-01",
"mq-01",
"log-01",
]
templates = [
# 内核 / 网络
"kernel: [TCP] Possible SYN flooding on port {port}. Sending cookies.",
"kernel: EXT4-fs (sda1): mounted filesystem with ordered data mode",
"kernel: nf_conntrack: table full, dropping packet.",
"kernel: br0: port 2(veth{hash}) entered blocking state",
"NetworkManager[{pid}]: <info> device (eth0): state change: activated -> connected",
# 系统 / systemd / 安全
"systemd[{pid}]: Started Daily apt download and clean activities.",
"systemd[{pid}]: Stopped User Manager for UID 1000.",
"systemd[{pid}]: Reloading nginx.service.",
'cron[{pid}]: (root) CMD (find /var/log -name "*.gz" -mtime +7 -delete)',
"sudo: pam_unix(sudo:session): session opened for user deploy by (uid=0)",
"sshd: Accepted publickey for deploy from x.x.x.x port {port} ssh2",
"sshd[{pid}]: Failed password for invalid user admin from {ip} port {port} ssh2",
# 应用
"nginx: worker process, pid={pid}, GET /api/v2/status HTTP/1.1 200 0.00{ms}",
'nginx: {ip} - - ["GET /v1/users HTTP/1.1"] 200 512',
"python3[{pid}]: heartbeat ok, workers={n}, queue_depth={q}",
"gunicorn[{pid}]: POST /api/chat 200 {ms}ms",
"celery[{pid}]: Task mail.send[abc{hash}] succeeded in 12.3s",
"node[{pid}]: memory usage 62%, heap {heap}MB, gc paused {gc}ms",
"app[{pid}]: slow query detected: SELECT * FROM metrics WHERE ts > now() - interval '1h'",
# 数据库 / 中间件
"postgres[{pid}]: LOG: checkpoint complete: wrote {n} buffers ({p}%)",
"postgres[{pid}]: LOG: autovacuum: processing database postgres",
"redis-server[{pid}]: Saving DB to disk complete",
"redis-server[{pid}]: 100 changes in 60 seconds. Saving...",
"mysql[{pid}]: [Note] InnoDB: Buffer pool(s) load completed",
"rabbitmq[{pid}]: connection from {ip}:{port} opened",
# 日志轮转 / 采集
"logrotate: rotating /var/log/nginx/access.log, log->access.log.1",
"rsyslogd: action 'action 18' resumed",
"fluent-bit[{pid}]: [info] tail input plugin started",
]
# 24 小时覆盖,行间隔 ≈ 86400/count 秒
step = 86400.0 / count
start_ts = datetime.now().timestamp() - 86400.0
lines = []
for i in range(count):
ts = datetime.fromtimestamp(start_ts + i * step).strftime("%b %d %H:%M:%S")
host = random.choice(hosts)
tpl = random.choice(templates)
msg = tpl.format(
port=random.randint(1024, 65535),
pid=random.randint(500, 9999),
n=random.randint(10, 500),
q=random.randint(0, 200),
ms=random.randint(1, 99),
hash="%04x" % random.randint(0, 0xFFFF),
a=random.randint(0, 255),
b=random.randint(0, 255),
heap=random.randint(200, 900),
gc=random.randint(1, 60),
p=random.randint(1, 100),
ip=f"10.0.{random.randint(0, 255)}.{random.randint(1, 254)}",
)
lines.append(f"{ts} {host} {msg}")
return lines
def load_base_logs():
"""
加载日志语料(按来源分组,保留行序,去除相邻重复、过滤敏感信息)。
:return: (来源列表, {来源: 日志行列表})
"""
source_groups = {}
order = []
for log_path in LOG_SOURCES:
if os.path.exists(log_path) and os.access(log_path, os.R_OK):
try:
with open(log_path, "r", errors="ignore") as f:
lines = [l.strip() for l in f.readlines()[-8000:] if l.strip()]
if lines:
key = os.path.basename(log_path)
source_groups[key] = [_sanitize_line(l) for l in lines]
order.append(key)
except Exception as e:
vls.log(f"⚠️ 读取 {log_path} 失败: {e}", level="WARNING")
continue
# 语料不足 1000 行时,用时间线连续的合成日志补充
total_lines = sum(len(v) for v in source_groups.values())
if total_lines < 1000:
vls.log("📄 本地日志不足,使用合成日志补齐语料")
source_groups["synthetic"] = _synthetic_lines(30000)
order.append("synthetic")
# 每次运行重置语料游标(新语料从头取窗口,跨请求时间轴推进)
_CURSOR.clear()
# 去除相邻重复行(日志轮转/重复打印的常见现象)
for key in source_groups:
deduped = []
for line in source_groups[key]:
if not deduped or line != deduped[-1]:
deduped.append(line)
source_groups[key] = deduped
return order, source_groups
def generate_dynamic_payload(source_groups, order):
"""
生成随机会话头 + 时间线连贯的日志片段。
从单个来源中随机起点取连续窗口(保持时间线连续性),混入少量轮转标记。
"""
target_chars = random.randint(MIN_INPUT_CHARS, MAX_INPUT_CHARS)
available = [k for k in order if len(source_groups[k]) >= 50]
if not available:
available = [k for k in order]
source_key = random.choice(available)
source_lines = source_groups[source_key]
# 游标取窗口:单次运行内连续前进,避免重复片段,且时间轴随请求推进
#(语料按时间排序;耗尽后环绕,跨请求重复可接受,跨天语料重建)
total = len(source_lines)
window_size = min(total, 5000)
start_idx = _CURSOR.get(source_key, 0)
if start_idx + window_size > total:
start_idx = 0
_CURSOR[source_key] = (start_idx + window_size) % total
sampled = source_lines[start_idx : start_idx + window_size]
content_lines = []
total_chars = 0
for line in sampled:
content_lines.append(line)
total_chars += len(line) + 1
if total_chars >= target_chars:
break
markers = [
"-- Reboot --",
"-- 日志轮转,新文件开始 --",
"-- 会话超时,连接已断开 --",
"# 服务健康检查快照",
"",
]
for _ in range(random.randint(1, 3)):
if len(content_lines) > 10:
pos = random.randint(0, len(content_lines) - 1)
content_lines.insert(pos, random.choice(markers))
content = "\n".join(content_lines)
session_id = uuid.uuid4()
ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
header = f"[LOG SESSION {session_id} | 来源: {source_key} | 采集时间: {ts} UTC]"
return f"{header}\n{content}\n[END OF SESSION {session_id}]"
# ==========================================
# 📊 消耗计划(旧 calculate_plan 核心 + v2 clamp)
# ==========================================
def _pkg_models(pkg):
"""解析资源包归属的监控模型(与 process_packages_breakdown 同口径)。"""
raw_code = pkg.get("ConfigurationCode", "")
raw_name = pkg.get("ConfigurationName", "Unknown")
target_raw = raw_code if raw_code else raw_name
norm = vls.normalize_volc_name(target_raw)
targets = vls.MONITORED_MODEL_ALIASES.get(norm, [norm])
if isinstance(targets, str):
targets = [targets]
return targets
def calculate_plan_for_model(packages, model):
"""
计算某模型的当日消耗计划与余额快照。
:return: (plan, details, balance) balance = 该模型全部 Effective token 包剩余
逻辑(保留旧脚本):
- 窗口内包(ExpiryTime - now ≤ CONSUME_MONITOR_DAYS):
days_left ≤ 1 → 冲刺全量;否则 remaining/ceil(days)×1.1,多包相加
- 窗口外包不消耗(留给真实用户,FIFO 下被自然消耗)
"""
total_plan = 0
balance = 0
details = []
now = datetime.now(timezone.utc)
for pkg in packages:
if model not in _pkg_models(pkg):
continue
if pkg.get("Status") != "Effective":
continue
unit = pkg.get("Unit", "token")
if str(unit).lower() != "token":
continue
try:
remaining = int(float(pkg.get("AvailableAmount", 0)))
except Exception:
continue
if remaining < 1000:
continue
balance += remaining
expiry_str = pkg.get("ExpiryTime", "")
if not expiry_str:
continue
try:
if expiry_str.endswith("Z"):
expiry_str = expiry_str[:-1] + "+00:00"
expiry_date = datetime.fromisoformat(expiry_str)
except Exception:
continue
days_left = (expiry_date - now).total_seconds() / 86400.0
if days_left <= 0 or days_left > CONSUME_MONITOR_DAYS:
continue
if days_left <= 1.0:
plan = remaining
strategy = "🚨 冲刺(最后24h)"
else:
div = math.ceil(days_left)
plan = int((remaining / div) * PLAN_OVERAGE)
if plan > remaining:
plan = remaining
strategy = f"📉 分{div}天(加量10%)"
if plan > 0:
total_plan += plan
details.append(
f" - [{pkg.get('ConfigurationName')}] 剩:{remaining:,} | 计划:{plan:,} | {strategy}"
)
return total_plan, details, balance
# ==========================================
# 🔌 Ark 直连请求
# ==========================================
def _send_ark(payload, timeout=120):
"""直连 Ark Chat Completions(Bearer token,不经过 LiteLLM)。
单源配置:endpoint 直接取 HUOSHAN_BASE_URL(与 config.yaml 同一 env),防 region 漂移。"""
url = f"{HUOSHAN_BASE_URL.rstrip('/')}/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {ARK_API_KEY}",
}
return requests.post(url, headers=headers, json=payload, timeout=timeout)
def _build_payload(model, source_groups, order):
"""构造消耗请求:日志语料 + 任务提示词。
模型参数与旧 MONITOR_TARGETS 实测配置一致:
- doubao-seed 系列 → max_completion_tokens + reasoning_effort minimal(火山方舟规范)
- 其他(glm/deepseek)→ max_tokens
"""
dynamic_input = generate_dynamic_payload(source_groups, order)
tpl = random.choice(TASK_PROMPTS)
payload = {
"model": model,
"messages": [
{"role": "system", "content": tpl["sys"]},
{
"role": "user",
"content": f"{tpl['user']}\n\n[DATA START]\n{dynamic_input}\n[DATA END]",
},
],
"stream": False,
}
if model.startswith("doubao"):
payload["reasoning_effort"] = "minimal"
payload["max_completion_tokens"] = 1000
else:
payload["max_tokens"] = 1000
return payload
# ==========================================
# 📈 用量记账(与 LiteLLM 回调同 key)
# ==========================================
def _usage_key(model):
return vls.get_usage_key(model, HUOSHAN_BASE_URL)
def _current_daily_usage(model):
if not vls.redis_client:
return 0
val = vls.redis_client.get(_usage_key(model))
return float(val or 0)
def _record_consumption(model, tokens):
if not vls.redis_client:
return
try:
key = _usage_key(model)
vls.redis_client.incrby(key, int(tokens))
vls.redis_client.expire(key, 172800)
except Exception as e:
vls.log(f"❌ 消耗记账失败: {e}", level="ERROR")
# ==========================================
# 🎯 单模型消耗执行
# ==========================================
def run_consume_model(model, source_groups, order, packages=None, dry_run=False):
"""
对单个监控模型执行"洗过期包"消耗。
目标 = Σ窗口内包计划(均分×1.1/冲刺),clamp:
- 采集上限:plan ≤ CONSUME_DAILY_CAP − 当日已消耗
- 防扣费:plan ≤ b0_est − SMALL_BUFFER − 当日已消耗(b0_est = 拉包时刻 余额+usage,
守恒口径与 v6 闸门同构,见 docs/volc-quota-notes.md §9)
停止条件(每轮检查):daily_usage ≥ 2M / 总用量 ≥ b0_est − SMALL_BUFFER / 已烧 ≥ plan
:param packages: 复用 run_consume_all 的一次拉包结果;None 时自行拉取
"""
if packages is None:
try:
packages = vls.fetch_all_packages()
except Exception as e:
vls.log(f"❌ [{model}] 拉取资源包失败: {e}", level="ERROR")
return
plan, details, balance = calculate_plan_for_model(packages, model)
for d in details:
vls.log(d)
if plan <= 0:
vls.log(f"💤 [{model}] 无快过期包,跳过")
return
usage_now = _current_daily_usage(model)
# clamp 1: 采集上限(超出部分不返还,白烧)
plan = min(plan, CONSUME_DAILY_CAP - usage_now)
# clamp 2: 守恒墙(与 v6 闸门同构,见 docs/volc-quota-notes.md §7.5/§9)
# b0_est = 拉包时刻 余额快照 + usage = B0 + L(L = 滞后窗口未结算消耗);
# 有效墙 min(2M, b0_est − SMALL_BUFFER),usage_now 实时含真实用户 → 两方合计结构性消除
b0_est = balance + usage_now
plan = min(plan, max(0, b0_est - vls.SMALL_BUFFER - usage_now))
plan = int(max(0, plan))
if plan < CONSUME_MIN_PLAN:
vls.log(f"💤 [{model}] 计划 {plan:,} < 阈值 {CONSUME_MIN_PLAN:,},跳过")
return
vls.log(
f"▶️ [{model}] 目标消耗 {plan:,} | 守恒墙 {int(b0_est - vls.SMALL_BUFFER):,} | 当日已用 {usage_now:.0f}"
)
if dry_run:
vls.log(f"🔍 [dry-run] 仅计算,不发送请求")
return
_run_requests(model, plan, b0_est, source_groups, order)
def _run_requests(model, plan, b0_est, source_groups, order):
"""
循环发送消耗请求。速率:目标 1 小时内完成(23:20 触发,00:00 结算)。
b0_est: 拉包时刻守恒池 = balance + usage(= B0 + L),有效墙 = min(2M, b0_est − SMALL_BUFFER)。
"""
burned = 0
fail_count = 0
deadline = time.time() + MAX_RUNTIME_SECONDS
# 速率三档(对齐旧 auto_consume run_stealthy_consume):
# 任务轻松(单次间隔 > MAX_SLEEP_BASE)→ 正常模式 15-40s 伪装,不赶时间
# 任务极重(间隔 < 2s)→ 极速模式 1-3s(本场景 plan≤2M 实际不会触发)
# 其余 → 动态 ±5s
# 目标:23:20 触发、00:00 结算,1 小时内完成(plan=2M ≈ 47 次 × 27s ≈ 21 分钟)
est_per_req = (MIN_INPUT_CHARS + MAX_INPUT_CHARS) / 2 + 1000
est_requests = max(1, plan / est_per_req)
interval = int(3600 / est_requests)
if interval > MAX_SLEEP_BASE:
cur_min, cur_max = MIN_SLEEP_BASE, MAX_SLEEP_BASE
mode_str = "正常模式 🐢"
elif interval < 2:
cur_min, cur_max = 1, 3
mode_str = "极速模式 🐇"
else:
cur_min, cur_max = max(1, interval - 5), interval + 5
mode_str = f"动态调整 🚗 ({cur_min}~{cur_max}s)"
vls.log(
f"⚡ 速率控制: {mode_str} | 预计 {int(est_requests)} 次请求 (单次 ≈{int(est_per_req):,} token)"
)
while burned < plan:
if time.time() >= deadline:
vls.log(f"⏰ [{model}] 已达运行时长上限,停止本次任务", level="WARNING")
break
if fail_count > MAX_CONSEC_FAILS:
vls.log(f"❌ [{model}] 失败次数过多,跳过剩余任务", level="ERROR")
break
# 停止条件 1: 采集上限
if _current_daily_usage(model) >= CONSUME_DAILY_CAP:
vls.log(f"🛑 [{model}] 当日采集已达上限 {CONSUME_DAILY_CAP:,},停止", level="WARNING")
break
# 停止条件 2: 守恒墙耗尽(防扣费;usage 实时含真实用户+自身,两方合计结构性杜绝)
if _current_daily_usage(model) >= b0_est - vls.SMALL_BUFFER:
vls.log(
f"🛑 [{model}] 守恒墙已耗尽(b0_est−SMALL_BUFFER),停止(防扣费)",
level="WARNING",
)
break
# 停止条件 3: 计划完成
if burned >= plan:
break
payload = _build_payload(model, source_groups, order)
try:
resp = _send_ark(payload)
if resp.status_code == 200:
fail_count = 0
total = resp.json().get("usage", {}).get("total_tokens", 0)
if total > 0:
_record_consumption(model, total)
burned += total
percent = min(100, burned / plan * 100)
vls.log(
f"✨ [{model}] 进度 {percent:.1f}% | 本次 {total:,} | 累计 {burned:,}/{plan:,}"
)
if burned < plan:
time.sleep(random.randint(cur_min, cur_max))
else:
if resp.status_code == 401:
vls.log(f"❌ [{model}] 认证失败 (401),API Key 可能已失效,中止", level="ERROR")
break
fail_count += 1
backoff = min(60, 2**fail_count) + random.uniform(0, 1)
vls.log(
f"⏳ [{model}] API 错误 {resp.status_code},{backoff:.1f}s 后重试",
level="ERROR",
)
time.sleep(backoff)
except Exception as e:
fail_count += 1
vls.log(f"❌ [{model}] 请求异常: {e}", level="ERROR")
time.sleep(10)
vls.log(f"✅ [{model}] 消耗结束。实际消耗: {burned:,}/{plan:,}")
# ==========================================
# 🚀 总入口
# ==========================================
def run_consume_all(dry_run=False):
"""遍历所有监控模型执行消耗。模型列表复用 volc_limit_sync 的 config.yaml 发现。"""
if not ARK_API_KEY:
vls.log("❌ 未配置 HUOSHAN_API_KEY,无法消耗", level="ERROR")
return
if not vls.MONITORED_MODEL_PAIRS:
vls.discover_monitored_models()
models = sorted({m for m, _h in vls.MONITORED_MODEL_PAIRS})
if not models:
vls.log("❌ 未发现监控模型(config.yaml 无 volcengine 条目)", level="ERROR")
return
vls.log(f"📚 加载语料库...")
order, source_groups = load_base_logs()
vls.log(
f"📄 语料来源: {', '.join(f'{k}({len(v)}行)' for k, v in source_groups.items())}"
)
# 只拉一次资源包,全部模型复用(每模型独立 fetch 会重复分页 6 次)
try:
packages = vls.fetch_all_packages()
except Exception as e:
vls.log(f"❌ 拉取资源包失败: {e}", level="ERROR")
packages = []
for model in models:
try:
run_consume_model(model, source_groups, order, packages=packages, dry_run=dry_run)
except Exception as e:
vls.log(f"❌ [{model}] 消耗任务异常: {e}\n{traceback.format_exc()}", level="ERROR")
vls.log("🎉 消耗任务完成")
def main():
dry_run = "--dry-run" in sys.argv
force = "--force" in sys.argv
if not force and not dry_run:
beijing_struct = time.gmtime(time.time() + 8 * 3600)
beijing_hour = beijing_struct.tm_hour
beijing_min = beijing_struct.tm_min
if beijing_hour != 23 or beijing_min < 20:
vls.log(
f"⏰ 当前北京时间 {beijing_hour}:{beijing_min:02d},非 23:20 触发窗口。加 --force 强制执行",
level="WARNING",
)
return
run_consume_all(dry_run=dry_run)
if __name__ == "__main__":
main()
+748 -260
View File
File diff suppressed because it is too large Load Diff