claude-code-proxy / providers /error_mapping.py
Yash030's picture
Better handle rate limit errors that come as 413 or BadRequestError.
c66ebaa
"""Provider-specific exception mapping."""
import httpx
import openai
from core.anthropic import get_user_facing_error_message
from providers.exceptions import (
APIError,
AuthenticationError,
InvalidRequestError,
OverloadedError,
RateLimitError,
)
from providers.rate_limit import GlobalRateLimiter
def user_visible_message_for_mapped_provider_error(
mapped: Exception,
*,
provider_name: str,
read_timeout_s: float | None,
) -> str:
"""Return the user-visible string after :func:`map_error` (405 + mapped types)."""
if getattr(mapped, "status_code", None) == 405:
return (
f"Upstream provider {provider_name} rejected the request method "
"or endpoint (HTTP 405)."
)
return get_user_facing_error_message(mapped, read_timeout_s=read_timeout_s)
def map_error(
e: Exception, *, rate_limiter: GlobalRateLimiter | None = None
) -> Exception:
"""Map OpenAI or HTTPX exception to specific ProviderError.
Streaming transports should pass their scoped limiter (``self._global_rate_limiter``)
so reactive 429 handling applies to the correct provider. Tests may omit
``rate_limiter`` to use the process-wide singleton.
"""
from loguru import logger
message = get_user_facing_error_message(e)
logger.info(
"map_error: original_exc_type={} message={}", type(e).__name__, message[:100]
)
limiter = rate_limiter or GlobalRateLimiter.get_instance()
if isinstance(e, openai.AuthenticationError):
logger.info("map_error: mapped to AuthenticationError")
return AuthenticationError(message, raw_error=str(e))
if isinstance(e, openai.RateLimitError):
limiter.set_blocked(60)
logger.info("map_error: mapped to RateLimitError")
return RateLimitError(message, raw_error=str(e))
if isinstance(e, openai.BadRequestError):
# Check if it's actually a rate limit in disguise
raw = str(e).lower()
if "too_many_requests" in raw or "rate_limit" in raw or "quota" in raw:
limiter.set_blocked(60)
logger.info("map_error: BadRequestError actually rate limit")
return RateLimitError(message, raw_error=str(e))
logger.info("map_error: mapped to InvalidRequestError")
return InvalidRequestError(message, raw_error=str(e))
if isinstance(e, openai.InternalServerError):
raw_message = str(e)
if "overloaded" in raw_message.lower() or "capacity" in raw_message.lower():
logger.info("map_error: mapped to OverloadedError")
return OverloadedError(message, raw_error=raw_message)
logger.info("map_error: mapped to APIError (InternalServerError)")
return APIError(message, status_code=500, raw_error=str(e))
if isinstance(e, openai.APIError):
logger.info("map_error: mapped to APIError (openai.APIError)")
return APIError(
message, status_code=getattr(e, "status_code", 500), raw_error=str(e)
)
if isinstance(e, httpx.HTTPStatusError):
status = e.response.status_code
logger.info("map_error: httpx.HTTPStatusError status={}", status)
if status in (401, 403):
logger.info("map_error: mapped to AuthenticationError (httpx)")
return AuthenticationError(message, raw_error=str(e))
if status == 429:
limiter.set_blocked(60)
logger.info("map_error: mapped to RateLimitError (httpx)")
return RateLimitError(message, raw_error=str(e))
if status == 413:
# "Request too large" - often actually a rate limit or quota issue
limiter.set_blocked(60)
logger.info("map_error: mapped to RateLimitError (413)")
return RateLimitError(message, raw_error=str(e))
if status == 400:
logger.info("map_error: mapped to InvalidRequestError (httpx)")
return InvalidRequestError(message, raw_error=str(e))
# Check response body for rate limit indicators
try:
body = e.response.json()
if body and isinstance(body, dict):
err_type = body.get("type", "")
if "too_many_requests" in err_type or "rate_limit" in err_type.lower():
limiter.set_blocked(60)
logger.info("map_error: detected rate limit from response body")
return RateLimitError(message, raw_error=str(e))
except Exception:
pass
if status >= 500:
if status in (502, 503, 504):
logger.info("map_error: mapped to OverloadedError (httpx)")
return OverloadedError(message, raw_error=str(e))
logger.info("map_error: mapped to APIError (httpx 5xx)")
return APIError(message, status_code=status, raw_error=str(e))
logger.info("map_error: mapped to APIError (httpx)")
return APIError(message, status_code=status, raw_error=str(e))
logger.info("map_error: falling through, returning original exception")
return e