diff --git a/volc_limit_sync.py b/volc_limit_sync.py index 991ce0b..c485672 100644 --- a/volc_limit_sync.py +++ b/volc_limit_sync.py @@ -97,6 +97,11 @@ BUFFER = 300000 # User requested 30w buffer CHECK_INTERVAL = 10 THRESHOLD = 300000 +# 前置闸门预留量:请求开始判定 usage + reserve >= limit 即 429。 +# reserve 优先取本次请求自带 max_tokens(真实上限,零采集损失), +# 未设置时 fallback 本常量。堵住"并发请求穿过闸门后集体撞线"的窗口。 +PREBLOCK_RESERVE_TOKENS = int(os.getenv("PREBLOCK_RESERVE_TOKENS", "50000")) + # stable_limit 缺失时(首次同步前/新增模型/TTL 过期)的保守兜底限额。 # 免费模型余额常态 < 2M(昨日采多少返多少),若 fallback 裸奔到采集上限 2M, # 余额不足的模型会被烧穿扣费。宁可短时收紧,不可裸奔。 @@ -597,6 +602,65 @@ def restore_blocked_models(): log(f"❌ Restore check failed: {e}", level="ERROR") +def restore_model_if_healthy(clean_model, allowed_hashes=None): + """ + 若指定模型已被软踢(_daily_limit_backup)且当前限额已恢复(usage < effective limit), + 立即恢复其路由条目。供同步守护在写健康限额后调用,不等 restore_blocked_models 的 + 30min/4h 闸门;否则额度到账后模型会带额度空转被禁(有额度但 no healthy deployments)。 + allowed_hashes: 限定匹配的 api_base hash 集合(None = 不限,按 clean 名匹配)。 + 返回恢复的条目数;任何异常 fail-safe 返回 0。 + """ + if not LITELLM_AVAILABLE or not redis_client: + return 0 + try: + from litellm.proxy.proxy_server import llm_router + + if not llm_router or not llm_router.model_list: + return 0 + + restored = 0 + for i, model_entry in enumerate(llm_router.model_list): + backup = model_entry.get("_daily_limit_backup") + if not backup: + continue + + params = model_entry.get("litellm_params", {}) + m_model = params.get("model", "") + if clean_model_name(m_model) != clean_model: + continue + + m_base = params.get("api_base", "") + if allowed_hashes is not None and generate_api_base_hash( + m_base + ) not in allowed_hashes: + continue + + effective_limit = get_effective_limit(clean_model, m_base) + should_restore = False + if effective_limit is None: + should_restore = True + else: + usage_key = get_usage_key(clean_model, m_base) + current_val = float(redis_client.get(usage_key) or 0) + if current_val < float(effective_limit): + should_restore = True + + if should_restore: + dprint(f"♻️ [RESTORE] Restoring {backup['model_name']} (Quota OK)") + llm_router.model_list[i]["model_name"] = backup["model_name"] + llm_router.model_list[i]["litellm_params"]["api_base"] = m_base + del llm_router.model_list[i]["_daily_limit_backup"] + restored += 1 + + if restored > 0: + log(f"✅ Restored {restored} entry/entries of {clean_model}.", level="INFO") + return restored + + except Exception as e: + log(f"❌ Restore failed for {clean_model}: {e}", level="ERROR") + return 0 + + def startup_audit(): """ 🚀 Startup Audit: @@ -661,10 +725,12 @@ def startup_audit(): log(f"❌ Startup audit failed: {e}", level="ERROR") -def _check_request_block(model_name): +def _check_request_block(model_name, reserve=0.0): """ 请求前闸门:对外模型名 → 判定是否超限。 只拦截 MONITORED_MODEL_PAIRS 内的火山监控条目;newapi 等非监控渠道天然放行。 + reserve: 本次请求的预估消耗(max_tokens 或常量兜底),usage + reserve >= limit 即拦截, + 避免并发请求在闸门检查后集体撞线(限额是低频快照,用量是实时累计)。 返回 (clean_model, usage, limit) 表示超限,None 表示放行。任何异常 fail-open。 """ if not redis_client or not LITELLM_AVAILABLE: @@ -705,7 +771,7 @@ def _check_request_block(model_name): val = redis_client.get(usage_key) usage = float(val or 0) - if usage >= float(effective_limit): + if usage + float(reserve) >= float(effective_limit): return (clean, usage, effective_limit) return None @@ -744,13 +810,24 @@ class DailyLimitHandler(CustomLogger): if not data or not data.get("model"): return None - blocked = _check_request_block(data["model"]) + # 预留本次请求的预估消耗:优先真实 max_tokens(零采集损失), + # 未设置/非法时 fallback 常量,避免并发请求集体撞线超限。 + try: + reserve = float( + data.get("max_tokens") + or data.get("max_completion_tokens") + or PREBLOCK_RESERVE_TOKENS + ) + except (TypeError, ValueError): + reserve = float(PREBLOCK_RESERVE_TOKENS) + + blocked = _check_request_block(data["model"], reserve=reserve) if not blocked: return None clean, usage, limit = blocked log( - f"🚫 [PRE-BLOCK] {data['model']} daily limit reached ({int(usage):,}/{int(limit):,})", + f"🚫 [PRE-BLOCK] {data['model']} daily limit reached ({int(usage):,}+{int(reserve):,}/{int(limit):,})", level="WARNING", ) raise RateLimitError( @@ -1435,6 +1512,11 @@ def active_monitor_loop(): flush=True, ) + # 即时恢复:同步发现限额已恢复(usage < effective limit)时立刻解除软踢, + # 不等 restore_blocked_models 的 30min/4h 闸门 —— 否则日返还额度到账后 + # 模型仍被禁(有额度但 no healthy deployments),直到下一次恢复检查。 + restore_model_if_healthy(target, target_hashes) + # Track min remaining for frequency control configured_models_found = True