Merge pull request #40644 from BerriAI/litellm_logs_last_page_jump

fix(ui): jump straight to the last Request Logs page instead of advancing one page
This commit is contained in:
ryan-crabbe-berri 2026-09-11 16:17:56 -07:00 committed by GitHub
commit d70e64d973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 322 additions and 33 deletions

View file

@ -2925,6 +2925,32 @@ async def _fetch_session_representatives(
return [rep_by_key[key] for key in session_keys if key in rep_by_key] # mutable-ok: rows are enriched in place
async def _count_grouped_sessions(
prisma_client: "PrismaClient",
where_clause: str,
sql_params: Sequence[object],
next_param_index: int,
) -> tuple[int, bool]:
"""Count the sessions matching the filter, returning ``(total, total_is_capped)`` bounded by the count cap."""
count_query: Final = f"""
SELECT COUNT(*) AS total_count
FROM (
SELECT 1
FROM "LiteLLM_SpendLogs"
WHERE {where_clause}
GROUP BY {_SESSION_GROUP_KEY_SQL}
LIMIT ${next_param_index}
) AS bounded_sessions
"""
count_rows: Final[Sequence[_SpendLogsCountRow]] = await _query_raw(
prisma_client, count_query, *sql_params, SPEND_LOGS_PAGINATION_COUNT_CAP + 1
)
raw_total: Final = int(count_rows[0]["total_count"]) if count_rows else 0
return (
(SPEND_LOGS_PAGINATION_COUNT_CAP, True) if raw_total > SPEND_LOGS_PAGINATION_COUNT_CAP else (raw_total, False)
)
async def _ui_session_grouped_spend_logs(
prisma_client: "PrismaClient",
sql_conditions: Sequence[str],
@ -2944,11 +2970,19 @@ async def _ui_session_grouped_spend_logs(
next ``page_size`` sessions ordered by ``(MAX(startTime), session_key,
api_key)``, resumed from the ``session_cursor`` keyset
``'<last_activity>|<api_key>|<session_key>'`` instead of an OFFSET, so
page depth does not degrade the query plan. Each session is represented
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``, 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
(capped like the flat total).
(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 ">"
@ -2963,6 +2997,10 @@ 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
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"""
SELECT {_SESSION_KEY_EXPR} AS session_key,
@ -2973,36 +3011,29 @@ async def _ui_session_grouped_spend_logs(
GROUP BY {_SESSION_GROUP_KEY_SQL}
{having_clause}
ORDER BY MAX("startTime") {direction}, {_SESSION_KEY_EXPR} {direction}, api_key {direction}
LIMIT ${limit_index}
LIMIT ${limit_index} {offset_clause}
"""
page_rows: Final[Sequence[_SessionPageRow]] = await _query_raw(
prisma_client, page_query, *sql_params, *cursor_params, page_size + 1
page_rows: Final[Sequence[_SessionPageRow]] = (
()
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
else None
)
count_query: Final = f"""
SELECT COUNT(*) AS total_count
FROM (
SELECT 1
FROM "LiteLLM_SpendLogs"
WHERE {where_clause}
GROUP BY {_SESSION_GROUP_KEY_SQL}
LIMIT ${next_param_index}
) AS bounded_sessions
"""
count_rows: Final[Sequence[_SpendLogsCountRow]] = await _query_raw(
prisma_client, count_query, *sql_params, SPEND_LOGS_PAGINATION_COUNT_CAP + 1
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
else await _count_grouped_sessions(prisma_client, where_clause, sql_params, next_param_index)
)
raw_total: Final = int(count_rows[0]["total_count"]) if count_rows else 0
total_is_capped: Final = raw_total > SPEND_LOGS_PAGINATION_COUNT_CAP
total_records: Final = SPEND_LOGS_PAGINATION_COUNT_CAP if total_is_capped else raw_total
session_keys: Final = tuple((row["session_key"], row["api_key"]) for row in visible_rows)
data: Final[list[dict[str, object]]] = ( # mutable-ok: _build_ui_spend_logs_response writes onto each row

View file

@ -6629,6 +6629,30 @@ def _session_page_row(session_key, last_activity):
return {"session_key": session_key, "api_key": "hashed-key", "last_activity": last_activity}
def _session_grouped_paginating_prisma(sessions, counted_total=None):
"""Mock prisma serving the grouped page query out of ``sessions``, honoring the LIMIT and OFFSET it asks for."""
async def mock_query_raw(sql_query, *params):
if "COUNT(*) AS total_count" in sql_query:
return [{"total_count": min(len(sessions) if counted_total is None else counted_total, params[-1])}]
if "DISTINCT ON" in sql_query:
return [_session_representative_row(f"req-{session_key}", session_key) for session_key in params[-2]]
if "COALESCE(SUM(spend)" in sql_query:
return []
bounds = re.search(r"LIMIT \$(\d+)(?: OFFSET \$(\d+))?", sql_query)
limit = params[int(bounds.group(1)) - 1]
offset = params[int(bounds.group(2)) - 1] if bounds.group(2) else 0
return [
_session_page_row(session_key, last_activity)
for session_key, last_activity in sessions[offset : offset + limit]
]
mock_prisma = MagicMock()
mock_prisma.db = MagicMock()
mock_prisma.db.query_raw = AsyncMock(side_effect=mock_query_raw)
return mock_prisma
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_first_page(client, monkeypatch):
"""One row per (session, api_key), session-count total, and a keyset cursor for the next page."""
@ -6742,6 +6766,203 @@ async def test_ui_view_spend_logs_group_by_session_cursor_page(client, monkeypat
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_jumps_to_page_without_cursor(client, monkeypatch):
"""page > 1 with no session_cursor (the UI's last-page jump) serves the sessions that page starts at."""
sessions = tuple((f"sess-{index:02d}", f"2026-08-29 10:{59 - index:02d}:00") for index in range(60))
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": 3,
"page_size": 25,
},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200, response.text
data = response.json()
assert data["page"] == 3
assert data["total"] == 60
assert data["has_more"] is False
assert data["next_session_cursor"] is None
assert [row["request_id"] for row in data["data"]] == [f"req-sess-{index:02d}" for index in range(50, 60)]
finally:
app.dependency_overrides.pop(ps.user_api_key_auth, None)
@pytest.mark.asyncio
async def test_ui_view_spend_logs_group_by_session_short_page_totals_itself(client, monkeypatch):
"""A page that runs out of sessions is the end of the list, so the total comes from it and nothing is counted."""
sessions = tuple((f"sess-{index:02d}", f"2026-08-29 10:{59 - index:02d}:00") for index in range(10))
mock_prisma = _session_grouped_paginating_prisma(sessions, counted_total=999)
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": 1,
"page_size": 25,
},
headers={"Authorization": "Bearer sk-test"},
)
assert response.status_code == 200, response.text
data = response.json()
assert data["total"] == 10, "the count query's 999 would have won if it had been asked"
assert data["total_is_capped"] is False
assert data["total_pages"] == 1
assert len(data["data"]) == 10
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_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."""
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()
params = {
"start_date": start_date,
"end_date": end_date,
"group_by_session": "true",
"page_size": 25,
}
last_page = client.get(
"/spend/logs/ui",
params={**params, "page": cap // 25},
headers={"Authorization": "Bearer sk-test"},
)
assert last_page.status_code == 200, last_page.text
last_page_data = last_page.json()
assert last_page_data["total"] == cap
assert last_page_data["total_is_capped"] is True
assert last_page_data["data"][0]["request_id"] == f"req-sess-{cap - 25:06d}"
assert len(last_page_data["data"]) == 25
mock_prisma.db.query_raw.reset_mock()
past_cap = client.get(
"/spend/logs/ui",
params={**params, "page": cap // 25 + 1},
headers={"Authorization": "Bearer sk-test"},
)
assert past_cap.status_code == 200, past_cap.text
past_cap_data = past_cap.json()
assert past_cap_data["data"] == []
assert past_cap_data["has_more"] is False
assert past_cap_data["total"] == cap
assert mock_prisma.db.query_raw.await_count == 1, "only the bounded count query runs past the capped window"
finally:
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

View file

@ -528,8 +528,8 @@ async def test_spend_logs_ui_group_by_session_paginates_sessions(monkeypatch):
group_key = "COALESCE(NULLIF(session_id, ''), request_id), api_key"
session_rows = [
{"session_key": "req-1", "api_key": "k", "last_activity": "2026-02-16 10:00:00"},
{"session_key": "req-2", "api_key": "k", "last_activity": "2026-02-16 09:00:00"},
{"session_key": f"req-{index}", "api_key": "k", "last_activity": f"2026-02-16 10:{59 - index:02d}:00"}
for index in range(51)
]
representative_rows = [
{"request_id": "req-1", "api_key": "k", "metadata": "{}", "session_id": None},
@ -538,7 +538,7 @@ async def test_spend_logs_ui_group_by_session_paginates_sessions(monkeypatch):
async def mock_query_raw(sql_query, *params):
if "COUNT(*) AS total_count" in sql_query:
return [{"total_count": 12}]
return [{"total_count": 60}]
if "DISTINCT ON" in sql_query:
return representative_rows
return session_rows
@ -590,11 +590,11 @@ async def test_spend_logs_ui_group_by_session_paginates_sessions(monkeypatch):
assert "COUNT(*) OVER ()" not in rep_sql
assert [row["request_id"] for row in response["data"]] == ["req-1", "req-2"]
assert response["total"] == 12
assert response["total"] == 60
assert response["total_is_capped"] is False
assert response["total_pages"] == 1
assert response["has_more"] is False
assert response["next_session_cursor"] is None
assert response["total_pages"] == 2
assert response["has_more"] is True
assert response["next_session_cursor"] == "2026-02-16 10:10:00|k|req-49"
@pytest.mark.asyncio

View file

@ -258,6 +258,44 @@ describe("RequestLogsPanel", () => {
});
});
it("jumps straight to the last page without a cursor when the last-page button is clicked", async () => {
const firstPage = Array.from({ length: 25 }, (_, index) => logEntry({ request_id: `req-${index}` }));
const lastPage = Array.from({ length: 10 }, (_, index) => logEntry({ request_id: `req-last-${index}` }));
vi.mocked(uiSpendLogsCall).mockImplementation(async ({ page }) =>
page === 3
? {
data: lastPage,
total: 60,
page: 3,
page_size: 25,
total_pages: 3,
next_session_cursor: null,
has_more: false,
}
: {
data: firstPage,
total: 60,
page: 1,
page_size: 25,
total_pages: 3,
next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
has_more: true,
},
);
renderPanel();
await waitFor(() => expect(row("req-0")).not.toBeNull());
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 1 of 3");
fireEvent.click(screen.getByTestId("pagination-last"));
await waitFor(() => expect(row("req-last-0")).not.toBeNull());
expect(lastCall()?.page).toBe(3);
expect(lastCall()?.params?.session_cursor).toBeUndefined();
expect(screen.getByTestId("pagination-page")).toHaveTextContent("Page 3 of 3");
expect(screen.getByTestId("pagination-range")).toHaveTextContent("Showing 51-60 of 60");
expect(vi.mocked(uiSpendLogsCall).mock.calls.filter(([options]) => options.page === 2)).toHaveLength(0);
});
it("drops the cursor and returns to the first page when a filter changes", async () => {
const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
vi.mocked(uiSpendLogsCall).mockResolvedValue({

View file

@ -209,15 +209,14 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
setPagination({ ...requested, pageIndex: 0 });
return;
}
if (requested.pageIndex <= pagination.pageIndex) {
if (requested.pageIndex !== pagination.pageIndex + 1) {
setPagination(requested);
return;
}
const nextCursor = filteredLogs.next_session_cursor;
if (!nextCursor || logsQuery.isPlaceholderData) return;
const nextPageIndex = pagination.pageIndex + 1;
setSessionCursors((previous) => ({ ...previous, [nextPageIndex]: nextCursor }));
setPagination({ ...requested, pageIndex: nextPageIndex });
setSessionCursors((previous) => ({ ...previous, [requested.pageIndex]: nextCursor }));
setPagination(requested);
},
[usesSessionCursor, pagination, filteredLogs.next_session_cursor, logsQuery.isPlaceholderData],
);