mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
1495 lines
46 KiB
Python
1495 lines
46 KiB
Python
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
import litellm
|
|
from litellm.caching.dual_cache import DualCache
|
|
from litellm.proxy._types import (
|
|
LiteLLM_BudgetTable,
|
|
LiteLLM_EndUserTable,
|
|
LiteLLM_OrganizationTable,
|
|
LiteLLM_TagTable,
|
|
LiteLLM_TeamMembership,
|
|
LiteLLM_TeamTable,
|
|
LiteLLM_UserTable,
|
|
UserAPIKeyAuth,
|
|
)
|
|
from litellm.proxy.spend_tracking.budget_reservation import (
|
|
estimate_request_max_cost,
|
|
get_budget_window_start,
|
|
invalidate_budget_reservation_counters,
|
|
release_budget_reservation,
|
|
reserve_budget_for_request,
|
|
)
|
|
from litellm.proxy.utils import ProxyLogging
|
|
|
|
|
|
@pytest.fixture()
|
|
def spend_counter_state():
|
|
import litellm.proxy.proxy_server as ps
|
|
|
|
original_counter_cache = ps.spend_counter_cache
|
|
original_key_cache = ps.user_api_key_cache
|
|
original_prisma_client = ps.prisma_client
|
|
|
|
counter_cache = DualCache()
|
|
key_cache = DualCache()
|
|
ps.spend_counter_cache = counter_cache
|
|
ps.user_api_key_cache = key_cache
|
|
ps.prisma_client = None
|
|
|
|
try:
|
|
yield counter_cache, key_cache
|
|
finally:
|
|
ps.spend_counter_cache = original_counter_cache
|
|
ps.user_api_key_cache = original_key_cache
|
|
ps.prisma_client = original_prisma_client
|
|
|
|
|
|
def _request_body() -> dict:
|
|
return {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [{"role": "user", "content": "hello"}],
|
|
"max_tokens": 10,
|
|
}
|
|
|
|
|
|
def test_should_not_serialize_budget_reservation_on_user_api_key_auth():
|
|
auth = UserAPIKeyAuth(
|
|
token="key-budget-runtime-state",
|
|
budget_reservation={
|
|
"reserved_cost": 0.5,
|
|
"entries": [{"counter_key": "spend:key:key-budget-runtime-state"}],
|
|
},
|
|
)
|
|
|
|
assert "budget_reservation" not in auth.model_dump()
|
|
assert "budget_reservation" not in auth.model_dump(exclude_none=True)
|
|
assert "budget_reservation" not in auth.model_dump_json()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_shrink_second_key_reservation_to_remaining_budget(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-race",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.6,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
assert reservation is not None
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(key="spend:key:key-budget-race")
|
|
== 0.6
|
|
)
|
|
|
|
second_reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
assert second_reservation is not None
|
|
assert second_reservation["reserved_cost"] == pytest.approx(0.4)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
with pytest.raises(litellm.BudgetExceededError):
|
|
await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(second_reservation)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-race"
|
|
) == pytest.approx(0.6)
|
|
await release_budget_reservation(reservation)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_shrink_second_end_user_reservation_to_remaining_budget(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-end-user",
|
|
end_user_id="end-user-budget-race",
|
|
)
|
|
end_user_object = LiteLLM_EndUserTable(
|
|
user_id="end-user-budget-race",
|
|
blocked=False,
|
|
spend=0.0,
|
|
litellm_budget_table=LiteLLM_BudgetTable(max_budget=1.0),
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.6,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
end_user_object=end_user_object,
|
|
)
|
|
assert reservation is not None
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:end_user:end-user-budget-race"
|
|
) == pytest.approx(0.6)
|
|
|
|
second_reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
end_user_object=end_user_object,
|
|
)
|
|
assert second_reservation is not None
|
|
assert second_reservation["reserved_cost"] == pytest.approx(0.4)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:end_user:end-user-budget-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
with pytest.raises(litellm.BudgetExceededError):
|
|
await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
end_user_object=end_user_object,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:end_user:end-user-budget-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(second_reservation)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:end_user:end-user-budget-race"
|
|
) == pytest.approx(0.6)
|
|
|
|
from litellm.proxy.proxy_server import increment_spend_counters
|
|
|
|
await increment_spend_counters(
|
|
token=None,
|
|
team_id=None,
|
|
user_id=None,
|
|
response_cost=0.2,
|
|
budget_reservation=reservation,
|
|
end_user_id="end-user-budget-race",
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:end_user:end-user-budget-race"
|
|
) == pytest.approx(0.2)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_shrink_second_tag_reservation_to_remaining_budget(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(token="key-budget-tag")
|
|
request_body = _request_body()
|
|
request_body["metadata"] = {
|
|
"tags": ["tag-budget-race", "tag-without-budget", "tag-budget-race"]
|
|
}
|
|
await key_cache.async_set_cache(
|
|
key="tag:tag-budget-race",
|
|
value=LiteLLM_TagTable(
|
|
tag_name="tag-budget-race",
|
|
spend=0.0,
|
|
budget_id="tag-budget-id",
|
|
litellm_budget_table=LiteLLM_BudgetTable(max_budget=1.0),
|
|
).model_dump(),
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="tag:tag-without-budget",
|
|
value=LiteLLM_TagTable(
|
|
tag_name="tag-without-budget",
|
|
spend=0.0,
|
|
).model_dump(),
|
|
)
|
|
prisma_client = MagicMock()
|
|
prisma_client.db.litellm_tagtable.find_many = AsyncMock(return_value=[])
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.6,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=prisma_client,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
assert reservation is not None
|
|
assert reservation["entries"] == [
|
|
{
|
|
"counter_key": "spend:tag:tag-budget-race",
|
|
"entity_type": "Tag",
|
|
"entity_id": "tag-budget-race",
|
|
"reserved_cost": 0.6,
|
|
"applied_adjustment": 0.0,
|
|
}
|
|
]
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:tag-budget-race"
|
|
) == pytest.approx(0.6)
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(key="spend:tag:tag-without-budget")
|
|
is None
|
|
)
|
|
|
|
second_reservation = await reserve_budget_for_request(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=prisma_client,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
assert second_reservation is not None
|
|
assert second_reservation["reserved_cost"] == pytest.approx(0.4)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:tag-budget-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
with pytest.raises(litellm.BudgetExceededError):
|
|
await reserve_budget_for_request(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=prisma_client,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:tag-budget-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(second_reservation)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:tag-budget-race"
|
|
) == pytest.approx(0.6)
|
|
|
|
from litellm.proxy.proxy_server import increment_spend_counters
|
|
|
|
await increment_spend_counters(
|
|
token=None,
|
|
team_id=None,
|
|
user_id=None,
|
|
response_cost=0.2,
|
|
budget_reservation=reservation,
|
|
tags=["tag-budget-race"],
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:tag-budget-race"
|
|
) == pytest.approx(0.2)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_seed_and_update_end_user_and_tag_counters_without_reservation(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
await key_cache.async_set_cache(
|
|
key="end_user_id:customer-1",
|
|
value=LiteLLM_EndUserTable(
|
|
user_id="customer-1",
|
|
blocked=False,
|
|
spend=4.0,
|
|
litellm_budget_table=LiteLLM_BudgetTable(max_budget=10.0),
|
|
).model_dump(),
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="tag:paid-tag",
|
|
value=LiteLLM_TagTable(
|
|
tag_name="paid-tag",
|
|
spend=7.0,
|
|
).model_dump(),
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="tag:other-tag",
|
|
value=LiteLLM_TagTable(
|
|
tag_name="other-tag",
|
|
spend=2.0,
|
|
).model_dump(),
|
|
)
|
|
|
|
from litellm.proxy.proxy_server import increment_spend_counters
|
|
|
|
await increment_spend_counters(
|
|
token=None,
|
|
team_id=None,
|
|
user_id=None,
|
|
response_cost=0.50,
|
|
end_user_id="customer-1",
|
|
tags=["paid-tag", "paid-tag", "other-tag", ""],
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:end_user:customer-1"
|
|
) == pytest.approx(4.50)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:paid-tag"
|
|
) == pytest.approx(7.50)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:tag:other-tag"
|
|
) == pytest.approx(2.50)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_reserve_team_member_and_org_budget_counters(spend_counter_state):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-shared",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
user_id="user-budget-shared",
|
|
team_id="team-budget-shared",
|
|
org_id="org-budget-shared",
|
|
)
|
|
team_object = LiteLLM_TeamTable(
|
|
team_id="team-budget-shared",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
user_object = LiteLLM_UserTable(
|
|
user_id="user-budget-shared",
|
|
spend=0.0,
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="team_membership:user-budget-shared:team-budget-shared",
|
|
value=LiteLLM_TeamMembership(
|
|
user_id="user-budget-shared",
|
|
team_id="team-budget-shared",
|
|
spend=0.1,
|
|
litellm_budget_table=LiteLLM_BudgetTable(max_budget=1.0),
|
|
).model_dump(),
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="org_id:org-budget-shared:with_budget",
|
|
value=LiteLLM_OrganizationTable(
|
|
organization_id="org-budget-shared",
|
|
organization_alias="shared-org",
|
|
budget_id="org-budget-id",
|
|
spend=0.1,
|
|
models=[],
|
|
created_by="test",
|
|
updated_by="test",
|
|
litellm_budget_table=LiteLLM_BudgetTable(max_budget=1.0),
|
|
).model_dump(),
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.3,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=team_object,
|
|
user_object=user_object,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team_member:user-budget-shared:team-budget-shared"
|
|
) == pytest.approx(0.4)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:org:org-budget-shared"
|
|
) == pytest.approx(0.4)
|
|
|
|
await release_budget_reservation(reservation)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_seed_org_counter_from_with_budget_cache(spend_counter_state):
|
|
counter_cache, key_cache = spend_counter_state
|
|
await key_cache.async_set_cache(
|
|
key="org_id:org-counter-with-budget:with_budget",
|
|
value=LiteLLM_OrganizationTable(
|
|
organization_id="org-counter-with-budget",
|
|
organization_alias="shared-org",
|
|
budget_id="org-budget-id",
|
|
spend=2.0,
|
|
models=[],
|
|
created_by="test",
|
|
updated_by="test",
|
|
litellm_budget_table=LiteLLM_BudgetTable(max_budget=10.0),
|
|
).model_dump(),
|
|
)
|
|
|
|
from litellm.proxy.proxy_server import increment_spend_counters
|
|
|
|
await increment_spend_counters(
|
|
token=None,
|
|
team_id=None,
|
|
user_id=None,
|
|
org_id="org-counter-with-budget",
|
|
response_cost=0.25,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:org:org-counter-with-budget"
|
|
) == pytest.approx(2.25)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_seed_org_counter_from_plain_org_cache(spend_counter_state):
|
|
counter_cache, key_cache = spend_counter_state
|
|
await key_cache.async_set_cache(
|
|
key="org_id:org-counter-plain",
|
|
value=LiteLLM_OrganizationTable(
|
|
organization_id="org-counter-plain",
|
|
organization_alias="shared-org",
|
|
budget_id="org-budget-id",
|
|
spend=2.0,
|
|
models=[],
|
|
created_by="test",
|
|
updated_by="test",
|
|
).model_dump(),
|
|
)
|
|
|
|
from litellm.proxy.proxy_server import increment_spend_counters
|
|
|
|
await increment_spend_counters(
|
|
token=None,
|
|
team_id=None,
|
|
user_id=None,
|
|
org_id="org-counter-plain",
|
|
response_cost=0.25,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:org:org-counter-plain"
|
|
) == pytest.approx(2.25)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_cap_known_estimate_to_remaining_budget(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-known-estimate-cap",
|
|
spend=0.9,
|
|
max_budget=1.0,
|
|
)
|
|
counter_cache.in_memory_cache.set_cache(
|
|
key="spend:key:key-budget-known-estimate-cap",
|
|
value=0.9,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.6,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is not None
|
|
assert reservation["reserved_cost"] == pytest.approx(0.1)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-known-estimate-cap"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(reservation)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-known-estimate-cap"
|
|
) == pytest.approx(0.9)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_reserve_remaining_budget_when_output_cap_missing(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-uncapped",
|
|
spend=0.2,
|
|
max_budget=1.0,
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="key-budget-uncapped",
|
|
value=valid_token,
|
|
)
|
|
request_body = _request_body()
|
|
request_body.pop("max_tokens")
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation._get_model_cost_info",
|
|
return_value={
|
|
"input_cost_per_token": 0.0,
|
|
"output_cost_per_token": 100.0,
|
|
"max_output_tokens": 200000,
|
|
},
|
|
):
|
|
assert (
|
|
estimate_request_max_cost(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
)
|
|
is None
|
|
)
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is not None
|
|
assert reservation["reserved_cost"] == pytest.approx(0.8)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-uncapped"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(reservation)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_shrink_uncapped_reservation_when_counter_advances(
|
|
spend_counter_state,
|
|
monkeypatch,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-uncapped-race",
|
|
spend=0.2,
|
|
max_budget=1.0,
|
|
)
|
|
request_body = _request_body()
|
|
request_body.pop("max_tokens")
|
|
|
|
from litellm.proxy.spend_tracking import budget_reservation
|
|
|
|
async def stale_counter_read(counter):
|
|
await counter_cache.async_increment_cache(
|
|
key=counter.counter_key,
|
|
value=0.3,
|
|
)
|
|
return 0.2
|
|
|
|
monkeypatch.setattr(
|
|
budget_reservation,
|
|
"_get_current_counter_value",
|
|
stale_counter_read,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=None,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is not None
|
|
assert reservation["reserved_cost"] == pytest.approx(0.7)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-uncapped-race"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-uncapped-race"
|
|
) == pytest.approx(0.3)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_shrink_uncapped_reservation_multiple_times(
|
|
spend_counter_state,
|
|
monkeypatch,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-double-resize",
|
|
spend=0.2,
|
|
max_budget=1.0,
|
|
team_id="team-budget-double-resize",
|
|
)
|
|
team_object = LiteLLM_TeamTable(
|
|
team_id="team-budget-double-resize",
|
|
spend=0.2,
|
|
max_budget=1.0,
|
|
)
|
|
request_body = _request_body()
|
|
request_body.pop("max_tokens")
|
|
|
|
from litellm.proxy.spend_tracking import budget_reservation
|
|
|
|
stale_spend_by_counter_key = {
|
|
"spend:key:key-budget-double-resize": 0.3,
|
|
"spend:team:team-budget-double-resize": 0.4,
|
|
}
|
|
|
|
async def stale_counter_read(counter):
|
|
await counter_cache.async_increment_cache(
|
|
key=counter.counter_key,
|
|
value=stale_spend_by_counter_key[counter.counter_key],
|
|
)
|
|
return 0.2
|
|
|
|
monkeypatch.setattr(
|
|
budget_reservation,
|
|
"_get_current_counter_value",
|
|
stale_counter_read,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=None,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=request_body,
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=team_object,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is not None
|
|
assert reservation["reserved_cost"] == pytest.approx(0.6)
|
|
assert [entry["reserved_cost"] for entry in reservation["entries"]] == [
|
|
pytest.approx(0.6),
|
|
pytest.approx(0.6),
|
|
]
|
|
assert [entry["applied_adjustment"] for entry in reservation["entries"]] == [
|
|
pytest.approx(0.0),
|
|
pytest.approx(0.0),
|
|
]
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-double-resize"
|
|
) == pytest.approx(0.9)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team:team-budget-double-resize"
|
|
) == pytest.approx(1.0)
|
|
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-double-resize"
|
|
) == pytest.approx(0.3)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team:team-budget-double-resize"
|
|
) == pytest.approx(0.4)
|
|
|
|
|
|
def test_should_start_window_without_reset_at_at_duration_boundary():
|
|
before = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
|
|
window_start = get_budget_window_start({"budget_duration": "1h"})
|
|
|
|
after = datetime.now(timezone.utc) - timedelta(hours=1)
|
|
assert window_start is not None
|
|
assert before <= window_start <= after
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_skip_budget_window_with_unparseable_duration(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-malformed-window",
|
|
spend=0.9,
|
|
max_budget=10.0,
|
|
budget_limits=[
|
|
{
|
|
"budget_duration": "not-a-duration",
|
|
"max_budget": 1.0,
|
|
}
|
|
],
|
|
)
|
|
counter_cache.in_memory_cache.set_cache(
|
|
key="spend:key:key-budget-malformed-window",
|
|
value=0.9,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.2,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is not None
|
|
assert [entry["counter_key"] for entry in reservation["entries"]] == [
|
|
"spend:key:key-budget-malformed-window"
|
|
]
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-malformed-window"
|
|
) == pytest.approx(1.1)
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-malformed-window:window:not-a-duration"
|
|
)
|
|
is None
|
|
)
|
|
|
|
await release_budget_reservation(reservation)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-malformed-window"
|
|
) == pytest.approx(0.9)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_skip_window_reservation_when_db_baseline_unavailable(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-window-db-unavailable",
|
|
budget_limits=[
|
|
{
|
|
"budget_duration": "1h",
|
|
"max_budget": 1.0,
|
|
}
|
|
],
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.5,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is None
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-window-db-unavailable:window:1h"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_skip_reservation_when_counter_increment_fails(
|
|
spend_counter_state,
|
|
monkeypatch,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-reserve-unavailable",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
async def fail_increment_cache(*args, **kwargs):
|
|
raise RuntimeError("counter unavailable")
|
|
|
|
monkeypatch.setattr(counter_cache, "async_increment_cache", fail_increment_cache)
|
|
|
|
with (
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.5,
|
|
),
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.verbose_proxy_logger.warning"
|
|
) as mock_warning,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is None
|
|
assert mock_warning.call_count >= 1
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-reserve-unavailable"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_skip_reservation_when_counter_initialization_fails(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-reserve-init-unavailable",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.5,
|
|
),
|
|
patch(
|
|
"litellm.proxy.proxy_server._ensure_spend_counter_initialized",
|
|
side_effect=RuntimeError("redis unavailable"),
|
|
),
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.verbose_proxy_logger.warning"
|
|
) as mock_warning,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is None
|
|
assert mock_warning.call_count >= 1
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-reserve-init-unavailable"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_release_tracked_entry_when_reservation_fails_after_increment(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-reserve-after-increment-failure",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
import litellm.proxy.proxy_server as ps
|
|
|
|
original_increment_counter = ps._increment_spend_counter_cache
|
|
first_increment = True
|
|
|
|
async def fail_after_increment(counter_key: str, increment: float):
|
|
nonlocal first_increment
|
|
if first_increment:
|
|
first_increment = False
|
|
await counter_cache.async_increment_cache(key=counter_key, value=increment)
|
|
raise RuntimeError("lost increment response")
|
|
return await original_increment_counter(
|
|
counter_key=counter_key,
|
|
increment=increment,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.5,
|
|
),
|
|
patch(
|
|
"litellm.proxy.proxy_server._increment_spend_counter_cache",
|
|
side_effect=fail_after_increment,
|
|
),
|
|
patch(
|
|
"litellm.proxy.proxy_server._invalidate_spend_counter",
|
|
side_effect=RuntimeError("invalidate unavailable"),
|
|
),
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is None
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-reserve-after-increment-failure"
|
|
) == pytest.approx(0.0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_not_re_read_uncapped_budget_after_reservation_fallback(
|
|
spend_counter_state,
|
|
monkeypatch,
|
|
):
|
|
_, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-uncapped-read-once",
|
|
spend=0.2,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
from litellm.proxy.spend_tracking import budget_reservation
|
|
|
|
current_counter_reads = []
|
|
|
|
async def mock_get_current_counter_value(counter):
|
|
current_counter_reads.append(counter.counter_key)
|
|
return counter.fallback_spend
|
|
|
|
async def mock_reserve_counter(counter, reservation_cost):
|
|
return None
|
|
|
|
monkeypatch.setattr(
|
|
budget_reservation,
|
|
"_get_current_counter_value",
|
|
mock_get_current_counter_value,
|
|
)
|
|
monkeypatch.setattr(
|
|
budget_reservation,
|
|
"_reserve_counter",
|
|
mock_reserve_counter,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=None,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert reservation is not None
|
|
assert reservation["reserved_cost"] == pytest.approx(0.8)
|
|
assert current_counter_reads == ["spend:key:key-budget-uncapped-read-once"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_reconcile_reserved_counter_to_actual_spend(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-reconcile",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.6,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
from litellm.proxy.proxy_server import increment_spend_counters
|
|
|
|
await increment_spend_counters(
|
|
token="key-budget-reconcile",
|
|
team_id="team-without-budget",
|
|
user_id=None,
|
|
response_cost=0.2,
|
|
budget_reservation=reservation,
|
|
)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-reconcile"
|
|
) == pytest.approx(0.2)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team:team-without-budget"
|
|
) == pytest.approx(0.2)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_release_reservation_on_failure(spend_counter_state):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-release",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.4,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=None,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
await release_budget_reservation(reservation)
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-release"
|
|
) == pytest.approx(0.0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_retry_partial_release_without_double_decrement(
|
|
spend_counter_state,
|
|
monkeypatch,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-partial-release",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
team_id="team-budget-partial-release",
|
|
)
|
|
team_object = LiteLLM_TeamTable(
|
|
team_id="team-budget-partial-release",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.4,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=team_object,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
original_increment_cache = counter_cache.async_increment_cache
|
|
fail_next_team_release = True
|
|
|
|
async def flaky_increment_cache(key, value, *args, **kwargs):
|
|
nonlocal fail_next_team_release
|
|
if (
|
|
key == "spend:team:team-budget-partial-release"
|
|
and value < 0
|
|
and fail_next_team_release
|
|
):
|
|
fail_next_team_release = False
|
|
raise RuntimeError("simulated counter failure")
|
|
return await original_increment_cache(key=key, value=value, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(counter_cache, "async_increment_cache", flaky_increment_cache)
|
|
|
|
with pytest.raises(RuntimeError):
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-partial-release"
|
|
) == pytest.approx(0.0)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team:team-budget-partial-release"
|
|
) == pytest.approx(0.4)
|
|
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-partial-release"
|
|
) == pytest.approx(0.0)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team:team-budget-partial-release"
|
|
) == pytest.approx(0.0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_preserve_budget_error_and_continue_partial_cleanup(
|
|
spend_counter_state,
|
|
monkeypatch,
|
|
):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-cleanup-failure",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
team_id="team-budget-cleanup-failure",
|
|
)
|
|
team_object = LiteLLM_TeamTable(
|
|
team_id="team-budget-cleanup-failure",
|
|
spend=0.3,
|
|
max_budget=0.3,
|
|
)
|
|
await key_cache.async_set_cache(
|
|
key="team_id:team-budget-cleanup-failure",
|
|
value=team_object,
|
|
)
|
|
|
|
original_increment_cache = counter_cache.async_increment_cache
|
|
fail_key_cleanup = True
|
|
|
|
async def flaky_increment_cache(key, value, *args, **kwargs):
|
|
nonlocal fail_key_cleanup
|
|
if key == "spend:key:key-budget-cleanup-failure" and value < 0:
|
|
if fail_key_cleanup:
|
|
fail_key_cleanup = False
|
|
raise RuntimeError("simulated cleanup failure")
|
|
return await original_increment_cache(key=key, value=value, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(counter_cache, "async_increment_cache", flaky_increment_cache)
|
|
|
|
with (
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.4,
|
|
),
|
|
patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.verbose_proxy_logger.exception"
|
|
) as mock_log_exception,
|
|
):
|
|
with pytest.raises(litellm.BudgetExceededError):
|
|
await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=team_object,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-cleanup-failure"
|
|
)
|
|
is None
|
|
)
|
|
assert counter_cache.in_memory_cache.get_cache(
|
|
key="spend:team:team-budget-cleanup-failure"
|
|
) == pytest.approx(0.3)
|
|
mock_log_exception.assert_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_not_create_negative_counter_when_release_counter_is_missing(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, _ = spend_counter_state
|
|
reservation = {
|
|
"reserved_cost": 0.4,
|
|
"entries": [
|
|
{
|
|
"counter_key": "spend:key:key-budget-missing-release",
|
|
"reserved_cost": 0.4,
|
|
"applied_adjustment": 0.0,
|
|
}
|
|
],
|
|
"finalized": False,
|
|
}
|
|
|
|
with pytest.raises(RuntimeError, match="missing counter"):
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-missing-release"
|
|
)
|
|
is None
|
|
)
|
|
assert reservation["finalized"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_invalidate_counter_when_release_would_underflow(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, _ = spend_counter_state
|
|
await counter_cache.async_increment_cache(
|
|
key="spend:key:key-budget-underflow-release",
|
|
value=0.1,
|
|
)
|
|
reservation = {
|
|
"reserved_cost": 0.4,
|
|
"entries": [
|
|
{
|
|
"counter_key": "spend:key:key-budget-underflow-release",
|
|
"reserved_cost": 0.4,
|
|
"applied_adjustment": 0.0,
|
|
}
|
|
],
|
|
"finalized": False,
|
|
}
|
|
|
|
with pytest.raises(RuntimeError, match="negative"):
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-underflow-release"
|
|
)
|
|
is None
|
|
)
|
|
assert reservation["finalized"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_invalidate_non_numeric_counter_during_release(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, _ = spend_counter_state
|
|
counter_cache.in_memory_cache.set_cache(
|
|
key="spend:key:key-budget-nonnumeric-release",
|
|
value="stale",
|
|
)
|
|
reservation = {
|
|
"reserved_cost": 0.4,
|
|
"entries": [
|
|
{
|
|
"counter_key": "spend:key:key-budget-nonnumeric-release",
|
|
"reserved_cost": 0.4,
|
|
"applied_adjustment": 0.0,
|
|
}
|
|
],
|
|
"finalized": False,
|
|
}
|
|
|
|
with pytest.raises(RuntimeError, match="non-numeric"):
|
|
await release_budget_reservation(reservation)
|
|
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(
|
|
key="spend:key:key-budget-nonnumeric-release"
|
|
)
|
|
is None
|
|
)
|
|
assert reservation["finalized"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_invalidate_reserved_counters_after_persisted_spend_failure(
|
|
spend_counter_state,
|
|
):
|
|
counter_cache, _ = spend_counter_state
|
|
await counter_cache.async_increment_cache(
|
|
key="spend:key:key-budget-invalidate",
|
|
value=0.4,
|
|
)
|
|
await counter_cache.async_increment_cache(
|
|
key="spend:team:team-budget-invalidate",
|
|
value=0.4,
|
|
)
|
|
|
|
await invalidate_budget_reservation_counters(
|
|
{
|
|
"reserved_cost": 0.4,
|
|
"entries": [
|
|
{"counter_key": "spend:key:key-budget-invalidate"},
|
|
{"counter_key": "spend:team:team-budget-invalidate"},
|
|
],
|
|
}
|
|
)
|
|
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(key="spend:key:key-budget-invalidate")
|
|
is None
|
|
)
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(key="spend:team:team-budget-invalidate")
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_should_reserve_all_budgeted_counters(spend_counter_state):
|
|
counter_cache, key_cache = spend_counter_state
|
|
proxy_logging_obj = ProxyLogging(user_api_key_cache=key_cache)
|
|
valid_token = UserAPIKeyAuth(
|
|
token="key-budget-all",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
team_id="team-budget-all",
|
|
)
|
|
team_object = LiteLLM_TeamTable(
|
|
team_id="team-budget-all",
|
|
spend=0.0,
|
|
max_budget=1.0,
|
|
)
|
|
|
|
with patch(
|
|
"litellm.proxy.spend_tracking.budget_reservation.estimate_request_max_cost",
|
|
return_value=0.3,
|
|
):
|
|
reservation = await reserve_budget_for_request(
|
|
request_body=_request_body(),
|
|
route="/chat/completions",
|
|
llm_router=None,
|
|
valid_token=valid_token,
|
|
team_object=team_object,
|
|
user_object=None,
|
|
prisma_client=None,
|
|
user_api_key_cache=key_cache,
|
|
proxy_logging_obj=proxy_logging_obj,
|
|
)
|
|
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(key="spend:key:key-budget-all") == 0.3
|
|
)
|
|
assert (
|
|
counter_cache.in_memory_cache.get_cache(key="spend:team:team-budget-all") == 0.3
|
|
)
|
|
|
|
await release_budget_reservation(reservation)
|