From a83ae177cbf2b4e31764a6e5d9f4c89d01460d3c Mon Sep 17 00:00:00 2001 From: hmo Date: Wed, 12 Aug 2026 16:08:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20ocg=5Frouter=20error=20classification=20?= =?UTF-8?q?=E2=80=94=20400=20client=20error=20no=20retry,=205xx/Router.Una?= =?UTF-8?q?vailable=20no=20longer=20kill=20keys=20into=20cooldown,=20clien?= =?UTF-8?q?t=20disconnect=20handled,=20max=20parallel=2016?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gateway/scripts/ocg_router.py | 46 ++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/gateway/scripts/ocg_router.py b/gateway/scripts/ocg_router.py index 9d37a79..f95a3bf 100644 --- a/gateway/scripts/ocg_router.py +++ b/gateway/scripts/ocg_router.py @@ -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) - success = False - break - mark_key_failed(kid, last_error) + 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 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)