feat: Snapshot-Offset 限额策略替换 BUFFER 盲猜

核心改动:不再用 BUFFER 猜滞后窗口消耗,而是用同步时点快照
 直接计算累计限额。

- BUFFER=300000 → SMALL_BUFFER=50000(仅覆盖非消耗性跳降)
- get_effective_limit: limit = B0 - SMALL_BUFFER,不依赖 current_usage
- 同步循环:写 balance_at_sync + usage_at_sync 替代 remaining/stable_limit
- 突发检测/低额度判断:读 balance_at_sync 替代 remaining
- volc_consume.py: vls.BUFFER → vls.SMALL_BUFFER
- debug/debug_volc_sync.py: 同步更新
- docs/volc-quota-notes.md: 新增 §7.6 v6 设计文档,更新各节口径
This commit is contained in:
vibe-code
2026-08-12 12:22:22 +00:00
parent 97484cb7f8
commit 23778a8f7c
4 changed files with 169 additions and 171 deletions
+65 -121
View File
@@ -93,7 +93,7 @@ HOST = "open.volcengineapi.com"
REGION = "cn-beijing"
# Daemon Config
BUFFER = 300000 # User requested 30w buffer
SMALL_BUFFER = 50000 # 覆盖资源包过期/API 未结算等非消耗性余额跳降
CHECK_INTERVAL = 10
# 前置闸门预留量:请求开始判定 usage + reserve >= limit 即 429。
@@ -161,7 +161,7 @@ MONITORED_MODEL_ALIASES = {}
HARD_DAILY_LIMITS = {
# 火山免费额度规则(2026-08):每模型每日采集上限 2M,次日按采集量发放资源包。
# 请求前闸门(async_pre_call_hook)已不滞后 → 无需打折(旧 1.8M/480 万为 post-hoc 时代遗产)。
# 统一 2M,余额相关的动态限额由 stable_limit(usage+balance−BUFFER)表达。
# 统一 2M,余额相关的动态限额由 balance_at_sync - (usage_now - usage_at_sync) 实时。
"__DEFAULT__": 2_000_000,
}
@@ -407,24 +407,26 @@ def calculate_effective_limit(static_limit, dynamic_limit):
return float(static_limit) if static_limit is not None else None
def format_limit_debug_fields(static_limit=None, stable_limit=None, effective_limit=None):
"""Render explicit limit labels for debug logs to avoid confusing stable and effective caps."""
def format_limit_debug_fields(static_limit=None, balance_at_sync=None, usage_at_sync=None, effective_limit=None):
"""Render explicit limit labels for debug logs."""
fields = []
if static_limit is not None:
fields.append(f"StaticLimit={int(float(static_limit)):,}")
if stable_limit is not None:
fields.append(f"StableLimit={int(float(stable_limit)):,}")
if balance_at_sync is not None:
fields.append(f"BalAtSync={int(float(balance_at_sync)):,}")
if usage_at_sync is not None:
fields.append(f"UsageAtSync={int(float(usage_at_sync)):,}")
if effective_limit is not None:
fields.append(f"EffectiveLimit={int(float(effective_limit)):,}")
return " | ".join(fields) if fields else "EffectiveLimit=unknown"
def get_effective_limit(clean_model, api_base, current_usage=None):
"""
Calculate effective limit using the Peak-Buffer strategy.
Target Limit = (Peak Daily Remaining) - Buffer.
Calculate effective limit using the Snapshot-Offset strategy.
limit = balance_at_sync + usage_at_sync - SMALL_BUFFER (= B0 - SMALL_BUFFER)
_check_request_block 用 current_usage + reserve >= limit 撞线,语义正确。
current_usage 参数仅用于无同步数据的 fallback 路径,非公式必需。
"""
if not redis_client:
return get_static_limit(clean_model, api_base)
@@ -432,34 +434,34 @@ def get_effective_limit(clean_model, api_base, current_usage=None):
# 1. Get Static Limit (Hard Config)
static_limit = get_static_limit(clean_model, api_base)
# 2. Get Dynamic Stable Limit (Calculated by Daemon)
# This limit is derived from: Peak_Daily_Balance - Buffer
# It is stable and immune to API latency spikes.
stable_limit_key = f"limit:dynamic:volcengine:stable_limit:{clean_model}"
stable_limit_val = redis_client.get(stable_limit_key)
# 2. Get Dynamic Limit from Snapshot
# balance_at_sync + usage_at_sync = 当日初始免费池 B0(常数)
# 累计限额 = B0 - SMALL_BUFFER,两次同步间不变
balance_key = f"limit:dynamic:volcengine:balance_at_sync:{clean_model}"
usage_key = f"limit:dynamic:volcengine:usage_at_sync:{clean_model}"
balance_val = redis_client.get(balance_key)
usage_val = redis_client.get(usage_key)
effective_limit = None
if stable_limit_val is not None:
if balance_val is not None and usage_val is not None:
try:
dynamic_limit = float(stable_limit_val)
effective_limit = calculate_effective_limit(static_limit, dynamic_limit)
balance_at_sync = float(balance_val)
usage_at_sync = float(usage_val)
# 累计限额 = B0 - SMALL_BUFFER,不依赖 current_usage
dynamic_limit = balance_at_sync + usage_at_sync - SMALL_BUFFER
dynamic_limit = max(0, dynamic_limit)
effective_limit = calculate_effective_limit(static_limit, dynamic_limit)
except (ValueError, TypeError):
effective_limit = calculate_effective_limit(static_limit, None)
else:
# Fallback: 如果 daemon 尚未计算 stable limit(首次同步前/新增模型/TTL 过期),
# 用保守兜底(FALLBACK_LIMIT),绝不裸奔到采集上限 —— 免费模型余额常态 < 2M,
# 裸奔 2M 会把余额不足的模型烧穿扣费。
# Fallback: 无同步数据时用保守兜底
fallback = float(static_limit) if static_limit is not None else float(FALLBACK_LIMIT)
fallback = min(fallback, float(FALLBACK_LIMIT))
effective_limit = calculate_effective_limit(fallback, None)
# Legacy fallback (Optional: Enable if you want immediate loose limits before first sync)
# dynamic_remaining = redis_client.get(f"limit:dynamic:volcengine:remaining:{clean_model}")
# if dynamic_remaining and current_usage:
# effective_limit = float(current_usage) + float(dynamic_remaining)
return effective_limit
@@ -902,16 +904,14 @@ class DailyLimitHandler(CustomLogger):
f" | Limit: {int(effective_limit):,}" if effective_limit is not None else ""
)
# Get Dynamic Remaining for Log
dynamic_key = f"limit:dynamic:volcengine:remaining:{clean_model}"
rem_val = redis_client.get(dynamic_key)
rem_display = ""
if rem_val is not None:
# Get Balance At Sync for Log
balance_key = f"limit:dynamic:volcengine:balance_at_sync:{clean_model}"
bal_val = redis_client.get(balance_key)
bal_display = ""
if bal_val is not None:
try:
# Display Real Balance (Remaining + Buffer)
rem_int = int(float(rem_val))
bal_est = rem_int + BUFFER
rem_display = f" | Rem: {bal_est:,}"
bal_int = int(float(bal_val))
bal_display = f" | BalAtSync: {bal_int:,}"
except Exception as e:
print(
f"DEBUG: Error calculating balance for {clean_model}: {e}"
@@ -920,11 +920,11 @@ class DailyLimitHandler(CustomLogger):
else:
# Debug why key is missing
if DEBUG_MODE:
print(f"DEBUG: Dynamic key not found: {dynamic_key}")
print(f"DEBUG: Balance key not found: {balance_key}")
key_display = key.split(":", 2)[-1]
log(
f"📊 [Usage] {key_display}: +{total_tokens} (Today: {current_total:,}{rem_display}{limit_str})",
f"📊 [Usage] {key_display}: +{total_tokens} (Today: {current_total:,}{bal_display}{limit_str})",
level="INFO",
)
@@ -1248,15 +1248,12 @@ def _maybe_trigger_daily_consume():
log(f"❌ 消耗任务调度失败: {e}", level="ERROR")
def compute_sync_interval(min_remaining, is_idle):
"""按最小正剩余余额 + 空闲状态计算同步间隔(秒)。"""
# 余额分档退化:余额驱动的"慢速档"已无意义(日返还模型下余额高 = 常态,
# 返还/过期/消耗随时发生,慢同步只制造检测延迟窗口)。
# 仅保留"低余额加密":<10万 5min、<30万 10min(返还解锁/扣费检测要紧);
# ≥30万 统一 30min,不再细分。真正的慢同步 = is_idle(base×4,上限 60min)。
if min_remaining < 100_000:
def compute_sync_interval(min_balance, is_idle):
"""按最小余额 + 空闲状态计算同步间隔(秒)。
min_balance 是 balance_at_sync 中的最小值(实际余额,非扣 SMALL_BUFFER 后)。"""
if min_balance < 100_000:
base_interval = 300 # 紧急 5 minutes
elif min_remaining < 300_000:
elif min_balance < 400_000:
base_interval = 600 # 低余额 10 minutes
else:
base_interval = 1800 # 常规 30 minutes
@@ -1304,7 +1301,7 @@ def active_monitor_loop():
"Starting Active Monitor Loop (Safety-Margin + Idle Awareness) / 正在启动主动监控循环 (安全余量 + 空闲感知)...",
flush=True,
)
print(f"Buffer / 缓冲: {BUFFER}", flush=True)
print(f"SmallBuffer / 缓冲: {SMALL_BUFFER}", flush=True)
while True:
try:
@@ -1334,11 +1331,11 @@ def active_monitor_loop():
# Calculate delta for this specific model
m_delta = m_usage - last_usage_map.get(m, 0.0)
# Check if this model is considered "low quota"
rem_key = f"limit:dynamic:volcengine:remaining:{m}"
rem_val = redis_client.get(rem_key)
# 未知额度视为充足,避免首次启动时所有模型被误判为"低额度"
remaining = float(rem_val) if rem_val is not None else float(LOW_QUOTA_THRESHOLD)
# Check if this model is considered "low quota"
bal_key = f"limit:dynamic:volcengine:balance_at_sync:{m}"
bal_val = redis_client.get(bal_key)
# 未知额度视为充足,避免首次启动时所有模型被误判为"低额度"
remaining = float(bal_val) if bal_val is not None else float(LOW_QUOTA_THRESHOLD)
if m_delta > 0 and remaining < LOW_QUOTA_THRESHOLD:
any_low_quota_usage = True
@@ -1377,9 +1374,9 @@ def active_monitor_loop():
model_delta = curr_usage - last_usage
# 4. Get Current Balance for Threshold Calculation
rem_key = f"limit:dynamic:volcengine:remaining:{model}"
rem_val = redis_client.get(rem_key)
current_balance = float(rem_val) if rem_val else 0.0
bal_key = f"limit:dynamic:volcengine:balance_at_sync:{model}"
bal_val = redis_client.get(bal_key)
current_balance = float(bal_val) if bal_val else 0.0
# 5. Determine Dynamic Threshold based on Balance
# Rich models get loose limits, Poor models get strict limits.
@@ -1427,19 +1424,16 @@ def active_monitor_loop():
configured_models_found = False
for model_name, balance in model_balances.items():
remaining = balance - BUFFER
print(
f"DEBUG: Checking {model_name} (Bal: {int(balance):,}, Rem: {int(remaining):,}) (余额/剩余)",
f"DEBUG: Checking {model_name} (Bal: {int(balance):,})",
flush=True,
)
# Target Resolution (Already handled in process_packages_breakdown mostly)
# But we'll treat model_name as the target since process_packages_breakdown now handles the list logic
# Target Resolution
targets = [model_name]
for target in targets:
# STRICT FILTER: Only sync models explicitly configured with VolcEngine Base URL
# User Requirement: Do NOT sync or limit models that share names but use other providers (e.g. NewAPI)
if target not in MONITORED_MODELS:
if DEBUG_MODE:
print(
@@ -1448,16 +1442,14 @@ def active_monitor_loop():
)
continue
# Fetch Current Usage from Redis (to calculate Total Pool)
# We need to find the correct hash for this model from MONITORED_MODEL_PAIRS
# Fetch Current Usage from Redis
current_usage = 0
target_hashes = [
p[1] for p in MONITORED_MODEL_PAIRS if p[0] == target
]
# Sum usage across all configured endpoints for this model (usually just one)
# Sum usage across all configured endpoints for this model
for h in target_hashes:
# Replicate get_usage_key logic (Beijing Time UTC+8)
now_ts = time.time()
beijing_time_ts = now_ts + (8 * 3600)
usage_date = time.strftime(
@@ -1468,78 +1460,30 @@ def active_monitor_loop():
if val:
current_usage += float(val)
# Use normalized key for storage
# Format: limit:dynamic:volcengine:remaining:{model_name}
dynamic_key = f"limit:dynamic:volcengine:remaining:{target}"
balance_key = f"limit:dynamic:volcengine:balance_at_sync:{target}"
usage_key = f"limit:dynamic:volcengine:usage_at_sync:{target}"
# ==========================================
# 📏 v5(恢复 v2 骨架):stable_limit = max(0, usage + balance − BUFFER)
# ==========================================
# 论证(2026-08-12 实测修正):
# 1. 「usage + balance = 当日初始免费池」数学正确的前提是 balance 随
# 消耗实时递减 —— 实测(14:59:51 同步)U=1,696,135 + B=596,869
# = 2,293,004 ≈ 守恒初始池,B 在滞后收敛后确实等于 B0 − U,
# 与理论剩余 597,004 几乎一致 → v2 公式本身是对的。
# 2. v4 的 balance − BUFFER 是错误:把「剩余余额」当「从 0 累计的
# 上限」,同步时 U>0 则 stable 低估初始池 → 有真实剩余额度却误拦
# (15:09:51 实测:Bal=596,869 剩 59.7 万却被 usage≥296,869 全拦)。
# 3. v2 残留风险仅剩「同步时点 B 滞后」(13:36 快照 1,558,307 高估
# 真实剩余约 50 万,池虚高)→ 由 BUFFER + 低余额加密同步(600s)
# + burst 触发覆盖;滞后收敛后(1 小时内实测)池回到守恒值。
# 4. 扣费判定 = 消耗 > 当下免费额度即扣费;今日消耗 1,696,135 <
# 初始池 2,293,004 → 无扣费(此前"超余额 13.8 万已扣费"基于滞后
# 快照 1,558,307 的推断不成立)。
# 结论:stable = usage + balance − BUFFER(v2 原式),BUFFER 覆盖
# 同步窗口内的余额滞后。见 docs/volc-quota-notes.md §7.5。
stable_limit = int(max(0, current_usage + balance - BUFFER))
stable_limit_key = (
f"limit:dynamic:volcengine:stable_limit:{target}"
)
# Store stable limit
redis_client.setex(stable_limit_key, 86400, int(stable_limit))
# Store snapshot: balance_at_sync, usage_at_sync
redis_client.setex(balance_key, 86400, int(balance))
redis_client.setex(usage_key, 86400, int(current_usage))
if DEBUG_MODE:
static_limit = get_monitored_static_limit(
target, target_hashes
)
effective_limit = calculate_effective_limit(
static_limit, stable_limit
)
print(
f"DEBUG: {target} Bal={int(balance):,} | Usage={int(current_usage):,} | {format_limit_debug_fields(static_limit=static_limit, stable_limit=stable_limit, effective_limit=effective_limit)}",
f"DEBUG: {target} Bal={int(balance):,} | Usage={int(current_usage):,} | BalAtSync={int(balance):,} UsageAtSync={int(current_usage):,}",
flush=True,
)
if DEBUG_MODE:
print(
f"DEBUG: Checking {target} (Configured / 已配置) (Bal: {int(balance):,}, Rem: {int(remaining):,})",
flush=True,
)
# Set with expiry (24h)
redis_client.setex(dynamic_key, 86400, int(remaining))
if DEBUG_MODE:
print(
f"DEBUG: Set Redis {dynamic_key} = {int(remaining):,}",
flush=True,
)
# 即时恢复:同步发现限额已恢复(usage < effective limit)时立刻解除软踢,
# 不等 restore_blocked_models 的 30min/4h 闸门 —— 否则日返还额度到账后
# 模型仍被禁(有额度但 no healthy deployments),直到下一次恢复检查。
# 即时恢复:同步发现限额已恢复(usage < effective limit)时立刻解除软踢,
restore_model_if_healthy(target, target_hashes)
# Track min remaining for frequency control
# Track min balance for frequency control
configured_models_found = True
# Only track POSITIVE remaining for frequency control
# If a model is 0 (dead), we shouldn't panic-poll the healthy ones.
# We rely on the Restore Loop to check dead models.
if remaining > 0 and remaining < temp_min_remaining:
temp_min_remaining = remaining
if balance > 0 and balance < temp_min_remaining:
temp_min_remaining = balance
# If all models are dead (temp_min_remaining is still inf), set to 0 for logic but handle gracefully
# If we found models but all were <= 0, then temp_min_remaining is inf.