This commit is contained in:
Jackson Riding 2026-08-26 14:35:32 -04:00 committed by GitHub
commit a61e1056ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 83 additions and 5 deletions

View file

@ -399,6 +399,7 @@ budget_duration: Optional[str] = (
)
default_soft_budget: float = DEFAULT_SOFT_BUDGET # by default all litellm proxy keys have a soft budget of 50.0
budget_exceeded_throttle_percentage: Optional[float] = None
budget_exceeded_error_message: Optional[str] = None
forward_traceparent_to_llm_provider: bool = False

View file

@ -1640,6 +1640,7 @@ LITELLM_SETTINGS_SAFE_DB_OVERRIDES: Final = [
"cost_margin_config",
"block_requests_for_models_without_pricing",
"budget_exceeded_throttle_percentage",
"budget_exceeded_error_message",
# Every field editable from the Admin UI (proxy_server._GENERAL_SETTINGS_UI_LITELLM_FIELDS)
# must be listed here so a DB write from one worker overrides the live litellm attribute on
# the others when config reloads; otherwise peer workers stay on their startup value.

View file

@ -985,6 +985,12 @@ class BudgetExceededError(Exception):
self.message = message
super().__init__(message)
@property
def client_facing_message(self) -> str:
import litellm
return litellm.budget_exceeded_error_message or self.message
## DEPRECATED ##
class InvalidRequestError(openai.BadRequestError):

View file

@ -1137,7 +1137,7 @@ class MCPRequestHandler:
except (HTTPException, ProxyException):
raise
except litellm.BudgetExceededError as e:
raise HTTPException(status_code=getattr(e, "status_code", 429), detail=str(e)) from None
raise HTTPException(status_code=getattr(e, "status_code", 429), detail=e.client_facing_message) from None
except Exception as e: # noqa: BLE001 # untyped gate failure: retryable 503 for a DB outage, else fail closed 401
MCPRequestHandler._raise_503_if_db_unavailable(e)
raise HTTPException(status_code=401, detail="Invalid or expired credential") from None

View file

@ -169,7 +169,7 @@ class UserAPIKeyAuthExceptionHandler:
if isinstance(e, litellm.BudgetExceededError):
raise ProxyException(
message=e.message,
message=e.client_facing_message,
type=ProxyErrorTypes.budget_exceeded,
param=None,
code=getattr(e, "status_code", status.HTTP_429_TOO_MANY_REQUESTS),

View file

@ -45,10 +45,11 @@ from litellm.litellm_core_utils.llm_response_utils.get_headers import (
get_response_headers,
)
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
from litellm.litellm_core_utils.streaming_handler import (
backfill_missing_cache_usage_fields,
)
from litellm.proxy._types import ProxyException, UserAPIKeyAuth
from litellm.proxy.auth.auth_checks import can_key_call_resolved_model
from litellm.proxy.auth.auth_utils import check_response_size_is_safe
from litellm.proxy.common_utils.callback_utils import (
@ -3291,6 +3292,15 @@ class ProxyBaseLLMRequestProcessing:
code=status.HTTP_400_BAD_REQUEST,
headers=safe_headers,
)
if isinstance(e, litellm.BudgetExceededError):
raise ProxyException(
message=e.client_facing_message,
type=ProxyErrorTypes.budget_exceeded,
param=None,
code=e.status_code,
headers=safe_headers,
)
# Extract status_code from the exception if it carries one.
# Provider exceptions (NotFoundError, BadRequestError, GeminiError,
# VertexAIError, etc.) all have a status_code attribute reflecting

View file

@ -237,7 +237,7 @@ async def _authorize_models_this_test_can_call(
)
except BudgetExceededError as e:
raise ProxyException(
message=e.message,
message=e.client_facing_message,
type=ProxyErrorTypes.budget_exceeded,
param=None,
code=status.HTTP_400_BAD_REQUEST,

View file

@ -5669,15 +5669,17 @@ class TestMCPDcrBridgeDelegateAdmission:
)
return exc_info.value
async def test_over_budget_admission_surfaces_429_not_401(self):
async def test_over_budget_admission_surfaces_429_not_401(self, monkeypatch):
"""A validly-authenticated but over-budget identity surfaces the standard pipeline's 429, not
a misleading 401. Flattening budget to 401 told the caller their credential was invalid, which
on a DCR client reads as broken auth and triggers a re-authorize that cannot fix a budget
problem. Regression for the status-flattening finding on the live-policy gate."""
import litellm
monkeypatch.setattr(litellm, "budget_exceeded_error_message", "Allowance reached")
mapped = await self._enforce_with_gate_error(litellm.BudgetExceededError(current_cost=10.0, max_budget=1.0))
assert mapped.status_code == 429
assert mapped.detail == "Allowance reached"
async def test_db_outage_during_policy_surfaces_503_not_401(self):
"""A transient database outage during the live-policy gate surfaces a retryable 503, not a 401

View file

@ -25,6 +25,7 @@ from prisma.errors import (
)
import litellm
from litellm._logging import verbose_proxy_logger
from litellm.exceptions import BudgetExceededError
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
@ -353,6 +354,41 @@ async def test_handle_authentication_error_budget_exceeded():
assert int(exc_info.value.code) == status.HTTP_429_TOO_MANY_REQUESTS
@pytest.mark.asyncio
async def test_handle_authentication_error_budget_exceeded_custom_message(monkeypatch):
from litellm.exceptions import BudgetExceededError
monkeypatch.setattr(
litellm,
"budget_exceeded_error_message",
"Your AI usage allowance has been reached. Please contact the AI team.",
)
budget_error = BudgetExceededError(
message="Budget has been exceeded! Current cost: 10.51, Max budget: 10.00",
current_cost=10.51,
max_budget=10.00,
)
with pytest.raises(ProxyException) as exc_info:
await UserAPIKeyAuthExceptionHandler()._handle_authentication_error(
budget_error,
MagicMock(),
{},
"/v1/chat/completions",
None,
"sk-test",
)
assert (
exc_info.value.message
== "Your AI usage allowance has been reached. Please contact the AI team."
)
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
assert int(exc_info.value.code) == status.HTTP_429_TOO_MANY_REQUESTS
assert "10.51" in budget_error.message
@pytest.mark.asyncio
async def test_route_passed_to_post_call_failure_hook():
"""

View file

@ -3314,6 +3314,28 @@ class TestHandleLLMApiExceptionRetryAfter:
assert proxy_exc.headers["retry-after"] == "43"
assert proxy_exc.headers["x-custom"] == "1"
async def test_handle_llm_api_exception_budget_exceeded_uses_custom_message(
self, monkeypatch
):
monkeypatch.setattr(
litellm, "budget_exceeded_error_message", "Allowance reached, contact the AI team."
)
exc = litellm.BudgetExceededError(current_cost=10.51, max_budget=10.0)
proxy_exc = await self._invoke(exc)
assert proxy_exc.message == "Allowance reached, contact the AI team."
assert proxy_exc.type == "budget_exceeded"
assert proxy_exc.code == "429"
async def test_handle_llm_api_exception_budget_exceeded_defaults_to_detail(self):
exc = litellm.BudgetExceededError(current_cost=10.51, max_budget=10.0)
proxy_exc = await self._invoke(exc)
assert "10.51" in proxy_exc.message
assert proxy_exc.type == "budget_exceeded"
class TestHandleLLMApiExceptionFramingHeaders:
"""HTTP-framing headers on the provider exception must be stripped before the