fix(gateway): 熔断计入持续 4xx + 去 assert + Protocol/transient 去重
P1-2 非可重试错误(持续 401/403)也 record_failure,坏 key 可触发熔断。 P1-9 _complete_structured 的 assert 改显式 raise ValueError(-O 安全)。 P2 GatewayRun 抽到 orchestrator/_protocols.py 单点(去 4 处重复); _is_transient 抽到 adapters/base.py is_transient_by_name(去 3 处重复); Gemini Protocol 改 async def;gateway._retrying 去无用 async。
This commit is contained in:
@@ -52,6 +52,19 @@ def _is_retryable(exc: BaseException) -> bool:
|
||||
return isinstance(exc, AppError) and exc.code == ErrorCode.RATE_LIMITED
|
||||
|
||||
|
||||
# 持续性鉴权错误状态码:坏 key / 被禁用 → 每次重打都失败,应计入熔断(P1-2)。
|
||||
_PERSISTENT_AUTH_STATUSES = frozenset({401, 403})
|
||||
|
||||
|
||||
def _is_persistent_auth_error(exc: BaseException) -> bool:
|
||||
"""持续性鉴权失败(401/403):错误 key / 账号被禁,重打无意义 → 计入熔断。
|
||||
|
||||
按 `status_code` 属性识别(不硬依赖任何厂商 SDK 异常类型)。
|
||||
"""
|
||||
status = getattr(exc, "status_code", None)
|
||||
return isinstance(status, int) and status in _PERSISTENT_AUTH_STATUSES
|
||||
|
||||
|
||||
def _input_len(req: LlmRequest) -> int:
|
||||
if isinstance(req.input, str):
|
||||
return len(req.input)
|
||||
@@ -187,7 +200,7 @@ class Gateway:
|
||||
project_id=str(req.scope.project_id) if req.scope.project_id else None,
|
||||
)
|
||||
|
||||
async def _retrying(self) -> AsyncRetrying:
|
||||
def _retrying(self) -> AsyncRetrying:
|
||||
return AsyncRetrying(
|
||||
stop=stop_after_attempt(self._max_retries + 1),
|
||||
wait=wait_exponential(min=_RETRY_MIN_SECONDS, max=_RETRY_MAX_SECONDS),
|
||||
@@ -212,6 +225,10 @@ class Gateway:
|
||||
result = await self._complete_with_retry(adapter, req, route.model)
|
||||
except Exception as exc: # noqa: BLE001 — 链内逐 provider 兜底,最终统一上抛
|
||||
if not _is_retryable(exc):
|
||||
# 持续性鉴权失败(401/403)虽不可重试,但坏 key 应触发熔断(P1-2),
|
||||
# 否则每次都白打同一坏 provider。其它不可重试错误直接上抛。
|
||||
if _is_persistent_auth_error(exc):
|
||||
self._breaker.record_failure(route.provider)
|
||||
raise
|
||||
self._breaker.record_failure(route.provider)
|
||||
last_error = exc
|
||||
@@ -240,7 +257,7 @@ class Gateway:
|
||||
async def _complete_with_retry(
|
||||
self, adapter: ProviderAdapter, req: LlmRequest, model: str
|
||||
) -> ProviderResult:
|
||||
retrying = await self._retrying()
|
||||
retrying = self._retrying()
|
||||
async for attempt in retrying:
|
||||
with attempt:
|
||||
return await adapter.complete(req, model)
|
||||
|
||||
Reference in New Issue
Block a user