fix(proxy): end the cursorless grouped log page at the capped total

A page size that does not divide SPEND_LOGS_PAGINATION_COUNT_CAP left the last
page starting inside the capped window and reading past it, so the rows
disagreed with the total reported next to them. The page limit now stops at the
end of that window, and has_more plus next_session_cursor still hand back a
cursor for walking further.

Claude-Session: https://claude.ai/code/session_01ESi9JwaXDww1vP3Qsrr4Mz
This commit is contained in:
ryan-crabbe-berri 2026-09-11 14:27:36 -07:00
parent 4295bf823a
commit cb434742c6
2 changed files with 48 additions and 9 deletions

View file

@ -2947,9 +2947,9 @@ async def _ui_session_grouped_spend_logs(
page depth does not degrade the query plan. A request for ``page > 1``
without a cursor (the UI jumping straight to the last page, or back to a
page it never walked through) falls back to ``OFFSET (page - 1) *
page_size``; a page starting at or past ``SPEND_LOGS_PAGINATION_COUNT_CAP``
lies outside the capped total the client is given, so it returns no rows
without running the query and the sort bound stays capped. Each session is represented
page_size``, trimmed to the end of the ``SPEND_LOGS_PAGINATION_COUNT_CAP``
window the capped ``total`` promises, so a page never runs past that total
and one starting at or past it returns no rows without a query. Each session is represented
by its newest non-MCP row, enriched by ``_build_ui_spend_logs_response``
exactly like the flat listing, and the response carries
``next_session_cursor`` / ``has_more`` while ``total`` counts sessions
@ -2969,8 +2969,8 @@ async def _ui_session_grouped_spend_logs(
cursor_params: Final[tuple[object, ...]] = cursor if cursor else ()
limit_index: Final = next_param_index + len(cursor_params)
offset: Final = (page - 1) * page_size if cursor is None else 0
beyond_capped_window: Final = offset >= SPEND_LOGS_PAGINATION_COUNT_CAP
offset_params: Final[tuple[int, ...]] = (offset,) if offset and not beyond_capped_window else ()
page_limit: Final = min(page_size, SPEND_LOGS_PAGINATION_COUNT_CAP - offset)
offset_params: Final[tuple[int, ...]] = (offset,) if offset and page_limit > 0 else ()
offset_clause: Final = f"OFFSET ${limit_index + 1}" if offset_params else ""
page_query: Final = f"""
@ -2986,12 +2986,12 @@ async def _ui_session_grouped_spend_logs(
"""
page_rows: Final[Sequence[_SessionPageRow]] = (
()
if beyond_capped_window
else await _query_raw(prisma_client, page_query, *sql_params, *cursor_params, page_size + 1, *offset_params)
if page_limit <= 0
else await _query_raw(prisma_client, page_query, *sql_params, *cursor_params, page_limit + 1, *offset_params)
)
has_more: Final = len(page_rows) > page_size
visible_rows: Final = page_rows[:page_size]
has_more: Final = len(page_rows) > page_limit
visible_rows: Final = page_rows[:page_limit]
next_cursor: Final = (
f"{visible_rows[-1]['last_activity']}|{visible_rows[-1]['api_key']}|{visible_rows[-1]['session_key']}"
if has_more and visible_rows

View file

@ -6853,6 +6853,45 @@ async def test_ui_view_spend_logs_group_by_session_page_past_count_cap_is_empty(
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_last_page_stops_at_the_capped_total(client, monkeypatch):
"""A page size that does not divide the cap still ends the last page at the capped total it reports."""
cap = spend_management_endpoints.SPEND_LOGS_PAGINATION_COUNT_CAP
sessions = tuple((f"sess-{index:06d}", "2026-08-29 10:00:00") for index in range(cap + 50))
mock_prisma = _session_grouped_paginating_prisma(sessions)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
monkeypatch.setattr(
"litellm.proxy.spend_tracking.spend_management_endpoints._is_admin_view_safe",
lambda user_api_key_dict: True,
)
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/ui",
params={
"start_date": start_date,
"end_date": end_date,
"group_by_session": "true",
"page": cap // 7 + 1,
"page_size": 7,
},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200, response.text
data = response.json()
assert data["total"] == cap
assert [row["request_id"] for row in data["data"]] == [
f"req-sess-{index:06d}" for index in range(cap - cap % 7, cap)
]
assert data["has_more"] is True
assert data["next_session_cursor"] is not None
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_offset_for_non_starttime_sort(
client, monkeypatch