feat: 手动软踢 Redis key (manual_evict) + redis 卷固定名称

- docker-compose: redis_data 卷加 name: litellm_redis_data, 卷名不再随
  compose 项目名(目录名)漂移, 目录迁移不会新建空卷丢数据
- _check_request_block: 监控条目命中后检查 Redis manual_evict:{clean} key,
  存在即返回 (clean,-2,-2) 恒 429; key 落持久卷, 重启后仍生效,
  显式 DEL 才恢复, 幂等
- async_pre_call_hook: 识别 -2 标记, 返回 manually soft-evicted 429
- 用法: SET/DEL/KEYS manual_evict:* (docker exec litellm_redis redis-cli)
This commit is contained in:
vibe-code
2026-08-13 10:05:02 +00:00
parent 5fddea126e
commit e91e2d1d5e
2 changed files with 30 additions and 0 deletions
+3
View File
@@ -89,3 +89,6 @@ volumes:
name: litellm_postgres_data name: litellm_postgres_data
redis_data: # 如果开启了 redis 持久化,这里也要打开 redis_data: # 如果开启了 redis 持久化,这里也要打开
driver: local driver: local
# 固定卷名:卷名不再由 compose 项目名(目录名)派生,
# 目录改名/迁移后 up 不会新建空卷导致数据"丢失"
name: litellm_redis_data
+27
View File
@@ -862,6 +862,8 @@ def _check_request_block(model_name, reserve=0.0):
返回 (clean_model, usage, limit) 表示超限,None 表示放行。 返回 (clean_model, usage, limit) 表示超限,None 表示放行。
fail-closed:Redis 不可用 / 限额获取失败 / 读取异常时,对监控条目返回 fail-closed:Redis 不可用 / 限额获取失败 / 读取异常时,对监控条目返回
(clean_model, -1, -1) 标记拒绝(宁可错杀,不可漏放);非监控条目不受影响。 (clean_model, -1, -1) 标记拒绝(宁可错杀,不可漏放);非监控条目不受影响。
手动软踢:Redis key manual_evict:{clean} 存在时返回 (clean_model, -2, -2)
恒 429,由运维 docker exec 设置/删除(见函数内注释)。
""" """
if not LITELLM_AVAILABLE: if not LITELLM_AVAILABLE:
return None return None
@@ -903,6 +905,20 @@ def _check_request_block(model_name, reserve=0.0):
) )
return (clean, -1, -1) return (clean, -1, -1)
# ── 手动软踢:Redis manual_evict:{clean} 存在 → 恒 429 (usage=-2, limit=-2) ──
# 运维手段:模型踩采集上限/异常时无需等同步阈值,直接 docker exec 踢出:
# 软踢: docker exec litellm_redis redis-cli SET manual_evict:<clean_model> 1
# 解除: docker exec litellm_redis redis-cli DEL manual_evict:<clean_model>
# 查看: docker exec litellm_redis redis-cli KEYS manual_evict:*
# key 落在 Redis 持久卷,重启 litellm 后仍生效(不依赖 router 内存态),
# 显式 DEL 才恢复。天然幂等:已存在时每次请求恒 429。
if redis_client.get(f"manual_evict:{clean}"):
log(
f"🚫 [MANUAL-EVICT] {clean} manually soft-evicted (429)",
level="WARNING",
)
return (clean, -2, -2)
effective_limit = get_effective_limit(clean, api_base) effective_limit = get_effective_limit(clean, api_base)
if effective_limit is None: if effective_limit is None:
# 监控条目取不到限额:拒绝而非放行(fail-closed) # 监控条目取不到限额:拒绝而非放行(fail-closed)
@@ -1023,6 +1039,17 @@ class DailyLimitHandler(CustomLogger):
clean, usage, limit = blocked clean, usage, limit = blocked
if usage < 0 or limit < 0: if usage < 0 or limit < 0:
if usage == -2:
# manual_evict Redis key 触发的运维手动软踢
log(
f"🚫 [PRE-BLOCK] {data['model']} manually soft-evicted by operator (429)",
level="WARNING",
)
raise RateLimitError(
message=f"Model '{data['model']}' manually soft-evicted by operator, request denied",
llm_provider="volcengine",
model=data["model"],
)
# fail-closed 标记:Redis 不可用/限额状态未知,拒绝防漏放 # fail-closed 标记:Redis 不可用/限额状态未知,拒绝防漏放
log( log(
f"🚫 [PRE-BLOCK] {data['model']} denied: quota state unavailable (fail-closed)", f"🚫 [PRE-BLOCK] {data['model']} denied: quota state unavailable (fail-closed)",