diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 805d0ec1953..cd977d225f1 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -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) diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index a986017339b..41c2c3180ca 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -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", diff --git a/uv.lock b/uv.lock index 04224dc5374..ce5f6156300 100644 --- a/uv.lock +++ b/uv.lock @@ -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" },