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:
@@ -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)
|
||||||
|
|||||||
@@ -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 ──
|
||||||
|
|||||||
Reference in New Issue
Block a user