From 479e997eed08e4393ceb7b0c4d39c896c4b19da3 Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Mon, 20 Jul 2026 09:55:38 -0700 Subject: [PATCH] feat(spend): raise /spend/logs/v2 page_size cap to 1000 Clients exporting large spend-log ranges were forced into 100-row pages, which meant a bounded COUNT plus an increasingly deep OFFSET scan per request. Larger pages reduce both the request count and the cumulative OFFSET cost for the same result set. The handler already excludes the heavy JSON columns (messages, response, proxy_server_request) from the paginated SELECT and bounds the COUNT via SPEND_LOGS_PAGINATION_COUNT_CAP, so per-row cost does not grow with page size. 1000 matches the ceiling already used by the user and user-agent analytics list endpoints. --- .../spend_management_endpoints.py | 2 +- .../test_spend_management_endpoints.py | 53 +++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 5a0b94d15248..55b50e7d9ffa 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1647,7 +1647,7 @@ async def ui_view_spend_logs( description="Time till which to view key spend", ), page: int = fastapi.Query(default=1, description="Page number for pagination", ge=1), - page_size: int = fastapi.Query(default=50, description="Number of items per page", ge=1, le=100), + page_size: int = fastapi.Query(default=50, description="Number of items per page", ge=1, le=1000), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), status_filter: str | None = fastapi.Query( default=None, description="Filter logs by status (e.g., success, failure)" 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 579f46c8c773..db72a7fb38c1 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 @@ -1467,6 +1467,59 @@ async def test_ui_view_spend_logs_pagination(client, monkeypatch): app.dependency_overrides.pop(ps.user_api_key_auth, None) +@pytest.mark.parametrize( + "page_size, expected_status, expected_rows", + [ + (1000, 200, 1000), + (1001, 422, None), + ], +) +@pytest.mark.asyncio +async def test_ui_view_spend_logs_page_size_upper_bound( + client, monkeypatch, page_size, expected_status, expected_rows +): + mock_spend_logs = [ + { + "id": f"log{i}", + "request_id": f"req{i}", + "api_key": "sk-test-key", + "startTime": datetime.datetime.now(timezone.utc).isoformat(), + "model": "gpt-4", + } + for i in range(1200) + ] + + monkeypatch.setattr( + "litellm.proxy.proxy_server.prisma_client", + make_ui_spend_logs_mock_prisma(mock_spend_logs, lambda where: mock_spend_logs), + ) + + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin_user" + ) + + try: + start_date, end_date = _default_date_range() + response = client.get( + "/spend/logs/v2", + params={ + "page": 1, + "page_size": page_size, + "start_date": start_date, + "end_date": end_date, + }, + headers={"Authorization": "Bearer sk-test"}, + ) + + assert response.status_code == expected_status + if expected_status == 200: + data = response.json() + assert data["page_size"] == page_size + assert len(data["data"]) == expected_rows + finally: + app.dependency_overrides.pop(ps.user_api_key_auth, None) + + @pytest.mark.asyncio async def test_ui_view_session_spend_logs_pagination(client, monkeypatch): mock_spend_logs = [