Repository navigation
test(integration): regression tests for August cost tracking and budgeting bugs #42622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 32 commits
1813790
8b3c216
d78f616
3160182
8efad10
6939792
4449c3c
c651180
50c6e18
bea0ef7
a5e51de
fdaf3de
becec33
3998d7b
18a809d
712f9cf
c03b684
c5af6bc
d567395
22d86f4
1ac0f47
9dc01c6
8c592a4
910348b
b270821
054c2a9
636adfe
f54bc74
df7db2c
96213de
767cb7d
2d2bdf8
240b274
272f420
28fab25
865c3c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| from datetime import datetime, timedelta, timezone | ||
| from typing import Final | ||
|
|
||
| import pytest | ||
|
|
||
| from tests.integration._support.client import Gateway, string_value | ||
| from tests.integration._support.database import read_rows | ||
|
|
||
|
|
||
| def _persisted_reset_at(budget_id: str) -> datetime: | ||
| rows: Final = read_rows( | ||
| 'SELECT budget_reset_at::text AS reset_at FROM "LiteLLM_BudgetTable" WHERE budget_id = %s', (budget_id,) | ||
| ) | ||
| assert len(rows) == 1, rows | ||
| reset_at: Final = datetime.fromisoformat(string_value(rows[0]["reset_at"])) | ||
| return reset_at if reset_at.tzinfo is not None else reset_at.replace(tzinfo=timezone.utc) | ||
|
|
||
|
|
||
| @pytest.mark.covers("mgmt.budget.update.duration_change_recomputes_reset_at") | ||
| def test_shortening_budget_duration_moves_reset_at_onto_the_new_schedule(gateway: Gateway) -> None: | ||
| with gateway.scenario() as scenario: | ||
| budget_id: Final = scenario.budget(max_budget=10.0, budget_duration="30d") | ||
| monthly_reset_at: Final = _persisted_reset_at(budget_id) | ||
| before: Final = datetime.now(timezone.utc) | ||
| response: Final = gateway.request("POST", "/budget/update", {"budget_id": budget_id, "budget_duration": "1d"}) | ||
| assert response.status_code == 200, response.text | ||
| updated: Final = _persisted_reset_at(budget_id) | ||
| assert updated < monthly_reset_at, f"{updated} not before {monthly_reset_at}" | ||
| assert before < updated <= before + timedelta(days=1, minutes=5), f"{updated} not within 1d of {before}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import uuid | ||
| from typing import Final | ||
|
|
||
| import pytest | ||
|
|
||
| from tests.integration._support.client import Gateway, object_value, string_value | ||
| from tests.integration._support.database import read_rows | ||
|
|
||
|
|
||
| def _budget_rows(budget_id: str) -> list[dict[str, object]]: | ||
| return read_rows( | ||
| 'SELECT tpm_limit, rpm_limit, max_budget FROM "LiteLLM_BudgetTable" WHERE budget_id = %s', (budget_id,) | ||
| ) | ||
|
|
||
|
|
||
| @pytest.mark.covers("mgmt.organization.update.null_clears_budget_limit") | ||
| def test_patch_organization_update_with_null_tpm_limit_clears_it_and_keeps_sibling_limits(gateway: Gateway) -> None: | ||
| created: Final = gateway.post( | ||
| "/organization/new", | ||
| { | ||
| "organization_alias": f"integration-{uuid.uuid4().hex}", | ||
| "tpm_limit": 4000, | ||
| "rpm_limit": 40, | ||
| "max_budget": 12.5, | ||
| }, | ||
| ) | ||
| organization_id: Final = string_value(created["organization_id"]) | ||
| budget_id: Final = string_value(created["budget_id"]) | ||
| try: | ||
| assert _budget_rows(budget_id) == [{"tpm_limit": 4000, "rpm_limit": 40, "max_budget": 12.5}] | ||
| updated: Final = gateway.request( | ||
| "PATCH", "/organization/update", {"organization_id": organization_id, "tpm_limit": None} | ||
| ) | ||
| assert updated.status_code == 200, updated.text | ||
| updated_budget: Final = object_value(object_value(updated.json())["litellm_budget_table"]) | ||
| assert (updated_budget["tpm_limit"], updated_budget["rpm_limit"], updated_budget["max_budget"]) == ( | ||
| None, | ||
| 40, | ||
| 12.5, | ||
| ), updated.text | ||
| assert _budget_rows(budget_id) == [{"tpm_limit": None, "rpm_limit": 40, "max_budget": 12.5}] | ||
| info: Final = gateway.request("GET", "/organization/info", params={"organization_id": organization_id}) | ||
| assert info.status_code == 200, info.text | ||
| info_budget: Final = object_value(object_value(info.json())["litellm_budget_table"]) | ||
| assert (info_budget["tpm_limit"], info_budget["rpm_limit"], info_budget["max_budget"]) == ( | ||
| None, | ||
| 40, | ||
| 12.5, | ||
| ), info.text | ||
| finally: | ||
| deleted: Final = gateway.request("DELETE", "/organization/delete", {"organization_ids": [organization_id]}) | ||
| assert deleted.status_code == 200, deleted.text | ||
| gateway.post("/budget/delete", {"id": budget_id}) | ||
| assert ( | ||
| read_rows( | ||
| 'SELECT organization_id FROM "LiteLLM_OrganizationTable" WHERE organization_id = %s', (organization_id,) | ||
| ) | ||
| == [] | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import uuid | ||
| from collections.abc import Iterator | ||
| from contextlib import contextmanager | ||
| from typing import Final | ||
|
|
||
| import pytest | ||
| from pydantic import JsonValue | ||
|
|
||
| from tests.integration._support.client import Gateway, string_value | ||
| from tests.integration._support.database import read_rows | ||
|
|
||
|
|
||
| @contextmanager | ||
| def _default_team_budget_duration(gateway: Gateway, duration: str) -> Iterator[None]: | ||
| configured: Final = gateway.request("PATCH", "/update/default_team_settings", {"budget_duration": duration}) | ||
| assert configured.status_code == 200, configured.text | ||
| try: | ||
| yield | ||
| finally: | ||
| cleared: Final = gateway.request("PATCH", "/update/default_team_settings", {}) | ||
| assert cleared.status_code == 200, cleared.text | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Team defaults leak across shared proxyMedium Severity
Reviewed by Cursor Bugbot for commit 2d2bdf8. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 240b274: the test now runs on an owned proxy with the default set in yaml, so nothing is persisted |
||
|
|
||
|
|
||
| def _budget_row(team_id: str) -> dict[str, JsonValue]: | ||
| rows: Final = read_rows( | ||
| 'SELECT max_budget, budget_duration, budget_reset_at::text FROM "LiteLLM_TeamTable" WHERE team_id = %s', | ||
| (team_id,), | ||
| ) | ||
| assert len(rows) == 1, rows | ||
| return rows[0] | ||
|
|
||
|
|
||
| @pytest.mark.covers("mgmt.team.new.explicit_null_budget_duration_overrides_default") | ||
| def test_team_new_explicit_null_budget_duration_is_not_replaced_by_default(gateway: Gateway) -> None: | ||
| with gateway.scenario() as scenario, _default_team_budget_duration(gateway, "30d"): | ||
| never_resetting: Final = gateway.request( | ||
| "POST", | ||
| "/team/new", | ||
| {"team_alias": f"integration-{uuid.uuid4().hex}", "max_budget": 500, "budget_duration": None}, | ||
| ) | ||
| assert never_resetting.status_code == 200, never_resetting.text | ||
| never_resetting_id: Final = string_value(never_resetting.json()["team_id"]) | ||
| scenario.cleanups.callback(scenario.delete_team, never_resetting_id) | ||
| assert never_resetting.json()["max_budget"] == 500.0, never_resetting.text | ||
| assert never_resetting.json()["budget_duration"] is None, never_resetting.text | ||
| assert never_resetting.json()["budget_reset_at"] is None, never_resetting.text | ||
| assert _budget_row(never_resetting_id) == { | ||
| "max_budget": 500.0, | ||
| "budget_duration": None, | ||
| "budget_reset_at": None, | ||
| } | ||
|
|
||
| inheriting: Final = gateway.request( | ||
| "POST", "/team/new", {"team_alias": f"integration-{uuid.uuid4().hex}", "max_budget": 500} | ||
| ) | ||
| assert inheriting.status_code == 200, inheriting.text | ||
| inheriting_id: Final = string_value(inheriting.json()["team_id"]) | ||
| scenario.cleanups.callback(scenario.delete_team, inheriting_id) | ||
| assert inheriting.json()["budget_duration"] == "30d", inheriting.text | ||
| assert inheriting.json()["budget_reset_at"] is not None, inheriting.text | ||
| inheriting_row: Final = _budget_row(inheriting_id) | ||
| assert inheriting_row["max_budget"] == 500.0, inheriting_row | ||
| assert inheriting_row["budget_duration"] == "30d", inheriting_row | ||
| assert inheriting_row["budget_reset_at"] is not None, inheriting_row | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import os | ||
| from typing import Final | ||
|
|
||
| import pytest | ||
| from pydantic import JsonValue, TypeAdapter | ||
| from redis import Redis | ||
|
|
||
| from tests.integration._support.client import Gateway, eventually, object_value, string_value | ||
| from tests.integration._support.database import read_rows | ||
|
|
||
| _CACHED_BUDGET: Final = TypeAdapter(dict[str, JsonValue]) | ||
|
|
||
|
|
||
| @pytest.mark.covers("mgmt.team_member_budget.default_budget_is_cached_in_redis_as_json") | ||
| def test_team_member_default_budget_lands_in_redis_after_first_member_call(gateway: Gateway) -> None: | ||
| with gateway.scenario() as scenario: | ||
| model: Final = scenario.model() | ||
| user: Final = scenario.user() | ||
| team: Final = scenario.team(team_member_budget=25) | ||
| key: Final = scenario.key(team_id=team, user_id=user, models=[model]) | ||
| teams: Final = read_rows('SELECT metadata FROM "LiteLLM_TeamTable" WHERE team_id = %s', (team,)) | ||
| assert len(teams) == 1, teams | ||
| budget_id: Final = string_value(object_value(teams[0]["metadata"])["team_member_budget_id"]) | ||
| response: Final = gateway.request( | ||
| "POST", | ||
| "/v1/chat/completions", | ||
| {"model": model, "messages": [{"role": "user", "content": "member budget cache"}]}, | ||
| key=key, | ||
| ) | ||
| assert response.status_code == 200, response.text | ||
| with Redis(host=os.environ["REDIS_HOST"], port=int(os.environ["REDIS_PORT"])) as cache: | ||
| cached: Final = eventually( | ||
| lambda: cache.get(f"team_member_default_budget:{budget_id}"), | ||
| lambda value: value is not None, | ||
| seconds=10, | ||
| ) | ||
| assert isinstance(cached, bytes), cached | ||
| budget: Final = _CACHED_BUDGET.validate_json(cached) | ||
| assert budget["budget_id"] == budget_id, cached | ||
| assert budget["max_budget"] == 25, cached |


Uh oh!
There was an error while loading. Please reload this page.