File size: 5,102 Bytes
0157ac7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
860ed88
 
0157ac7
860ed88
 
 
0157ac7
 
 
860ed88
0157ac7
 
 
860ed88
0157ac7
 
c66ebaa
 
 
 
 
 
860ed88
0157ac7
 
 
 
860ed88
0157ac7
860ed88
0157ac7
 
860ed88
0157ac7
 
 
 
 
 
860ed88
0157ac7
860ed88
0157ac7
 
 
860ed88
0157ac7
c66ebaa
 
 
 
 
0157ac7
860ed88
0157ac7
c66ebaa
 
 
 
 
 
 
 
 
 
 
0157ac7
 
860ed88
0157ac7
860ed88
0157ac7
860ed88
0157ac7
 
860ed88
0157ac7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""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