Compare commits
47 Commits
f74f228322
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 10d3c7f88c | |||
| fafcf6e732 | |||
| f0aa78c904 | |||
| 6dfaa3f34c | |||
| 84828646bf | |||
| 5869d311c9 | |||
| 69d144ba34 | |||
| 2502a6b507 | |||
| 4479e179bb | |||
| 9eaafdec6b | |||
| 9423adeea6 | |||
| 0d1776c8a1 | |||
| 9338070562 | |||
| e91e2d1d5e | |||
| 5fddea126e | |||
| 9d46fd4aa5 | |||
| 4e98c14920 | |||
| 795cc99faa | |||
| f15b64e31f | |||
| 660efdfbeb | |||
| 6a2760bd03 | |||
| f86aa59af1 | |||
| 729b1a3987 | |||
| b246393be4 | |||
| 65c52442c8 | |||
| 9f8c6dc44c | |||
| 6edb40d01e | |||
| 8a6416f28f | |||
| 82502dda05 | |||
| 23778a8f7c | |||
| 97484cb7f8 | |||
| d9c7b1406d | |||
| dd349b64cf | |||
| c6c122426a | |||
| 505a55987f | |||
| ee1245621d | |||
| 56b46652f2 | |||
| b82fe3aa7e | |||
| a93f806b33 | |||
| 068f2ee658 | |||
| 5e40a311d1 | |||
| b7df36bb4e | |||
| 96b81d52c4 | |||
| 2cc7c7d08b | |||
| 8feb38db37 | |||
| e29d94f9e8 | |||
| 729e5c7bc3 |
@@ -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 基础规范
|
||||
|
||||
@@ -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`
|
||||
|
||||
+60
-204
@@ -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,109 +44,28 @@ 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: doubao-seed-1-8
|
||||
- model_name: huoshan/deepseek-v4-pro
|
||||
litellm_params:
|
||||
<<: *token_pricing
|
||||
model: volcengine/doubao-seed-1-8-251228
|
||||
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
|
||||
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:
|
||||
<<: *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"
|
||||
@@ -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
|
||||
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-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
|
||||
+34
-3
@@ -9,6 +9,10 @@ services:
|
||||
- ./config.yaml:/app/config.yaml
|
||||
# 【关键】挂载自定义脚本到容器根目录
|
||||
- ./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,7 +70,9 @@ 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
|
||||
# 配置详解:
|
||||
@@ -54,7 +80,9 @@ services:
|
||||
# --maxmemory-policy allkeys-lru: 内存满时挤掉旧数据(安全兜底)
|
||||
# --save 300 1: 每300秒(5分钟)如果至少有1个key变化,就保存快照到磁盘 (降低IO)
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user