diff --git a/README.md b/README.md index 29e2044..219886a 100644 --- a/README.md +++ b/README.md @@ -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`。 --- diff --git a/volc_limit_sync.py b/volc_limit_sync.py index c35bd27..e4df69d 100644 --- a/volc_limit_sync.py +++ b/volc_limit_sync.py @@ -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/ 1 - # 解除: docker exec litellm_redis redis-cli DEL manual_evict:volcengine/ - # 查看: 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}"): - log( - f"🚫 [MANUAL-EVICT] {raw_model} manually soft-evicted (429)", - level="WARNING", - ) - return (clean, -2, -2) + # 但自动解除:若当前 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: