fix: usage collection error no longer kills keys; 401/403 treated as key failure; key3 (renewal cancelled) skipped via accounts.json

This commit is contained in:
hmo
2026-08-03 11:21:12 +08:00
parent be2b194b71
commit f25a15f3bf
2 changed files with 24 additions and 11 deletions
+23 -10
View File
@@ -95,6 +95,8 @@ def load_accounts():
"label": a.get("label", kid), "label": a.get("label", kid),
"api_key": a.get("key", ""), "api_key": a.get("key", ""),
"workspace_id": a.get("workspace_id", ""), "workspace_id": a.get("workspace_id", ""),
# 续订取消/显式标记 → 视为不可用(采集失败时也能正确跳过)
"subscribed": a.get("subscribed", a.get("renewal") != "cancelled"),
}) })
log.info("loaded %d OCG keys from accounts.json", len(keys)) log.info("loaded %d OCG keys from accounts.json", len(keys))
except Exception as e: except Exception as e:
@@ -137,6 +139,12 @@ def refresh_state():
global _keys, _key_usage global _keys, _key_usage
_keys = load_accounts() _keys = load_accounts()
_key_usage = load_usage() _key_usage = load_usage()
# 合并 accounts.json 的订阅标记:采集失败时 usage 缺失该信息,
# 以 accounts.json 的 renewal/subscribed 为准
for k in _keys:
kid = k["key_id"]
if not k.get("subscribed", True):
_key_usage.setdefault(kid, {})["subscribed"] = False
log.info("state refreshed: %d keys, %d with usage data", len(_keys), len(_key_usage)) log.info("state refreshed: %d keys, %d with usage data", len(_keys), len(_key_usage))
@@ -147,16 +155,21 @@ def refresh_state():
def _key_health_score(kid): def _key_health_score(kid):
""" """
计算 key 健康度(越低越好): 计算 key 健康度(越低越好):
- 有 error 或 unsubscribe 的 key 排最末(score = 999 - 已取消订阅的 key 排最末(score = 999
- 在冷却期内的 key 排倒数第二(score = 888 - 在冷却期内的 key 排倒数第二(score = 888
- 用量采集失败(error)仅降权(score = 500),key 仍可用
- 否则按 rolling% → weekly% → monthly% 加权 - 否则按 rolling% → weekly% → monthly% 加权
""" """
usage = _key_usage.get(kid, {}) usage = _key_usage.get(kid, {})
# 订阅取消或采集错误 → 不可用 # 订阅取消 → 不可用
if not usage.get("subscribed", True) or usage.get("error"): if not usage.get("subscribed", True):
return 999 return 999
# 用量采集失败 → 降权但可用(不因采集问题误杀 key)
if usage.get("error"):
return 500
# 故障冷却期内 → 低优先级 # 故障冷却期内 → 低优先级
now = time.time() now = time.time()
fail_until = _key_failures.get(kid, 0) fail_until = _key_failures.get(kid, 0)
@@ -264,7 +277,7 @@ class RouterHandler(BaseHTTPRequestHandler):
"rolling_pct": usage.get("rolling_pct", None), "rolling_pct": usage.get("rolling_pct", None),
"weekly_pct": usage.get("weekly_pct", None), "weekly_pct": usage.get("weekly_pct", None),
"monthly_pct": usage.get("monthly_pct", None), "monthly_pct": usage.get("monthly_pct", None),
"healthy": usage.get("subscribed", True) and not usage.get("error") and (time.time() >= fail_until), "healthy": usage.get("subscribed", True) and (time.time() >= fail_until),
"in_cooldown": time.time() < fail_until, "in_cooldown": time.time() < fail_until,
"error": usage.get("error", False), "error": usage.get("error", False),
"hits": _route_stats.get(kid, 0), "hits": _route_stats.get(kid, 0),
@@ -295,9 +308,9 @@ class RouterHandler(BaseHTTPRequestHandler):
api_key = k.get("api_key", "") api_key = k.get("api_key", "")
if not api_key: if not api_key:
continue continue
# 跳过不可用 key # 跳过不可用 key(采集 error 不跳过,仅降权)
usage = _key_usage.get(kid, {}) usage = _key_usage.get(kid, {})
if not usage.get("subscribed", True) or usage.get("error"): if not usage.get("subscribed", True):
continue continue
if time.time() < _key_failures.get(kid, 0): if time.time() < _key_failures.get(kid, 0):
continue continue
@@ -370,9 +383,9 @@ class RouterHandler(BaseHTTPRequestHandler):
if not api_key: if not api_key:
continue continue
# 检查 key 是否可用 # 检查 key 是否可用(采集 error 不跳过,仅降权)
usage = usage_snapshot.get(kid, {}) usage = usage_snapshot.get(kid, {})
if not usage.get("subscribed", True) or usage.get("error"): if not usage.get("subscribed", True):
continue continue
if time.time() < failures_snapshot.get(kid, 0): if time.time() < failures_snapshot.get(kid, 0):
continue continue
@@ -390,9 +403,9 @@ class RouterHandler(BaseHTTPRequestHandler):
else: else:
last_error = result.get("error", "unknown") last_error = result.get("error", "unknown")
log.warning("%s via %s FAILED: %s", model_name, kid, last_error) log.warning("%s via %s FAILED: %s", model_name, kid, last_error)
# 402/429/5xx → 标记 key 故障 # 401(未授权/CreditsError)/402/403/429/5xx → 标记 key 故障
status = result.get("status", 0) status = result.get("status", 0)
if status in (402, 429) or status >= 500: if status in (401, 402, 403, 429) or status >= 500:
mark_key_failed(kid) mark_key_failed(kid)
except Exception as e: except Exception as e:
last_error = str(e) last_error = str(e)
+1 -1
View File
@@ -77,7 +77,7 @@ DEFAULT_POLL_INTERVAL_SEC = 300 # 5 minutes
CDP_PROXY_URL = os.environ.get('CDP_PROXY_URL', 'http://localhost:3456') CDP_PROXY_URL = os.environ.get('CDP_PROXY_URL', 'http://localhost:3456')
CDP_EVAL_TIMEOUT = 20 CDP_EVAL_TIMEOUT = 20
CDP_TARGETS_TIMEOUT = 5 CDP_TARGETS_TIMEOUT = 5
HTTP_TIMEOUT = 15 HTTP_TIMEOUT = 45 # opencode.ai 响应慢(2026-08 实测 10-12s),放宽到 45s
OPENCODE_BASE_URL = 'https://opencode.ai' OPENCODE_BASE_URL = 'https://opencode.ai'
# ── SSR HTML parsing ── # ── SSR HTML parsing ──