mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(spend): scope org report team fallback to unstamped rows and bound report date ranges
This commit is contained in:
parent
722d9ffa4f
commit
26ffb5d04e
2 changed files with 82 additions and 2 deletions
|
|
@ -1458,6 +1458,8 @@ async def get_global_spend_report(
|
|||
|
||||
_SPEND_REPORT_SCOPE_COLUMNS = frozenset({"api_key", "user", "team_id"})
|
||||
|
||||
_SPEND_REPORT_MAX_RANGE_DAYS = 366
|
||||
|
||||
|
||||
def _scoped_spend_report_sql(scope_column: str) -> str:
|
||||
"""Spend grouped by api_key with a per-model breakdown, cut to one scope column.
|
||||
|
|
@ -1520,7 +1522,13 @@ _ORG_SPEND_REPORT_SQL = """
|
|||
WHERE
|
||||
sl."startTime" >= ($1::timestamptz AT TIME ZONE 'UTC')
|
||||
AND sl."startTime" < (($2::timestamptz + INTERVAL '1 day') AT TIME ZONE 'UTC')
|
||||
AND (sl.organization_id = $3 OR sl.team_id = ANY($4::text[]))
|
||||
AND (
|
||||
sl.organization_id = $3
|
||||
OR (
|
||||
(sl.organization_id IS NULL OR sl.organization_id = '')
|
||||
AND sl.team_id = ANY($4::text[])
|
||||
)
|
||||
)
|
||||
GROUP BY
|
||||
sl.api_key,
|
||||
sl.team_id,
|
||||
|
|
@ -1579,6 +1587,17 @@ def _parse_spend_report_date_range(start_date: str | None, end_date: str | None)
|
|||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="start_date and end_date must be in YYYY-MM-DD format",
|
||||
)
|
||||
start_date_obj, end_date_obj = parsed
|
||||
if end_date_obj < start_date_obj:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="start_date must be on or before end_date",
|
||||
)
|
||||
if end_date_obj - start_date_obj > timedelta(days=_SPEND_REPORT_MAX_RANGE_DAYS):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Date range too large; maximum is {_SPEND_REPORT_MAX_RANGE_DAYS} days",
|
||||
)
|
||||
return parsed
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5096,7 +5096,11 @@ def test_org_spend_report_proxy_admin_override(client, monkeypatch):
|
|||
assert response.status_code == 200
|
||||
args, _ = mock_prisma.db.query_raw.await_args
|
||||
sql, _, _, org_param, team_ids_param = args
|
||||
assert "(sl.organization_id = $3 OR sl.team_id = ANY($4::text[]))" in sql
|
||||
normalized_sql = " ".join(sql.split())
|
||||
assert (
|
||||
"AND ( sl.organization_id = $3 OR ( (sl.organization_id IS NULL OR sl.organization_id = '') "
|
||||
"AND sl.team_id = ANY($4::text[]) ) )"
|
||||
) in normalized_sql
|
||||
assert org_param == "org-x"
|
||||
assert team_ids_param == ("team-a", "team-b")
|
||||
finally:
|
||||
|
|
@ -5238,3 +5242,60 @@ def test_scoped_spend_report_invalid_date_format_400(client, monkeypatch):
|
|||
mock_prisma.db.query_raw.assert_not_awaited()
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
def test_scoped_spend_report_reversed_range_400(client, monkeypatch):
|
||||
mock_prisma = _spend_report_mock_prisma()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER, user_id="alice", api_key="hashed-k"
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/key/spend/report",
|
||||
params={"start_date": "2026-08-04", "end_date": "2026-08-01"},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
mock_prisma.db.query_raw.assert_not_awaited()
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
def test_scoped_spend_report_range_over_max_400(client, monkeypatch):
|
||||
mock_prisma = _spend_report_mock_prisma()
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER, user_id="alice", api_key="hashed-k"
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/key/spend/report",
|
||||
params={"start_date": "0001-01-01", "end_date": "9999-12-31"},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
mock_prisma.db.query_raw.assert_not_awaited()
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
def test_scoped_spend_report_range_at_max_allowed(client, monkeypatch):
|
||||
mock_prisma = _spend_report_mock_prisma(query_raw_returns=[])
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", True)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.INTERNAL_USER, user_id="alice", api_key="hashed-k"
|
||||
)
|
||||
try:
|
||||
response = client.get(
|
||||
"/key/spend/report",
|
||||
params={"start_date": "2025-08-03", "end_date": "2026-08-04"},
|
||||
headers={"Authorization": "Bearer sk-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
mock_prisma.db.query_raw.assert_awaited_once()
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue