From 51e67205d77beaceb6d88e066a012dd821d2a12a Mon Sep 17 00:00:00 2001 From: lzhan011 <35493221+lzhan011@users.noreply.github.com> Date: Mon, 5 Oct 2026 05:07:52 -0500 Subject: [PATCH] fix(ui): honor model view scope Two bugs in the models tab, both still present on `main`: 1. `teamIdForQuery` was derived from the team picker alone, so "All Proxy Models" still sent `teamId` and the tab showed the current team's models under a heading that promises every model the user can reach. 2. `handleViewModeChange` was the only handler that changes the result set without passing `page: null`. Switching view while past page 1 kept the old page number against a different, usually shorter, list -- so the user saw an empty table or skipped earlier rows until they paged back manually. Rebuilt on `main`: this PR was opened against the since-deleted `litellm_internal_staging`. `main` has since moved this component onto `useQueryStates`/`setTableState`, so the pagination fix is now one `page: null` in the same shape as its siblings (`handleTeamChange`, the search handler, the access-group handler all already pass it) rather than the `resetToFirstPage()` call the original patch used. The test is rewritten against `main`'s `renderWithProviders` + `onUrlUpdate` idiom, and the view label is "All Proxy Models" here rather than "All Available Models". NOT VERIFIED LOCALLY: the dashboard's deps will not install in this environment -- `npm ci` refuses with `Required: {"node":">=24.14.1"}` against node v22.22.3 -- so `vitest` could not be run on the new case. The source change is four lines and matches the surrounding idiom; CI's own run is the real check here, and I will fix whatever it reports. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_016SxQ2jq4B4KLGwPoLgfHye --- .../components/AllModelsTab.test.tsx | 33 +++++++++++++++++++ .../components/AllModelsTab.tsx | 11 +++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx index 0c93bb234a5..b105349c867 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx @@ -250,6 +250,39 @@ describe("AllModelsTab", () => { expect(lastModelsInfoCall().page).toBe(1); }); + it("drops the team filter and the page when switching to all proxy models", async () => { + // "All Proxy Models" promises every model the user can reach, so the team + // must stop narrowing the query -- and the page has to go back to 1 like it + // does for every other control that changes the result set. + const user = userEvent.setup(); + setModelsInfo([makeRow()], 200); + const onUrlUpdate = vi.fn(); + renderWithProviders(, { + searchParams: { page: "3", filter_team: "team-1" }, + onUrlUpdate, + }); + + await waitFor(() => { + expect(lastModelsInfoCall().teamId).toBe("team-1"); + }); + expect(lastModelsInfoCall().page).toBe(3); + + await user.click(screen.getByTestId("models-view-select")); + await user.click(await screen.findByRole("option", { name: "All Proxy Models" })); + + await waitFor(() => { + expect(lastModelsInfoCall().teamId).toBeUndefined(); + }); + expect(lastUrlParams(onUrlUpdate)?.get("page")).toBeNull(); + + await user.click(screen.getByTestId("models-view-select")); + await user.click(await screen.findByRole("option", { name: "Current Team Models" })); + + await waitFor(() => { + expect(lastModelsInfoCall().teamId).toBe("team-1"); + }); + }); + it("debounces the model name search into the server query", async () => { renderWithProviders(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx index 2217bca0fa0..dd68052a9e2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx @@ -101,7 +101,11 @@ const AllModelsTab = ({ const [deleteLoading, setDeleteLoading] = useState(false); const [pausingModelId, setPausingModelId] = useState(null); - const teamIdForQuery = selectedTeamValue === PERSONAL_TEAM_VALUE ? undefined : selectedTeamValue; + // Scoped by the view mode, not just by the team picker: in "all available + // models" the team must not narrow the query, or the tab shows the current + // team's models under a heading that promises every model the user can reach. + const teamIdForQuery = + modelViewMode === "current_team" && selectedTeamValue !== PERSONAL_TEAM_VALUE ? selectedTeamValue : undefined; const isConcreteModelGroup = Boolean(selectedModelGroup) && selectedModelGroup !== ALL_MODEL_GROUPS_VALUE && @@ -210,7 +214,10 @@ const AllModelsTab = ({ }; const handleViewModeChange = (value: ModelViewMode) => { - void setTableState({ view_mode: value }); + // `page: null` like every other handler that changes the result set -- + // without it, switching view while past page 1 keeps the old page number + // against a different, usually shorter, list. + void setTableState({ view_mode: value, page: null }); }; const resetFilters = () => {