diff --git a/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py
index f4c93432943..f4bfca9e8e8 100644
--- a/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py
+++ b/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py
@@ -7178,8 +7178,38 @@ async def test_ui_view_spend_logs_grouped_exclusion_reaches_page_count_and_repre
page_rows = [
_session_page_row("sess-1", "2026-08-29 10:00:00"),
_session_page_row("sess-2", "2026-08-29 09:00:00"),
+ _session_page_row("sess-3", "2026-08-29 08:00:00"),
]
- mock_prisma = _session_grouped_mock_prisma(page_rows, 2, [_session_representative_row("req-1", "sess-1")])
+ session_aliases = {"sess-1": "noisy", "sess-2": "quiet", "sess-3": "quiet"}
+ representatives = []
+ for request_id, session_id in (("req-1", "sess-1"), ("req-2", "sess-2"), ("req-3", "sess-3")):
+ representative = _session_representative_row(request_id, session_id)
+ representative["metadata"] = {"user_api_key_alias": session_aliases[session_id]}
+ representatives.append(representative)
+
+ async def mock_query_raw(sql_query, *params):
+ excluded_alias_match = re.search(r"user_api_key_alias' IS DISTINCT FROM \$(\d+)", sql_query)
+ excluded_alias = params[int(excluded_alias_match.group(1)) - 1] if excluded_alias_match else None
+ visible_session_ids = {session_id for session_id, alias in session_aliases.items() if alias != excluded_alias}
+ if "COUNT(*) AS total_count" in sql_query:
+ return [{"total_count": len(visible_session_ids)}]
+ if "DISTINCT ON" in sql_query:
+ requested_sessions = set(params[-2])
+ requested_api_keys = set(params[-1])
+ return [
+ representative
+ for representative in representatives
+ if representative["session_id"] in visible_session_ids
+ and representative["session_id"] in requested_sessions
+ and representative["api_key"] in requested_api_keys
+ ]
+ if "COALESCE(SUM(spend)" in sql_query:
+ return []
+ return [row for row in page_rows if row["session_key"] in visible_session_ids]
+
+ mock_prisma = MagicMock()
+ mock_prisma.db = MagicMock()
+ mock_prisma.db.query_raw = AsyncMock(side_effect=mock_query_raw)
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma)
monkeypatch.setattr(
"litellm.proxy.spend_tracking.spend_management_endpoints._is_admin_view_safe",
@@ -7203,6 +7233,7 @@ async def test_ui_view_spend_logs_grouped_exclusion_reaches_page_count_and_repre
)
assert response.status_code == 200, response.text
assert response.json()["total"] == 2
+ assert [row["request_id"] for row in response.json()["data"]] == ["req-2"]
queries = [call.args for call in mock_prisma.db.query_raw.await_args_list]
assert len(queries) >= 3
for sql, *params in queries[:3]:
diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx
index 16f6ce77866..55f119baadd 100644
--- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.test.tsx
@@ -339,6 +339,40 @@ describe("RequestLogsPanel", () => {
});
});
+ it("drops the cursor and returns to the first page when the alias changes through URL navigation", async () => {
+ const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
+ vi.mocked(uiSpendLogsCall).mockImplementation(async ({ page }) => ({
+ data: firstPage,
+ total: 80,
+ page: page ?? 1,
+ page_size: 50,
+ total_pages: 2,
+ next_session_cursor: "2026-07-07 09:50:13|key-1|sess-1",
+ has_more: true,
+ }));
+ const tree = (searchParams: string) => (
+
+
+
+
+
+ );
+ const view = render(tree(""));
+
+ await waitFor(() => expect(row("req-0")).not.toBeNull());
+ fireEvent.click(screen.getByTestId("pagination-next"));
+ await waitFor(() => expect(lastCall()?.page).toBe(2));
+
+ view.rerender(tree("exclude_key_alias=noisy"));
+
+ await waitFor(() => {
+ const call = lastCall();
+ expect(call?.page).toBe(1);
+ expect(call?.params?.exclude_key_alias).toBe("noisy");
+ expect(call?.params?.session_cursor).toBeUndefined();
+ });
+ });
+
it("ignores another next click while the next page is still fetching", async () => {
const firstPage = Array.from({ length: 50 }, (_, index) => logEntry({ request_id: `req-${index}` }));
const firstResponse = {
diff --git a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx
index c25858f34c0..70988223027 100644
--- a/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx
+++ b/ui/litellm-dashboard/src/components/logs/request/RequestLogsPanel.tsx
@@ -56,6 +56,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
{ key_alias: parseAsString, exclude_key_alias: parseAsString },
{ history: "push" },
);
+ const [observedAliasParams, setObservedAliasParams] = useState({ key_alias, exclude_key_alias });
const columnFilters = useMemo(() => {
const aliasFilters: ColumnFiltersState = [];
if (exclude_key_alias) {
@@ -70,6 +71,13 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
columnFiltersRef.current = columnFilters;
}, [columnFilters]);
const [sessionCursors, setSessionCursors] = useState>({});
+ const aliasParamsChanged =
+ observedAliasParams.key_alias !== key_alias || observedAliasParams.exclude_key_alias !== exclude_key_alias;
+ const effectivePagination = useMemo(
+ () => (aliasParamsChanged ? { ...pagination, pageIndex: 0 } : pagination),
+ [aliasParamsChanged, pagination],
+ );
+ const effectiveSessionCursors = aliasParamsChanged ? {} : sessionCursors;
const [timeRange, setTimeRange] = useState(defaultLogsTimeRange);
const { startTime, endTime, isCustomDate } = timeRange;
@@ -124,10 +132,10 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
excludeInternalHealthChecks,
startTime,
endTime,
- pagination,
+ pagination: effectivePagination,
isCustomDate,
sorting,
- sessionCursors,
+ sessionCursors: effectiveSessionCursors,
});
// Follow the table's own last fetch so a live-tail refresh carries the filter
@@ -192,7 +200,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
const isDrawerOpen = displayLog !== null || displaySessionId !== null;
const rows: LogEntry[] = filteredLogs.data;
- const rowsThroughThisPage = pagination.pageIndex * pagination.pageSize + rows.length;
+ const rowsThroughThisPage = effectivePagination.pageIndex * effectivePagination.pageSize + rows.length;
const isLastPage =
filteredLogs.has_more === false || (filteredLogs.has_more === undefined && rows.length < pagination.pageSize);
const rowCount = isLastPage ? rowsThroughThisPage : Math.max(filteredLogs.total, rowsThroughThisPage);
@@ -241,24 +249,37 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
[setAliasParams],
);
- const resetToFirstPage = useCallback(() => {
- setSessionCursors({});
- setPagination((previous) => ({ ...previous, pageIndex: 0 }));
- }, []);
+ const resetToFirstPage = useCallback(
+ (nextAliasParams?: { key_alias: string | null; exclude_key_alias: string | null }) => {
+ if (nextAliasParams) {
+ setObservedAliasParams(nextAliasParams);
+ }
+ setSessionCursors({});
+ setPagination((previous) => ({ ...previous, pageIndex: 0 }));
+ },
+ [],
+ );
+
+ useEffect(() => {
+ if (!aliasParamsChanged) return;
+ // Browser history changes arrive outside the table event handlers, so local cursor state must follow the URL.
+ // eslint-disable-next-line react-hooks/set-state-in-effect
+ resetToFirstPage({ key_alias, exclude_key_alias });
+ }, [aliasParamsChanged, key_alias, exclude_key_alias, resetToFirstPage]);
const handlePaginationChange = useCallback>(
(updaterOrValue) => {
- const requested = typeof updaterOrValue === "function" ? updaterOrValue(pagination) : updaterOrValue;
+ const requested = typeof updaterOrValue === "function" ? updaterOrValue(effectivePagination) : updaterOrValue;
if (!usesSessionCursor) {
setPagination(requested);
return;
}
- if (requested.pageSize !== pagination.pageSize) {
+ if (requested.pageSize !== effectivePagination.pageSize) {
setSessionCursors({});
setPagination({ ...requested, pageIndex: 0 });
return;
}
- if (requested.pageIndex !== pagination.pageIndex + 1) {
+ if (requested.pageIndex !== effectivePagination.pageIndex + 1) {
setPagination(requested);
return;
}
@@ -267,7 +288,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
setSessionCursors((previous) => ({ ...previous, [requested.pageIndex]: nextCursor }));
setPagination(requested);
},
- [usesSessionCursor, pagination, filteredLogs.next_session_cursor, logsQuery.isPlaceholderData],
+ [usesSessionCursor, effectivePagination, filteredLogs.next_session_cursor, logsQuery.isPlaceholderData],
);
const handleExcludeInternalHealthChecksChange = useCallback(
@@ -345,7 +366,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
rowCount={rowCount}
isLoading={logsQuery.isLoading}
isRefreshing={logsQuery.isFetching}
- pagination={pagination}
+ pagination={effectivePagination}
onPaginationChange={handlePaginationChange}
sorting={sorting}
onSortingChange={handleSortingChange}
@@ -363,7 +384,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
- {isLiveTail && pagination.pageIndex === 0 && (
+ {isLiveTail && effectivePagination.pageIndex === 0 && (
Refreshing every 15s