diff --git a/litellm/litellm_core_utils/core_helpers.py b/litellm/litellm_core_utils/core_helpers.py index 1089499a64f..9d5b0c86da6 100644 --- a/litellm/litellm_core_utils/core_helpers.py +++ b/litellm/litellm_core_utils/core_helpers.py @@ -58,7 +58,17 @@ def safe_divide( return numerator / denominator +def _is_proxy_rejection(exception: BaseException) -> bool: + try: + from starlette.exceptions import HTTPException + except ImportError: + return False + return isinstance(exception, HTTPException) + + def _is_provider_originated(exception: BaseException) -> bool: + if _is_proxy_rejection(exception): + return False if getattr(exception, "llm_provider", None): return True from litellm.llms.base_llm.chat.transformation import BaseLLMException @@ -74,6 +84,8 @@ def is_expected_client_error(exception: BaseException | None) -> bool: client error and keeps its traceback: a mapped litellm exception carries ``llm_provider``, and the raw ``BaseLLMException`` that provider handlers raise before mapping (the /v1/messages route surfaces it as-is) is one too. + The proxy's own limiters raise ``HTTPException`` subclasses that also carry + an ``llm_provider``, so any ``HTTPException`` stays a proxy rejection. ProxyException stores the status on .code (as a str), HTTPException and litellm exceptions on .status_code. diff --git a/tests/test_litellm/litellm_core_utils/test_core_helpers.py b/tests/test_litellm/litellm_core_utils/test_core_helpers.py index efbf7192a0e..a1f3f5eac70 100644 --- a/tests/test_litellm/litellm_core_utils/test_core_helpers.py +++ b/tests/test_litellm/litellm_core_utils/test_core_helpers.py @@ -286,6 +286,7 @@ class TestIsExpectedClientError: from litellm.exceptions import AuthenticationError, RateLimitError from litellm.litellm_core_utils.core_helpers import is_expected_client_error from litellm.llms.anthropic.common_utils import AnthropicError + from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError provider_auth_failure = AuthenticationError( message="AnthropicException - API key is invalid.", llm_provider="anthropic", model="claude-haiku-4-5" @@ -298,6 +299,12 @@ class TestIsExpectedClientError: unmapped_provider_failure = AnthropicError(status_code=401, message='{"type":"authentication_error"}') assert is_expected_client_error(unmapped_provider_failure) is False + proxy_rate_limit = ProxyRateLimitError( + detail={"error": "Max parallel requests reached"}, model="claude-haiku-4-5", llm_provider="anthropic" + ) + assert proxy_rate_limit.llm_provider == "anthropic" + assert is_expected_client_error(proxy_rate_limit) is True + class RouterRejection(Exception): def __init__(self): self.status_code = 429