fix(ui): jump straight to the last Request Logs page instead of advancing one page

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
ryan 2026-09-11 01:08:20 +00:00
parent 8a4fae0e17
commit 857b9ad7d3
4 changed files with 91 additions and 7 deletions

View file

@ -2944,7 +2944,10 @@ 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``, bounded by the capped total. 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
@ -2963,6 +2966,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_params: Final[tuple[int, ...]] = ((page - 1) * page_size,) if cursor is None and page > 1 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,10 +2978,10 @@ 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
prisma_client, page_query, *sql_params, *cursor_params, page_size + 1, *offset_params
)
has_more: Final = len(page_rows) > page_size

View file

@ -6742,6 +6742,48 @@ 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) skips (page - 1) * page_size sessions by OFFSET."""
page_rows = [_session_page_row("sess-3", "2026-08-29 06:00:00")]
reps = [_session_representative_row("req-3", "sess-3")]
mock_prisma = _session_grouped_mock_prisma(page_rows, 60, reps)
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["has_more"] is False
assert [row["request_id"] for row in data["data"]] == ["req-3"]
page_query_call = mock_prisma.db.query_raw.await_args_list[0]
page_query_sql = page_query_call.args[0]
assert "HAVING" not in page_query_sql
assert "OFFSET" in page_query_sql
assert page_query_call.args[-2:] == (26, 50), "LIMIT page_size + 1 then OFFSET (page - 1) * page_size"
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

@ -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],
);