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
This commit is contained in:
vibe-code
2026-08-13 10:24:05 +00:00
parent 0d1776c8a1
commit 9423adeea6
2 changed files with 58 additions and 14 deletions
+2
View File
@@ -256,6 +256,8 @@ docker exec litellm_redis redis-cli SET manual_evict:volcengine/deepseek-v4-flas
- 软踢只作用于**纳入监控**的火山模型(`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`。
---
+51 -9
View File
@@ -116,6 +116,13 @@ THINKING_RESERVE_FLOOR = int(os.getenv("THINKING_RESERVE_FLOOR", "4096"))
# 不可漏放大烧;快照由守护线程恢复后立即回到真实限额。
FALLBACK_LIMIT = int(os.getenv("FALLBACK_LIMIT", "200000"))
# 手动软踢自动解除增量:当前 balance_at_sync ≥ 踢除时快照 + 本值,才视为"免费额度已返还"
# 并自动 DEL manual_evict。默认 50 万(DeepSeek 主用模型返还需量 ≥ 100 万):
# 阈值取"返还量级的一半"级别,既能捕获次日返还,又不会因小额余额波动误放。
AUTO_RELEASE_BALANCE_MARGIN = int(
os.getenv("AUTO_RELEASE_BALANCE_MARGIN", "500000")
)
# Redis Config
REDIS_HOST = os.getenv(
"REDIS_HOST", "redis"
@@ -864,6 +871,8 @@ def _check_request_block(model_name, reserve=0.0):
(clean_model, -1, -1) 标记拒绝(宁可错杀,不可漏放);非监控条目不受影响。
手动软踢:Redis key manual_evict:{config model 字段全名} 存在时返回
(clean_model, -2, -2) 恒 429,由运维 docker exec 设置/删除(见函数内注释)。
自动解除:当前 balance_at_sync 比踢除时快照上升 ≥ AUTO_RELEASE_BALANCE_MARGIN
时删除 key 恢复放行(免费额度返还条件)。
"""
if not LITELLM_AVAILABLE:
return None
@@ -906,20 +915,53 @@ def _check_request_block(model_name, reserve=0.0):
return (clean, -1, -1)
# ── 手动软踢:Redis manual_evict:{raw_model} 存在 → 恒 429 (usage=-2, limit=-2) ──
# 运维手段:模型踩采集上限/异常时无需等同步阈值,直接 docker exec 踢出:
# 软踢: 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:*
# key 用 config.yaml 中该条目的完整 model 字段(含渠道前缀)全匹配,
# 杜绝同名 clean 但不同渠道的模型被误伤(monitored pairs 过滤后仍精确到条目)。
# key 落在 Redis 持久卷,重启 litellm 后仍生效(不依赖 router 内存态),
# 显式 DEL 才恢复。天然幂等:已存在时每次请求恒 429。
if redis_client.get(f"manual_evict:{raw_model}"):
# 但自动解除:若当前 balance_at_sync ≥ 踢除时快照 + AUTO_RELEASE_BALANCE_MARGIN,
# 视为免费额度已返还,自动 DEL 恢复(余额大幅上升不会误判)。
# 惰性记录快照:首次发现 manual_evict key 无对应 snapshot 时,以当前余额为基准。
manual_key = f"manual_evict:{raw_model}"
manual_snap = f"manual_evict:snapshot:{raw_model}"
if redis_client.get(manual_key):
# 1) 确保 snapshot 存在(惰性记录)
snap_val = redis_client.get(manual_snap)
if snap_val is None:
# 首次命中:记录当前 balance_at_sync 作为踢除时刻快照
bal_key = f"limit:dynamic:volcengine:balance_at_sync:{clean}"
current_bal = redis_client.get(bal_key)
if current_bal is not None:
redis_client.set(manual_snap, current_bal)
snap_val = current_bal
else:
snap_val = "0"
# 2) 检查余额是否已显著恢复
auto_release = False
try:
snap_bal = float(snap_val)
bal_key = f"limit:dynamic:volcengine:balance_at_sync:{clean}"
current_bal = redis_client.get(bal_key)
if current_bal is not None:
current_bal_f = float(current_bal)
# 余额从踢除时水位上升了 ≥ AUTO_RELEASE_BALANCE_MARGIN = 返还已发生
if current_bal_f >= snap_bal + AUTO_RELEASE_BALANCE_MARGIN:
log(
f"♻️ [AUTO-RELEASE] {raw_model} manual_evict released (balance "
f"{int(snap_bal):,} → {int(current_bal_f):,}, "
f"delta {int(current_bal_f - snap_bal):,} ≥ margin {AUTO_RELEASE_BALANCE_MARGIN:,})",
level="WARNING",
)
redis_client.delete(manual_key)
redis_client.delete(manual_snap)
auto_release = True
except (TypeError, ValueError):
pass
if not auto_release:
log(
f"🚫 [MANUAL-EVICT] {raw_model} manually soft-evicted (429)",
level="WARNING",
)
return (clean, -2, -2)
# 自动解除后,继续走正常限额判定(不 return)
effective_limit = get_effective_limit(clean, api_base)
if effective_limit is None: