"""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