From 379aa7b79a4ba163ce4892a655d3fb0e3254ee95 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Thu, 13 Nov 2025 22:03:00 -0800 Subject: [PATCH] Pagination for /spend/logs/session/ui endpoint (#16603) --- .../spend_management_endpoints.py | 51 ++++++++++++++++-- .../test_spend_management_endpoints.py | 53 +++++++++++++++++++ 2 files changed, 99 insertions(+), 5 deletions(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index b60acd14281..a167f564fd9 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -2968,10 +2968,30 @@ async def ui_view_session_spend_logs( session_id: str = fastapi.Query( description="Get all spend logs for a particular session", ), + page: int = fastapi.Query( + default=1, + ge=1, + description="Page number for pagination", + ), + page_size: int = fastapi.Query( + default=50, + ge=1, + le=100, + description="Number of items per page", + ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - Get all spend logs for a particular session + Get paginated spend logs for a particular session. + + Returns: + { + "data": List[LiteLLM_SpendLogs], + "total": int, + "page": int, + "page_size": int, + "total_pages": int, + } """ from litellm.proxy.proxy_server import prisma_client @@ -2984,11 +3004,32 @@ async def ui_view_session_spend_logs( # Build query conditions where_conditions = {"session_id": session_id} - # Query the database - result = await prisma_client.db.litellm_spendlogs.find_many( - where=where_conditions, order={"startTime": "asc"} + + # Calculate pagination offsets + skip = (page - 1) * page_size + + # Get total count for pagination metadata + total_records = await prisma_client.db.litellm_spendlogs.count( + where=where_conditions ) - return result + + # Query the database with pagination + result = await prisma_client.db.litellm_spendlogs.find_many( + where=where_conditions, + order={"startTime": "asc"}, + skip=skip, + take=page_size, + ) + + total_pages = (total_records + page_size - 1) // page_size + + return { + "data": result, + "total": total_records, + "page": page, + "page_size": page_size, + "total_pages": total_pages, + } except Exception as e: if isinstance(e, HTTPException): raise e 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 3a9229b3c45..7acca1804f7 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 @@ -628,6 +628,59 @@ async def test_ui_view_spend_logs_pagination(client, monkeypatch): assert data["page"] == 2 +@pytest.mark.asyncio +async def test_ui_view_session_spend_logs_pagination(client, monkeypatch): + mock_spend_logs = [ + { + "id": "log1", + "request_id": "req1", + "session_id": "session-123", + "startTime": "2024-01-01T00:00:00Z", + }, + { + "id": "log2", + "request_id": "req2", + "session_id": "session-123", + "startTime": "2024-01-02T00:00:00Z", + }, + ] + + class MockDB: + async def count(self, *args, **kwargs): + assert kwargs.get("where") == {"session_id": "session-123"} + return len(mock_spend_logs) + + async def find_many(self, *args, **kwargs): + assert kwargs.get("where") == {"session_id": "session-123"} + assert kwargs.get("order") == {"startTime": "asc"} + assert kwargs.get("skip") == 1 # page=2, page_size=1 + assert kwargs.get("take") == 1 + return [mock_spend_logs[1]] + + class MockPrismaClient: + def __init__(self): + self.db = MockDB() + self.db.litellm_spendlogs = self.db + + mock_prisma_client = MockPrismaClient() + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + response = client.get( + "/spend/logs/session/ui", + params={"session_id": "session-123", "page": 2, "page_size": 1}, + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == 200 + data = response.json() + assert data["total"] == 2 + assert data["page"] == 2 + assert data["page_size"] == 1 + assert data["total_pages"] == 2 + assert len(data["data"]) == 1 + assert data["data"][0]["request_id"] == "req2" + + @pytest.mark.asyncio async def test_ui_view_spend_logs_date_range_filter(client, monkeypatch): # Create mock data with different dates