mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(proxy): return 422 instead of 429 for BudgetExceededError
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
58065d46fd
commit
f9244749e0
8 changed files with 17 additions and 17 deletions
|
|
@ -1002,7 +1002,7 @@ class BudgetExceededError(Exception):
|
|||
):
|
||||
self.current_cost = current_cost
|
||||
self.max_budget = max_budget
|
||||
self.status_code = 429
|
||||
self.status_code = 422
|
||||
self.llm_provider = llm_provider or ""
|
||||
self.entity_type = entity_type
|
||||
self.entity_id = entity_id
|
||||
|
|
|
|||
|
|
@ -1154,7 +1154,7 @@ class MCPRequestHandler:
|
|||
|
||||
Failures surface with the status the standard pipeline would give them, mirroring
|
||||
``UserAPIKeyAuthExceptionHandler``: a disallowed route is the route gate's own 403, an
|
||||
over-budget identity is a 429, a sub-check that raised its own ``HTTPException``/
|
||||
over-budget identity is a 422, a sub-check that raised its own ``HTTPException``/
|
||||
``ProxyException`` keeps that status, a transient database outage is a retryable 503, and
|
||||
only a genuinely unresolvable failure (a blocked team/project raises a bare ``Exception``,
|
||||
same as the standard pipeline's fallback) becomes the fail-closed 401. Collapsing every
|
||||
|
|
|
|||
|
|
@ -3033,7 +3033,7 @@ def test_get_error_information_budget_exceeded_structured_fields():
|
|||
assert result["error_budget_entity_id"] == "repro-user"
|
||||
assert result["error_budget_limit"] == 1e-06
|
||||
assert result["error_budget_spend"] == 3.4e-05
|
||||
assert result["error_code"] == "429"
|
||||
assert result["error_code"] == "422"
|
||||
assert result["error_class"] == "BudgetExceededError"
|
||||
assert result["error_rate_limit_type"] == "budget"
|
||||
|
||||
|
|
@ -6407,7 +6407,7 @@ def test_get_error_information_keeps_traceback_for_unmapped_provider_4xx():
|
|||
|
||||
|
||||
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
|
||||
"""A key-over-budget 422 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
|
||||
|
||||
|
|
@ -6416,7 +6416,7 @@ def test_get_error_information_skips_traceback_for_budget_rejection_with_provide
|
|||
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["error_code"] == "422"
|
||||
assert result["llm_provider"] == "anthropic"
|
||||
assert result["traceback"] == ""
|
||||
|
||||
|
|
|
|||
|
|
@ -6339,15 +6339,15 @@ class TestMCPDcrBridgeDelegateAdmission:
|
|||
)
|
||||
return exc_info.value
|
||||
|
||||
async def test_over_budget_admission_surfaces_429_not_401(self):
|
||||
"""A validly-authenticated but over-budget identity surfaces the standard pipeline's 429, not
|
||||
async def test_over_budget_admission_surfaces_422_not_401(self):
|
||||
"""A validly-authenticated but over-budget identity surfaces the standard pipeline's 422, 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
|
||||
|
||||
mapped = await self._enforce_with_gate_error(litellm.BudgetExceededError(current_cost=10.0, max_budget=1.0))
|
||||
assert mapped.status_code == 429
|
||||
assert mapped.status_code == 422
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -448,7 +448,7 @@ async def test_handle_authentication_error_budget_exceeded():
|
|||
)
|
||||
|
||||
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
|
||||
assert int(exc_info.value.code) == status.HTTP_429_TOO_MANY_REQUESTS
|
||||
assert int(exc_info.value.code) == status.HTTP_422_UNPROCESSABLE_CONTENT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -687,7 +687,7 @@ def _http_request(client_host: str | None = "10.1.2.3", headers: dict[str, str]
|
|||
{"allow_requests_on_db_unavailable": False},
|
||||
{},
|
||||
"10.1.2.3",
|
||||
id="429_budget_exceeded",
|
||||
id="422_budget_exceeded",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
|
@ -697,7 +697,7 @@ async def test_auth_failure_logs_requester_ip_address(
|
|||
request_kwargs: dict[str, dict[str, str]],
|
||||
expected_ip: str,
|
||||
) -> None:
|
||||
"""401s and budget 429s are rejected before `add_litellm_data_to_request` stamps
|
||||
"""401s and budget 422s are rejected before `add_litellm_data_to_request` stamps
|
||||
the caller IP, so without this the failure logs (spend logs, prometheus client_ip)
|
||||
had no IP, and a 401 rarely carries a key or user identity either."""
|
||||
with (
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ async def test_over_first_window_raises():
|
|||
await _virtual_key_multi_budget_check(valid_token=token)
|
||||
|
||||
err = exc_info.value
|
||||
assert err.status_code == 429
|
||||
assert err.status_code == 422
|
||||
assert "24h" in str(err)
|
||||
assert "Key over" in str(err)
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ async def test_over_second_window_raises():
|
|||
await _virtual_key_multi_budget_check(valid_token=token)
|
||||
|
||||
err = exc_info.value
|
||||
assert err.status_code == 429
|
||||
assert err.status_code == 422
|
||||
assert "30d" in str(err)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8156,7 +8156,7 @@ async def test_reset_key_spend_resets_budget_windows(monkeypatch):
|
|||
counter without also advancing reset_at is not durable either: the very
|
||||
next request would re-sum the unchanged historical spend and put the
|
||||
counter right back above the window's max_budget, so
|
||||
_virtual_key_multi_budget_check kept raising BudgetExceededError (429) on
|
||||
_virtual_key_multi_budget_check kept raising BudgetExceededError (422) on
|
||||
every request even though the key's own reported spend read $0.
|
||||
"""
|
||||
mock_prisma_client = MagicMock()
|
||||
|
|
@ -16593,7 +16593,7 @@ async def test_info_key_fn_reads_the_configured_budget_model_key(monkeypatch):
|
|||
|
||||
It used to probe a second, provider-stripped key because the counter was
|
||||
written under the request model instead, which is what let a key report zero
|
||||
usage while being blocked at 429.
|
||||
usage while being blocked at 422.
|
||||
"""
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
|
|
|
|||
|
|
@ -495,7 +495,7 @@ class TestProxyBaseLLMRequestProcessing:
|
|||
)
|
||||
|
||||
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
|
||||
assert exc_info.value.code == "429"
|
||||
assert exc_info.value.code == "422"
|
||||
tag_budget_check.assert_awaited_once()
|
||||
_, call_kwargs = tag_budget_check.call_args
|
||||
assert call_kwargs["tags"] == ("guardrail-tag",)
|
||||
|
|
@ -702,7 +702,7 @@ class TestProxyBaseLLMRequestProcessing:
|
|||
)
|
||||
|
||||
assert exc_info.value.type == ProxyErrorTypes.budget_exceeded
|
||||
assert exc_info.value.code == "429"
|
||||
assert exc_info.value.code == "422"
|
||||
assert "guardrail-tag" in exc_info.value.message
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue