mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-27 01:22:18 +00:00
test: expect 422 for budget refusals in unification, e2e and integration suites
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
92d3a1d87d
commit
b7e11546b5
8 changed files with 27 additions and 27 deletions
|
|
@ -95,7 +95,7 @@ class UnauthorizedError(BaseModel):
|
|||
class RateLimitedError(BaseModel):
|
||||
kind: Literal["rate_limited"] = "rate_limited"
|
||||
retry_after_seconds: int | None = None
|
||||
# litellm overloads 429 for budget_exceeded too, so keep the body to tell them apart.
|
||||
# keep the body so callers can tell limiter kinds apart.
|
||||
body: str = ""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -96,8 +96,8 @@ def _spend_until_budget_blocks(client: ManagementClient, key: str) -> None:
|
|||
for _ in range(40):
|
||||
outcome = client.chat_status(key, SPEND_MODEL, f"spend {unique_marker()}")
|
||||
if _is_budget_block(outcome):
|
||||
assert outcome.status_code == 429, (
|
||||
f"budget refusal must be 429, got {outcome.status_code}: {outcome.body[:200]}"
|
||||
assert outcome.status_code == 422, (
|
||||
f"budget refusal must be 422, got {outcome.status_code}: {outcome.body[:200]}"
|
||||
)
|
||||
return
|
||||
assert outcome.ok, f"paid call failed before the budget tripped ({outcome.status_code}): {outcome.body[:300]}"
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ def _assert_budget_blocks(client: BudgetClient, key: str, *, user: str = "") ->
|
|||
pytest.fail("budget never enforced within the call budget")
|
||||
|
||||
|
||||
def _assert_blocked_429(client: BudgetClient, key: str) -> StreamingResponse:
|
||||
def _assert_blocked_422(client: BudgetClient, key: str) -> StreamingResponse:
|
||||
blocked = _assert_budget_blocks(client, key)
|
||||
assert blocked.status_code == 429, (
|
||||
f"budget refusal must be 429, got {blocked.status_code}: {blocked.body[:200]}"
|
||||
assert blocked.status_code == 422, (
|
||||
f"budget refusal must be 422, got {blocked.status_code}: {blocked.body[:200]}"
|
||||
)
|
||||
return blocked
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ class TestBudgetBlocksPerLevel:
|
|||
key = client.generate_key(max_budget=TINY_CAP)
|
||||
resources.defer(lambda: client.delete_key(key))
|
||||
|
||||
_assert_blocked_429(client, key)
|
||||
_assert_blocked_422(client, key)
|
||||
|
||||
@pytest.mark.covers("quota_management.budget.team.blocks_over_limit")
|
||||
def test_team_budget_blocks_every_team_key(self, client: BudgetClient, resources: ResourceManager) -> None:
|
||||
|
|
@ -71,10 +71,10 @@ class TestBudgetBlocksPerLevel:
|
|||
sibling_key = client.generate_key(team_id=team_id)
|
||||
resources.defer(lambda: client.delete_key(sibling_key))
|
||||
|
||||
_assert_blocked_429(client, spender_key)
|
||||
_assert_blocked_422(client, spender_key)
|
||||
sibling = _chat(client, sibling_key)
|
||||
assert is_budget_block(sibling) and sibling.status_code == 429, (
|
||||
f"a sibling key on the capped team must get the same 429 budget_exceeded, "
|
||||
assert is_budget_block(sibling) and sibling.status_code == 422, (
|
||||
f"a sibling key on the capped team must get the same 422 budget_exceeded, "
|
||||
f"got {sibling.status_code}: {sibling.body[:200]}"
|
||||
)
|
||||
|
||||
|
|
@ -99,10 +99,10 @@ class TestBudgetBlocksPerLevel:
|
|||
team_key = client.generate_key(team_id=team_id, user_id=user_id)
|
||||
resources.defer(lambda: client.delete_key(team_key))
|
||||
|
||||
_assert_blocked_429(client, first_key)
|
||||
_assert_blocked_422(client, first_key)
|
||||
second = _chat(client, second_key)
|
||||
assert is_budget_block(second) and second.status_code == 429, (
|
||||
f"the second personal key of a user over budget must get the same 429 budget_exceeded, "
|
||||
assert is_budget_block(second) and second.status_code == 422, (
|
||||
f"the second personal key of a user over budget must get the same 422 budget_exceeded, "
|
||||
f"got {second.status_code}: {second.body[:200]}"
|
||||
)
|
||||
team_result = _chat(client, team_key)
|
||||
|
|
@ -133,7 +133,7 @@ class TestBudgetBlocksPerLevel:
|
|||
key = client.generate_key(team_id=team_id)
|
||||
resources.defer(lambda: client.delete_key(key))
|
||||
|
||||
blocked = _assert_blocked_429(client, key)
|
||||
blocked = _assert_blocked_422(client, key)
|
||||
assert f"Organization={org_id}" in blocked.body, (
|
||||
f"refusal must name the org as the blocker, got: {blocked.body[:200]}"
|
||||
)
|
||||
|
|
@ -155,7 +155,7 @@ class TestBudgetBlocksPerLevel:
|
|||
teammate_key = client.generate_key(team_id=team_id, user_id=teammate_id)
|
||||
resources.defer(lambda: client.delete_key(teammate_key))
|
||||
|
||||
_assert_blocked_429(client, member_key)
|
||||
_assert_blocked_422(client, member_key)
|
||||
require_successful_call(_chat(client, teammate_key))
|
||||
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ class TestKeyBudgetBlocksAcrossKeyKinds:
|
|||
control_key = client.generate_key(user_id=user_id)
|
||||
resources.defer(lambda: client.delete_key(control_key))
|
||||
|
||||
_assert_blocked_429(client, capped_key)
|
||||
_assert_blocked_422(client, capped_key)
|
||||
require_successful_call(_chat(client, control_key))
|
||||
|
||||
@pytest.mark.covers("quota_management.budget.key.blocks_over_limit")
|
||||
|
|
@ -188,7 +188,7 @@ class TestKeyBudgetBlocksAcrossKeyKinds:
|
|||
control_key = client.generate_key(team_id=team_id)
|
||||
resources.defer(lambda: client.delete_key(control_key))
|
||||
|
||||
_assert_blocked_429(client, capped_key)
|
||||
_assert_blocked_422(client, capped_key)
|
||||
require_successful_call(_chat(client, control_key))
|
||||
|
||||
@pytest.mark.covers("quota_management.budget.key.blocks_over_limit")
|
||||
|
|
@ -205,5 +205,5 @@ class TestKeyBudgetBlocksAcrossKeyKinds:
|
|||
control_key = client.generate_key(team_id=team_id, user_id=member_id)
|
||||
resources.defer(lambda: client.delete_key(control_key))
|
||||
|
||||
_assert_blocked_429(client, capped_key)
|
||||
_assert_blocked_422(client, capped_key)
|
||||
require_successful_call(_chat(client, control_key))
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ def test_long_window_blocks_after_short_window_resets(client: BudgetClient, reso
|
|||
|
||||
# 1. drive the key to get blocked by SHORT_WINDOW, assert it's budget error
|
||||
blocked = _drive_to_block(client, key)
|
||||
assert blocked.status_code == 429, f"budget block was not a 429: {blocked.status_code} {blocked.body[:200]}"
|
||||
assert blocked.status_code == 422, f"budget block was not a 422: {blocked.status_code} {blocked.body[:200]}"
|
||||
|
||||
# 2. check the reset times of both budget windows after we drove to being blocked
|
||||
blocked_reset_at = window_reset_at(client.key_budget_windows(key), SHORT_WINDOW)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ def test_team_long_window_blocks_after_short_window_resets(client: BudgetClient,
|
|||
|
||||
# 1. drive the key to being blocked, assert its blocked by budget budget_exceeded
|
||||
blocked = _drive_to_block(client, key)
|
||||
assert blocked.status_code == 429, f"budget block was not a 429: {blocked.status_code} {blocked.body[:200]}"
|
||||
assert blocked.status_code == 422, f"budget block was not a 422: {blocked.status_code} {blocked.body[:200]}"
|
||||
|
||||
# 2. check the the teams budget windows
|
||||
blocked_reset_at = window_reset_at(client.team_budget_windows(team_id), SHORT_WINDOW)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ def test_zero_false_and_empty_values_are_not_treated_as_omission(gateway: Gatewa
|
|||
"POST", "/v1/chat/completions",
|
||||
{"model": models[0], "messages": [{"role": "user", "content": "zero budget"}]}, key=key,
|
||||
)
|
||||
assert denied.status_code == 429, denied.text
|
||||
assert denied.status_code == 422, denied.text
|
||||
assert denied.json()["error"]["type"] == "budget_exceeded"
|
||||
gateway.post("/key/update", {"key": key, "max_budget": 1, "models": [], "metadata": {}})
|
||||
info: Final = object_value(gateway.get("/key/info", {"key": key})["info"])
|
||||
|
|
@ -127,7 +127,7 @@ def test_zero_false_and_empty_values_are_not_treated_as_omission(gateway: Gatewa
|
|||
"POST", "/v1/chat/completions",
|
||||
{"model": models[0], "messages": [{"role": "user", "content": "updated zero budget"}]}, key=key,
|
||||
)
|
||||
assert zero_after_update.status_code == 429, zero_after_update.text
|
||||
assert zero_after_update.status_code == 422, zero_after_update.text
|
||||
assert zero_after_update.json()["error"]["type"] == "budget_exceeded"
|
||||
gateway.post("/key/update", {"key": key, "max_budget": None})
|
||||
assert read_rows(
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ def test_key_budget_at_boundary_blocks_provider_then_explicit_reset_restores(gat
|
|||
{"model": model, "messages": [{"role": "user", "content": f"over budget {uuid.uuid4().hex}"}]},
|
||||
key=key,
|
||||
)
|
||||
assert denied.status_code == 429 and denied.json()["error"]["type"] == "budget_exceeded", denied.text
|
||||
assert denied.status_code == 422 and denied.json()["error"]["type"] == "budget_exceeded", denied.text
|
||||
assert upstream.get("/__observations").json()["requests"] == []
|
||||
assert gateway.chat(model, key=control, text=f"control {uuid.uuid4().hex}")["usage"]["total_tokens"] == 40
|
||||
gateway.post("/key/update", {"key": key, "spend": 0})
|
||||
|
|
@ -205,7 +205,7 @@ def test_key_budget_at_boundary_blocks_provider_then_explicit_reset_restores(gat
|
|||
{"model": model, "messages": [{"role": "user", "content": f"boundary again {uuid.uuid4().hex}"}]},
|
||||
key=key,
|
||||
)
|
||||
assert denied_again.status_code == 429 and denied_again.json()["error"]["type"] == "budget_exceeded", (
|
||||
assert denied_again.status_code == 422 and denied_again.json()["error"]["type"] == "budget_exceeded", (
|
||||
denied_again.text
|
||||
)
|
||||
assert upstream.get("/__observations").json()["requests"] == []
|
||||
|
|
|
|||
|
|
@ -1397,10 +1397,10 @@ class TestBudgetExceededErrorSurfacesUnifiedFields:
|
|||
assert e.llm_provider == "anthropic"
|
||||
|
||||
def test_should_keep_existing_status_code_and_message(self):
|
||||
# Backward-compat guard: existing callers depend on `status_code=429`
|
||||
# Backward-compat guard: existing callers depend on `status_code=422`
|
||||
# and the canonical message format.
|
||||
e = litellm.BudgetExceededError(current_cost=0.000109, max_budget=0.0001)
|
||||
assert e.status_code == 429
|
||||
assert e.status_code == 422
|
||||
assert "Current cost: 0.000109" in e.message
|
||||
assert "Max budget: 0.0001" in e.message
|
||||
|
||||
|
|
@ -1424,7 +1424,7 @@ class TestBudgetExceededErrorSurfacesUnifiedFields:
|
|||
info = StandardLoggingPayloadSetup.get_error_information(e)
|
||||
assert info["error_rate_limit_category"] == "litellm_rate_limit"
|
||||
assert info["error_rate_limit_type"] == "budget"
|
||||
assert info["error_code"] == "429"
|
||||
assert info["error_code"] == "422"
|
||||
assert info["error_class"] == "BudgetExceededError"
|
||||
|
||||
def test_should_propagate_llm_provider_to_standard_logging_payload(self):
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue