mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(logging): keep the proxy's own budget rejection an expected client error
The auth handler stamps the requested model's provider onto BudgetExceededError before logging it, which made a key-over-budget 429 look provider-originated and regain its traceback (and an OTel stack_trace) after the provider 4xx carve-out. Any exception whose unified rate-limit category names litellm's own limiter is now a proxy rejection, matching the HTTPException rule.
This commit is contained in:
parent
a6e1708e6c
commit
514cae1b3d
3 changed files with 51 additions and 1 deletions
|
|
@ -58,7 +58,18 @@ def safe_divide(
|
|||
return numerator / denominator
|
||||
|
||||
|
||||
def _is_litellm_limit_rejection(exception: BaseException) -> bool:
|
||||
from litellm.exceptions import RateLimitErrorCategory
|
||||
|
||||
litellm_limit_categories: Final = frozenset(
|
||||
(RateLimitErrorCategory.LITELLM_RATE_LIMIT.value, RateLimitErrorCategory.LITELLM_BATCH_RATE_LIMIT.value)
|
||||
)
|
||||
return getattr(exception, "category", None) in litellm_limit_categories
|
||||
|
||||
|
||||
def _is_proxy_rejection(exception: BaseException) -> bool:
|
||||
if _is_litellm_limit_rejection(exception):
|
||||
return True
|
||||
try:
|
||||
from starlette.exceptions import HTTPException
|
||||
except ImportError:
|
||||
|
|
@ -85,7 +96,10 @@ def is_expected_client_error(exception: BaseException | None) -> bool:
|
|||
``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.
|
||||
an ``llm_provider``, so any ``HTTPException`` stays a proxy rejection, and
|
||||
so does any exception whose unified rate-limit ``category`` names litellm's
|
||||
own limiter (``BudgetExceededError`` is a plain ``Exception`` that the auth
|
||||
handler decorates with the requested model's provider).
|
||||
|
||||
ProxyException stores the status on .code (as a str), HTTPException and
|
||||
litellm exceptions on .status_code.
|
||||
|
|
|
|||
|
|
@ -311,3 +311,26 @@ class TestIsExpectedClientError:
|
|||
self.llm_provider = ""
|
||||
|
||||
assert is_expected_client_error(RouterRejection()) is True
|
||||
|
||||
def test_budget_rejection_decorated_with_provider_is_expected(self):
|
||||
"""The auth handler stamps the requested model's provider onto the proxy's
|
||||
own BudgetExceededError before logging it, which must not turn a key-over-budget
|
||||
429 into a provider error that keeps its traceback."""
|
||||
from litellm.exceptions import BudgetExceededError, RateLimitError, RateLimitErrorCategory
|
||||
from litellm.litellm_core_utils.core_helpers import is_expected_client_error
|
||||
|
||||
over_budget = BudgetExceededError(current_cost=0.01, max_budget=0.0, llm_provider="anthropic")
|
||||
assert over_budget.llm_provider == "anthropic"
|
||||
assert is_expected_client_error(over_budget) is True
|
||||
|
||||
litellm_limit = RateLimitError(
|
||||
message="key over rpm", llm_provider="anthropic", model="claude-haiku-4-5",
|
||||
category=RateLimitErrorCategory.LITELLM_RATE_LIMIT,
|
||||
)
|
||||
assert is_expected_client_error(litellm_limit) is True
|
||||
|
||||
vendor_limit = RateLimitError(
|
||||
message="rate limited upstream", llm_provider="anthropic", model="claude-haiku-4-5",
|
||||
category=RateLimitErrorCategory.VENDOR_RATE_LIMIT,
|
||||
)
|
||||
assert is_expected_client_error(vendor_limit) is False
|
||||
|
|
|
|||
|
|
@ -5746,6 +5746,19 @@ def test_get_error_information_keeps_traceback_for_unmapped_provider_4xx():
|
|||
assert "test_litellm_logging" in result["traceback"]
|
||||
|
||||
|
||||
def test_get_error_information_skips_traceback_for_budget_rejection_with_provider():
|
||||
"""A key-over-budget 429 is the proxy's own rejection even after the auth
|
||||
handler stamps the requested model's provider onto it, so it stays cheap."""
|
||||
from litellm.litellm_core_utils.litellm_logging import StandardLoggingPayloadSetup
|
||||
|
||||
assert litellm.log_client_error_tracebacks is False
|
||||
over_budget = _raise_and_catch(litellm.BudgetExceededError(current_cost=0.01, max_budget=0.0, llm_provider="anthropic"))
|
||||
result = StandardLoggingPayloadSetup.get_error_information(over_budget)
|
||||
assert result["error_code"] == "429"
|
||||
assert result["llm_provider"] == "anthropic"
|
||||
assert result["traceback"] == ""
|
||||
|
||||
|
||||
def test_failure_handler_helper_fn_builds_payload_once_per_exception():
|
||||
"""Regression for LIT-6043: async and sync failure handlers both call
|
||||
_failure_handler_helper_fn for the same failed request; the standardized
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue