diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index dcdc17ef31..6e49e4244e 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1681,6 +1681,9 @@ async def ui_view_spend_logs( # noqa: PLR0915 error_code: Optional[str] = fastapi.Query( default=None, description="Filter logs by error code (e.g., '404', '500')" ), + error_message: Optional[str] = fastapi.Query( + default=None, description="Filter logs by error message (partial string match)" + ), ): """ View spend logs with pagination support. @@ -1774,6 +1777,12 @@ async def ui_view_spend_logs( # noqa: PLR0915 "equals": f'"{error_code}"', }) + if error_message is not None: + metadata_filters.append({ + "path": ["error_information", "error_message"], + "string_contains": error_message, + }) + if metadata_filters: if len(metadata_filters) == 1: where_conditions["metadata"] = metadata_filters[0] 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 33e000143c..13368d0a14 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 @@ -1952,6 +1952,89 @@ async def test_ui_view_spend_logs_with_error_code(client): assert metadata["error_information"]["error_code"] == "404" +@pytest.mark.asyncio +async def test_ui_view_spend_logs_with_error_message(client): + """Test filtering spend logs by error message""" + 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", + "metadata": '{"error_information": {"error_message": "Rate limit exceeded"}}', + }, + { + "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", + "metadata": '{"error_information": {"error_message": "Invalid API key"}}', + }, + ] + + with patch.object(ps, "prisma_client") as mock_prisma: + # Mock the find_many method to return filtered results + async def mock_find_many(*args, **kwargs): + where_conditions = kwargs.get("where", {}) + if "metadata" in where_conditions: + metadata_filter = where_conditions["metadata"] + if metadata_filter.get("path") == ["error_information", "error_message"]: + error_message_filter = metadata_filter.get("string_contains") + # Check if the error message contains the filter string + if error_message_filter == "Rate limit": + return [mock_spend_logs[0]] + elif error_message_filter == "Invalid API": + return [mock_spend_logs[1]] + return mock_spend_logs + + async def mock_count(*args, **kwargs): + where_conditions = kwargs.get("where", {}) + if "metadata" in where_conditions: + metadata_filter = where_conditions["metadata"] + if metadata_filter.get("path") == ["error_information", "error_message"]: + error_message_filter = metadata_filter.get("string_contains") + if error_message_filter == "Rate limit": + return 1 + elif error_message_filter == "Invalid API": + return 1 + return len(mock_spend_logs) + + mock_prisma.db.litellm_spendlogs.find_many = mock_find_many + mock_prisma.db.litellm_spendlogs.count = mock_count + + start_date = ( + datetime.datetime.now(timezone.utc) - datetime.timedelta(days=7) + ).strftime("%Y-%m-%d %H:%M:%S") + end_date = datetime.datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") + + response = client.get( + "/spend/logs/ui", + params={ + "error_message": "Rate limit", + "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]["id"] == "log1" + metadata = json.loads(data["data"][0]["metadata"]) + assert "error_information" in metadata + assert "Rate limit exceeded" in metadata["error_information"]["error_message"] + + @pytest.mark.asyncio async def test_ui_view_spend_logs_with_error_code_and_key_alias(client): """Test merging error_code and key_alias filters with AND logic"""