diff --git a/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py index 59bb3cdc471..e86a6d000ed 100644 --- a/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -618,6 +618,90 @@ async def test_ui_view_spend_logs_with_model(client, monkeypatch): assert data["data"][0]["model"] == "gpt-3.5-turbo" +@pytest.mark.asyncio +async def test_ui_view_spend_logs_with_key_hash(client, monkeypatch): + # Mock data for the test + mock_spend_logs = [ + { + "id": "log1", + "request_id": "req1", + "api_key": "sk-test-key-1", + "user": "test_user_1", + "team_id": "team1", + "spend": 0.05, + "startTime": datetime.datetime.now(timezone.utc).isoformat(), + "model": "gpt-3.5-turbo", + }, + { + "id": "log2", + "request_id": "req2", + "api_key": "sk-test-key-2", + "user": "test_user_2", + "team_id": "team2", + "spend": 0.10, + "startTime": datetime.datetime.now(timezone.utc).isoformat(), + "model": "gpt-4", + }, + ] + + # Create a mock prisma client + class MockDB: + async def find_many(self, *args, **kwargs): + # Filter based on key_hash in the where conditions + if ( + "where" in kwargs + and "api_key" in kwargs["where"] + and kwargs["where"]["api_key"] == "sk-test-key-1" + ): + return [mock_spend_logs[0]] + return mock_spend_logs + + async def count(self, *args, **kwargs): + # Return count based on key_hash filter + if ( + "where" in kwargs + and "api_key" in kwargs["where"] + and kwargs["where"]["api_key"] == "sk-test-key-1" + ): + return 1 + return len(mock_spend_logs) + + class MockPrismaClient: + def __init__(self): + self.db = MockDB() + self.db.litellm_spendlogs = self.db + + # Apply the monkeypatch + mock_prisma_client = MockPrismaClient() + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + # Set up test dates + 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") + + # Make the request with key_hash filter + response = client.get( + "/spend/logs/ui", + params={ + "api_key": "sk-test-key-1", + "start_date": start_date, + "end_date": end_date, + }, + headers={"Authorization": "Bearer sk-test"}, + ) + + # Assert response + assert response.status_code == 200 + data = response.json() + + # Verify the filtered data + assert data["total"] == 1 + assert len(data["data"]) == 1 + assert data["data"][0]["api_key"] == "sk-test-key-1" + + class TestSpendLogsPayload: @pytest.mark.asyncio async def test_spend_logs_payload_e2e(self): diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 06dd31eb9af..b5946bc67c1 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -251,6 +251,7 @@ export default function SpendLogsTable({ } setSelectedStatus(filters['Status'] || ""); setSelectedModel(filters['Model'] || ""); + setSelectedKeyHash(filters['Key Hash'] || ""); }, [filters]); // Fetch logs for a session if selected @@ -383,6 +384,11 @@ export default function SpendLogsTable({ value: model })); } + }, + { + name: 'Key Hash', + label: 'Key Hash', + isSearchable: false, } ] diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index 0ff5fd5be2e..9e8ce094c97 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -133,6 +133,12 @@ export function useLogFilterLogic({ log => log.model === filters[FILTER_KEYS.MODEL] ); } + + if (filters[FILTER_KEYS.KEY_HASH]) { + filteredData = filteredData.filter( + log => log.api_key === filters[FILTER_KEYS.KEY_HASH] + ); + } const newFilteredLogs: PaginatedResponse = { data: filteredData,