fix: ocg_router error classification — 400 client error no retry, 5xx/Router.Unavailable no longer kill keys into cooldown, client disconnect handled, max parallel 16

This commit is contained in:
hmo
2026-08-12 16:08:14 +08:00
parent 44b77be525
commit a83ae177cb
+30 -12
View File
@@ -461,20 +461,31 @@ class RouterHandler(BaseHTTPRequestHandler):
else:
last_error = result.get("error", "unknown")
log.warning("%s via %s FAILED: %s", model_name, kid, last_error)
# 401(未授权/CreditsError)/402/403/429/5xx → 标记 key 故障
# 错误分类:只有真正的 key 级错误才冷却 key
# - 400 客户端错误:不重试、不冷却(换 key 无意义),直接返回给调用方
# - 401/403 认证失败:key 级故障 → 冷却 300s
# - 402/429 配额/限流:key 级 → 冷却 60s
# - 5xx/Router.Unavailable:上游服务问题 → 不冷却 key(误杀元凶),立即失败
status = result.get("status", 0)
if status in (401, 402, 403, 429) or status >= 500:
if "Router.Unavailable" in last_error:
# 上游模型级故障:换 key 无意义 → 全体短冷却并立即失败返回
with _state_lock:
until = time.time() + UPSTREAM_COOLDOWN_SEC
for k2 in _keys:
_key_failures[k2["key_id"]] = until
_key_failure_reason[k2["key_id"]] = last_error
log.warning("upstream Router.Unavailable → all keys short-cooldown %ds", UPSTREAM_COOLDOWN_SEC)
if status == -1:
# 客户端已断开:直接终止,不重试不冷却
log.info("client disconnected during %s via %s, aborting", model_name, kid)
success = False
break
if status == 400:
log.warning("client error 400, returning to caller without retry")
success = False
break
if status in (401, 403):
mark_key_failed(kid, last_error, cooldown=FAIL_COOLDOWN_SEC)
elif status in (402, 429):
mark_key_failed(kid, last_error, cooldown=60)
elif status >= 500:
# 上游服务端错误(Internal server error / Router.Unavailable):
# 不杀 key。立即失败返回,避免"全部 key 被误杀进冷却"的雪崩。
log.warning("upstream %d (service-level) via %s — NOT cooling key, failing request", status, kid)
success = False
break
mark_key_failed(kid, last_error)
except Exception as e:
last_error = str(e)
log.warning("%s via %s exception: %s", model_name, kid, e)
@@ -523,6 +534,9 @@ class RouterHandler(BaseHTTPRequestHandler):
except HTTPError as e:
err_body = e.read().decode("utf-8", errors="replace")[:500]
return {"ok": False, "status": e.code, "error": f"HTTP {e.code}: {err_body}"}
except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError) as e:
# 客户端断开:不是 key 问题,标记特殊错误码让调用方不重试不冷却
return {"ok": False, "status": -1, "error": f"client_disconnected: {e}"}
except URLError as e:
return {"ok": False, "status": 0, "error": f"URLError: {e.reason}"}
except Exception as e:
@@ -652,8 +666,12 @@ def main():
refresh_thread.start()
# 启动 HTTP 服务(ThreadingHTTPServer — 多线程,避免大请求阻塞 /api/status 等监控端点)
# daemon_threads=True:客户端断开/线程卡住不会阻塞服务退出
server = ThreadingHTTPServer((LISTEN_HOST, LISTEN_PORT), RouterHandler)
log.info("ocg_router 启动 → http://%s:%d", LISTEN_HOST, LISTEN_PORT)
server.daemon_threads = True
# 限制最大并发连接数(每个连接一个线程;信号量在 handler 层控制)
MAX_CONCURRENT = 16
log.info("ocg_router 启动 → http://%s:%d (max_parallel=%d)", LISTEN_HOST, LISTEN_PORT, MAX_CONCURRENT)
log.info("keys loaded: %d (%d healthy)", len(_keys),
sum(1 for k in _keys if _key_health_score(k["key_id"]) < 888))
log.info("fail cooldown: %ds, usage refresh: %ds", FAIL_COOLDOWN_SEC, USAGE_REFRESH_INTERVAL)