fix: classify provider code-only errors into structured failover reasons
A provider error body that carries only a structured code and no HTTP status (Gemini UNAVAILABLE / DEADLINE_EXCEEDED / INTERNAL, Anthropic API_ERROR, OpenAI SERVER_ERROR) fell through every classifier stage to FailoverReason.unknown, so the retry loop treated a provider overload or timeout as a generic retryable failure and never reached the overload/timeout recovery hints. Add a per-provider code table consulted after the shared _ERROR_CODE_VERDICTS map; codes are scoped to their provider family so a same-named code from another backend stays unknown. Fixes #70414 Salvages #70425 (@ooiuuii) onto the rule-table classifier layout.
This commit is contained in:
@@ -596,6 +596,19 @@ _ERROR_CODE_VERDICTS: Dict[str, Verdict] = {
|
||||
"invalid_encrypted_content": _V_INVALID_ENCRYPTED,
|
||||
}
|
||||
|
||||
# Provider-native status codes that arrive as a bare ``{"error": {"code": …}}`` body
|
||||
# (no HTTP status, no prose): gRPC canonical names from Gemini, Anthropic error
|
||||
# types, OpenAI's ``server_error``. Scoped per provider so a coincidentally named
|
||||
# code from another backend stays ``unknown`` (#70414). Provider aliases collapse
|
||||
# to the family key before lookup.
|
||||
_PROVIDER_CODE_FAMILIES = {"openai-codex": "openai", "google": "gemini", "google-gemini": "gemini",
|
||||
"google-ai-studio": "gemini", "vertex": "gemini", "google-vertex": "gemini"}
|
||||
_PROVIDER_CODE_VERDICTS: Dict[str, Dict[str, Verdict]] = {
|
||||
"openai": {"server_error": _V_SERVER_ERROR},
|
||||
"gemini": {"unavailable": _V_OVERLOADED, "deadline_exceeded": _V_TIMEOUT, "internal": _V_SERVER_ERROR},
|
||||
"anthropic": {"api_error": _V_SERVER_ERROR, "rate_limit_error": _V_RATE_LIMIT},
|
||||
}
|
||||
|
||||
# Generic ``invalid_request_error`` is deliberately NOT a 400 validation
|
||||
# signal — OpenAI stamps it on genuine overflow 400s too.
|
||||
_400_VALIDATION_CODES = {"unknown_parameter", "unsupported_parameter"}
|
||||
@@ -770,7 +783,11 @@ def _by_error_code(c: _Ctx) -> Optional[Verdict]:
|
||||
# HTTP 200: retrying cannot succeed, a configured fallback still may.
|
||||
if c.code == PROVIDER_STREAM_NON_JSON_ERROR_CODE and "request validation failed:" in c.msg:
|
||||
return _V_FORMAT_ERROR
|
||||
return _ERROR_CODE_VERDICTS.get(c.code)
|
||||
verdict = _ERROR_CODE_VERDICTS.get(c.code)
|
||||
if verdict is None:
|
||||
family = _PROVIDER_CODE_FAMILIES.get(c.provider_slug, c.provider_slug)
|
||||
verdict = _PROVIDER_CODE_VERDICTS.get(family, {}).get(c.code)
|
||||
return verdict
|
||||
|
||||
|
||||
def _by_message(c: _Ctx) -> Optional[Verdict]:
|
||||
|
||||
@@ -1393,6 +1393,29 @@ class TestSSLCertVerificationFailFast:
|
||||
|
||||
# ── Test: RateLimitError without status_code (Copilot/GitHub Models) ──────────
|
||||
|
||||
class TestProviderCodeOnlyErrors:
|
||||
"""Bare ``{"error": {"code": …}}`` bodies with no HTTP status map to the
|
||||
provider's structured reason instead of ``unknown`` (#70414)."""
|
||||
|
||||
@pytest.mark.parametrize("provider, code, reason", [
|
||||
("gemini", "UNAVAILABLE", FailoverReason.overloaded),
|
||||
("google", "DEADLINE_EXCEEDED", FailoverReason.timeout),
|
||||
("vertex", "INTERNAL", FailoverReason.server_error),
|
||||
("anthropic", "API_ERROR", FailoverReason.server_error),
|
||||
("openai-codex", "SERVER_ERROR", FailoverReason.server_error),
|
||||
])
|
||||
def test_provider_native_code_maps_to_structured_reason(self, provider, code, reason):
|
||||
e = MockAPIError(code, body={"error": {"code": code}})
|
||||
result = classify_api_error(e, provider=provider)
|
||||
assert result.reason == reason
|
||||
assert result.retryable is True
|
||||
assert result.should_rotate_credential is False
|
||||
|
||||
def test_code_meaning_does_not_leak_across_providers(self):
|
||||
e = MockAPIError("UNAVAILABLE", body={"error": {"code": "UNAVAILABLE"}})
|
||||
assert classify_api_error(e, provider="openai").reason == FailoverReason.unknown
|
||||
|
||||
|
||||
class TestRateLimitErrorWithoutStatusCode:
|
||||
"""Regression tests for the Copilot/GitHub Models edge case where the
|
||||
OpenAI SDK raises RateLimitError but does not populate .status_code."""
|
||||
|
||||
Reference in New Issue
Block a user