fix(spend): scope org report team fallback to unstamped rows and bound report date ranges

This commit is contained in:
Yuneng Jiang 2026-08-03 17:41:49 -07:00
parent 722d9ffa4f
commit 26ffb5d04e
No known key found for this signature in database
2 changed files with 82 additions and 2 deletions

View file

@ -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

View file

@ -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)