feat(proxy): add model_group filter to /spend/logs/v2 endpoint

Add an optional `model_group` query parameter to the `/spend/logs/v2`
and `/spend/logs/ui` endpoints, allowing users to filter spend logs by
model group. This is consistent with the existing `model` and `model_id`
filters and requires no schema changes since `model_group` is already a
column in the `LiteLLM_SpendLogs` table.

Fixes #24781

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lengkejun 2026-03-30 18:09:48 +08:00
parent 72a461ba4a
commit 0fad9e2cbd
3 changed files with 95 additions and 20 deletions

View file

@ -1740,6 +1740,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
default=None,
description="Filter logs by model ID (litellm model deployment id)",
),
model_group: Optional[str] = fastapi.Query(
default=None, description="Filter logs by model group"
),
key_alias: Optional[str] = fastapi.Query(
default=None, description="Filter logs by key alias"
),
@ -1869,6 +1872,9 @@ async def ui_view_spend_logs( # noqa: PLR0915
if model_id is not None:
where_conditions["model_id"] = model_id
if model_group is not None:
where_conditions["model_group"] = model_group
# Build metadata filters
metadata_filters = []
if key_alias is not None:
@ -1992,6 +1998,7 @@ async def ui_view_spend_logs( # noqa: PLR0915
("request_id", "request_id"),
("model", "model"),
("model_id", "model_id"),
("model_group", "model_group"),
("end_user", "end_user"),
]:
val = where_conditions.get(wc_key)

View file

@ -1373,6 +1373,71 @@ async def test_ui_view_spend_logs_with_model_id(client, monkeypatch):
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_with_model_group(client, monkeypatch):
"""Test that the model_group query param filters spend logs by model group."""
mock_spend_logs = [
{
"id": "log1",
"request_id": "req1",
"api_key": "sk-test-key",
"user": "test_user_1",
"team_id": "team1",
"spend": 0.05,
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
"model": "gpt-3.5-turbo",
"model_group": "gpt-3.5-turbo",
"status": "success",
},
{
"id": "log2",
"request_id": "req2",
"api_key": "sk-test-key",
"user": "test_user_2",
"team_id": "team1",
"spend": 0.10,
"startTime": datetime.datetime.now(timezone.utc).isoformat(),
"model": "gpt-4-0613",
"model_group": "gpt-4",
"status": "success",
},
]
def filter_by_model_group(where):
if "model_group" in where and where["model_group"] == "gpt-4":
return [mock_spend_logs[1]]
return mock_spend_logs
monkeypatch.setattr(
"litellm.proxy.proxy_server.prisma_client",
make_ui_spend_logs_mock_prisma(mock_spend_logs, filter_by_model_group),
)
start_date, end_date = _default_date_range()
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=LitellmUserRoles.PROXY_ADMIN
)
try:
response = client.get(
"/spend/logs/ui",
params={
"model_group": "gpt-4",
"start_date": start_date,
"end_date": end_date,
},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert len(data["data"]) == 1
assert data["data"][0]["model_group"] == "gpt-4"
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_with_key_hash(client, monkeypatch):
mock_spend_logs = [
@ -1465,10 +1530,13 @@ class TestSpendLogsPayload:
litellm.callbacks = [_ProxyDBLogger(message_logging=False)]
# litellm._turn_on_debug()
with patch.object(
litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter,
"_insert_spend_log_to_db",
) as mock_client, patch.object(litellm.proxy.proxy_server, "prisma_client"):
with (
patch.object(
litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter,
"_insert_spend_log_to_db",
) as mock_client,
patch.object(litellm.proxy.proxy_server, "prisma_client"),
):
response = await litellm.acompletion(
model="gpt-4o",
messages=[{"role": "user", "content": "Hello, world!"}],
@ -1558,13 +1626,13 @@ class TestSpendLogsPayload:
client = AsyncHTTPHandler()
with patch.object(
litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter,
"_insert_spend_log_to_db",
) as mock_client, patch.object(
litellm.proxy.proxy_server, "prisma_client"
), patch.object(
client, "post", side_effect=self.mock_anthropic_response
with (
patch.object(
litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter,
"_insert_spend_log_to_db",
) as mock_client,
patch.object(litellm.proxy.proxy_server, "prisma_client"),
patch.object(client, "post", side_effect=self.mock_anthropic_response),
):
response = await litellm.acompletion(
model="claude-4-sonnet-20250514",
@ -1652,13 +1720,13 @@ class TestSpendLogsPayload:
]
)
with patch.object(
litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter,
"_insert_spend_log_to_db",
) as mock_client, patch.object(
litellm.proxy.proxy_server, "prisma_client"
), patch.object(
client, "post", side_effect=self.mock_anthropic_response
with (
patch.object(
litellm.proxy.db.db_spend_update_writer.DBSpendUpdateWriter,
"_insert_spend_log_to_db",
) as mock_client,
patch.object(litellm.proxy.proxy_server, "prisma_client"),
patch.object(client, "post", side_effect=self.mock_anthropic_response),
):
response = await router.acompletion(
model="my-anthropic-model-group",

4
uv.lock generated
View file

@ -11,7 +11,7 @@ resolution-markers = [
]
[options]
exclude-newer = "2026-04-08T16:01:27.663665Z"
exclude-newer = "2026-04-12T07:48:54.018235Z"
exclude-newer-span = "P3D"
[manifest]
@ -3602,7 +3602,7 @@ wheels = [
[[package]]
name = "litellm"
version = "1.83.6"
version = "1.83.8"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },