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:
yassin 2026-09-20 06:00:06 +00:00
parent 58065d46fd
commit f9244749e0
8 changed files with 17 additions and 17 deletions

View file

@ -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

View file

@ -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

View file

@ -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"] == ""

View file

@ -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

View file

@ -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 (

View file

@ -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)

View file

@ -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

View file

@ -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