fix(proxy): count when a grouped log page starts past the last one

An out-of-range cursorless page returns nothing, and reading its total off the
offset reported more sessions than exist (page 4 of 100 sessions at page size 50
claimed 150). Only a page that holds rows, or the first page, ends the list;
anything past it falls back to the bounded count.

Claude-Session: https://claude.ai/code/session_01ESi9JwaXDww1vP3Qsrr4Mz
This commit is contained in:
ryan-crabbe-berri 2026-09-11 14:59:18 -07:00
parent 4bcd60e72b
commit 06fe2691c3
2 changed files with 41 additions and 4 deletions

View file

@ -2979,9 +2979,10 @@ async def _ui_session_grouped_spend_logs(
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
(capped like the flat total). A page that runs out of sessions is itself
the end of the list, so its ``total`` is ``offset + len(page)`` and the
grouped count query is skipped.
(capped like the flat total). A page that runs out of sessions while still
holding some is itself the end of the list, so its ``total`` is
``offset + len(page)`` and the grouped count query is skipped; a page that
starts past the end says nothing about the total, so that one is counted.
"""
where_clause: Final = " AND ".join(sql_conditions) if sql_conditions else "TRUE"
cmp_op: Final = "<" if sort_desc else ">"
@ -3026,7 +3027,8 @@ async def _ui_session_grouped_spend_logs(
else None
)
page_ends_the_list: Final = cursor is None and page_limit > 0 and not has_more
page_starts_inside_the_list: Final = offset == 0 or len(page_rows) > 0
page_ends_the_list: Final = cursor is None and page_limit > 0 and not has_more and page_starts_inside_the_list
total_records, total_is_capped = (
(offset + len(page_rows), False)
if page_ends_the_list

View file

@ -6839,6 +6839,41 @@ async def test_ui_view_spend_logs_group_by_session_short_page_totals_itself(clie
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_page_past_the_end_keeps_the_real_total(client, monkeypatch):
"""An empty page past the last one says nothing about the total, so it is counted rather than inferred."""
sessions = tuple((f"sess-{index:02d}", f"2026-08-29 10:{59 - index:02d}:00") for index in range(100))
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": 4,
"page_size": 50,
},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200, response.text
data = response.json()
assert data["data"] == []
assert data["total"] == 100, "the empty page's offset is not a total"
assert data["total_pages"] == 2
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_page_past_count_cap_is_empty(client, monkeypatch):
"""The last page inside the capped total still lists sessions; the page after it is empty and costs no query."""