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
Merged
Merged
Changes from all commits
Commits
Show all changes
36 commits
Select commit
Hold shift + click to select a range
1813790
test(integration): saving echoed model_info never persists cost map p…
kerry-berri 8b3c216
test(integration): budget_duration change on /budget/update recompute…
kerry-berri d78f616
test(integration): count_tokens on a budgeted key reserves no budget …
kerry-berri 3160182
test(integration): /cost/estimate reports configured prices for a dep…
kerry-berri 8efad10
test(integration): cache the team member default budget in Redis as J…
kerry-berri 6939792
test(integration): aggregated team daily activity reports whole-range…
kerry-berri 4449c3c
test(integration): failed daily user rollup commits are retried so sp…
kerry-berri c651180
test(integration): CLI session token without org_id is charged to and…
kerry-berri 50c6e18
test(integration): gemini passthrough success releases its budget res…
kerry-berri bea0ef7
test(integration): batch retrieval spend row sums reasoning tokens an…
kerry-berri a5e51de
test(integration): uncostable batches retire from the cost poll page …
kerry-berri fdaf3de
test(integration): charge a team member added without any budget on i…
kerry-berri becec33
test(integration): failed dispatched requests keep estimated input to…
kerry-berri 3998d7b
test(integration): bedrock passthrough converse guardrail ignores too…
kerry-berri 18a809d
test(integration): explicit null budget_duration on /team/new is not …
kerry-berri 712f9cf
test(integration): PATCH /organization/update with a null limit clear…
kerry-berri c03b684
test(integration): ultrafast service_tier bills ultrafast rates witho…
kerry-berri c5af6bc
test(integration): keep the selected model in the response and spend …
kerry-berri d567395
test(integration): disconnected Bedrock /v1/messages stream still bil…
kerry-berri 22d86f4
test(integration): databricks cached prompt tokens bill at cache rate…
kerry-berri 1ac0f47
test(integration): completed batch spend row records reasoning tokens…
kerry-berri 9dc01c6
test(integration): bill OCR annotation pages at annotation_cost_per_p…
kerry-berri 8c592a4
test(integration): in-flight count tokens request reserves no key bud…
kerry-berri 910348b
test(integration): fail-closed key rejects known estimate over remain…
kerry-berri b270821
test(integration): streamed /v1/responses success callbacks keep prov…
kerry-berri 054c2a9
Revert "test(integration): fail-closed key rejects known estimate ove…
kerry-berri 636adfe
test(integration): reconcile contracts manifest for bundled regressio…
kerry-berri f54bc74
test(integration): isolate proxy config writes in bundled regression …
kerry-berri df7db2c
test(integration): address review feedback on budget reset bounds and…
kerry-berri 96213de
test(integration): avoid rebinding the cache identity accumulator
kerry-berri 767cb7d
test(integration): assert forwarded messages per cache identity call
kerry-berri 2d2bdf8
Merge branch 'main' into litellm_pylon_regression_tests_accounting
kerry-berri 240b274
test(integration): make budget reset and team default tests determini…
kerry-berri 272f420
Merge remote-tracking branch 'origin/main' into litellm_pylon_regress…
kerry-berri 28fab25
Merge remote-tracking branch 'origin/main' into litellm_pylon_regress…
kerry-berri 865c3c2
Merge remote-tracking branch 'origin/main' into litellm_pylon_regress…
kerry-berri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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="10d") | ||
| ten_day_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 < ten_day_reset_at, f"{updated} not before {ten_day_reset_at}" | ||
| assert before < updated <= before + timedelta(days=1, minutes=5), f"{updated} not within 1d of {before}" | ||
59 changes: 59 additions & 0 deletions
59
tests/integration/management/test_organization_budget_clear.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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,) | ||
| ) | ||
| == [] | ||
| ) |
61 changes: 61 additions & 0 deletions
61
tests/integration/management/test_team_budget_duration_defaults.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import uuid | ||
| from pathlib import Path | ||
| from typing import Final | ||
|
|
||
| import pytest | ||
| import yaml | ||
| from pydantic import JsonValue | ||
|
|
||
| from tests.integration._support.client import Gateway, string_value | ||
| from tests.integration._support.database import read_rows | ||
| from tests.integration._support.process import owned_proxy | ||
|
|
||
|
|
||
| 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, tmp_path: Path) -> None: | ||
| config: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()) | ||
| config["litellm_settings"]["default_team_params"] = {"budget_duration": "30d"} | ||
| path: Final = tmp_path / "team-defaults.yaml" | ||
| path.write_text(yaml.safe_dump(config)) | ||
| with ( | ||
| owned_proxy(gateway, tmp_path, {"STORE_MODEL_IN_DB": "False"}, config=path) as candidate, | ||
| candidate.scenario() as scenario, | ||
| ): | ||
| never_resetting: Final = candidate.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 = candidate.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 |
40 changes: 40 additions & 0 deletions
40
tests/integration/management/test_team_member_budget_cache.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.