mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
* test(proxy): move auth, hooks, policy_engine and client tests into tests/unit/proxy Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): stub HIBP through respx by disabling the aiohttp transport Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): share the httpx transport fixture across proxy unit tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): restore proxy globals without a missing-value sentinel Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): package moved dirs and stub the login breach check at the HTTP boundary Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(proxy): isolate the mcp server manager per test Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: yuneng <yuneng@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
137 lines
4.3 KiB
Python
137 lines
4.3 KiB
Python
"""
|
|
Unit tests for multi-budget-window enforcement on API keys.
|
|
"""
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.proxy._types import UserAPIKeyAuth
|
|
from litellm.proxy.auth.auth_checks import _virtual_key_multi_budget_check
|
|
|
|
|
|
def _make_valid_token(**kwargs) -> UserAPIKeyAuth:
|
|
defaults = dict(
|
|
token="sk-test-token",
|
|
key_name="test",
|
|
spend=0.0,
|
|
max_budget=None,
|
|
budget_limits=[],
|
|
)
|
|
defaults.update(kwargs)
|
|
return UserAPIKeyAuth(**defaults)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_no_budget_limits_passes():
|
|
"""Keys with empty budget_limits should pass without raising."""
|
|
token = _make_valid_token(budget_limits=[])
|
|
# Should not raise
|
|
await _virtual_key_multi_budget_check(valid_token=token)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_under_budget_passes():
|
|
"""Key with spend under all windows should pass."""
|
|
token = _make_valid_token(
|
|
budget_limits=[
|
|
{"budget_duration": "24h", "max_budget": 10.0, "reset_at": None},
|
|
{"budget_duration": "30d", "max_budget": 100.0, "reset_at": None},
|
|
]
|
|
)
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_current_spend",
|
|
new_callable=AsyncMock,
|
|
return_value=1.0, # well under both windows
|
|
):
|
|
await _virtual_key_multi_budget_check(valid_token=token)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_over_first_window_raises():
|
|
"""Key exceeding the first (daily) window should raise BudgetExceededError."""
|
|
token = _make_valid_token(
|
|
budget_limits=[
|
|
{"budget_duration": "24h", "max_budget": 5.0, "reset_at": None},
|
|
{"budget_duration": "30d", "max_budget": 100.0, "reset_at": None},
|
|
]
|
|
)
|
|
|
|
spend_by_window = [6.0, 6.0] # over daily, under monthly
|
|
|
|
call_count = 0
|
|
|
|
async def fake_get_spend(counter_key, fallback_spend, max_budget=None, **kwargs):
|
|
nonlocal call_count
|
|
val = spend_by_window[call_count]
|
|
call_count += 1
|
|
return val
|
|
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_current_spend", side_effect=fake_get_spend
|
|
):
|
|
with pytest.raises(litellm.BudgetExceededError) as exc_info:
|
|
await _virtual_key_multi_budget_check(valid_token=token)
|
|
|
|
err = exc_info.value
|
|
assert err.status_code == 422
|
|
assert "24h" in str(err)
|
|
assert "Key over" in str(err)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_over_second_window_raises():
|
|
"""Key exceeding only the monthly window should raise BudgetExceededError referencing 30d."""
|
|
token = _make_valid_token(
|
|
budget_limits=[
|
|
{"budget_duration": "24h", "max_budget": 50.0, "reset_at": None},
|
|
{"budget_duration": "30d", "max_budget": 5.0, "reset_at": None},
|
|
]
|
|
)
|
|
|
|
spend_by_window = [1.0, 10.0] # under daily, over monthly
|
|
|
|
call_count = 0
|
|
|
|
async def fake_get_spend(counter_key, fallback_spend, max_budget=None, **kwargs):
|
|
nonlocal call_count
|
|
val = spend_by_window[call_count]
|
|
call_count += 1
|
|
return val
|
|
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_current_spend", side_effect=fake_get_spend
|
|
):
|
|
with pytest.raises(litellm.BudgetExceededError) as exc_info:
|
|
await _virtual_key_multi_budget_check(valid_token=token)
|
|
|
|
err = exc_info.value
|
|
assert err.status_code == 422
|
|
assert "30d" in str(err)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_budget_limit_entry_objects_coerced():
|
|
"""BudgetLimitEntry Pydantic objects (not dicts) must be handled without KeyError.
|
|
|
|
While budget_limits is normally serialized as List[dict], the auth check must
|
|
tolerate BudgetLimitEntry objects in case they arrive without prior serialization.
|
|
"""
|
|
from litellm.proxy._types import BudgetLimitEntry
|
|
|
|
token = _make_valid_token(budget_limits=[])
|
|
# Bypass Pydantic validation to simulate BudgetLimitEntry objects reaching the check
|
|
object.__setattr__(
|
|
token,
|
|
"budget_limits",
|
|
[BudgetLimitEntry(budget_duration="24h", max_budget=10.0)],
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.proxy_server.get_current_spend",
|
|
new_callable=AsyncMock,
|
|
return_value=1.0,
|
|
):
|
|
# Should not raise TypeError / KeyError — model_dump() coerces the object
|
|
await _virtual_key_multi_budget_check(valid_token=token)
|