From 1277cbe454d1df41cd9ca0e94fab518a3e3124c1 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 29 Apr 2026 15:50:55 -0700 Subject: [PATCH 1/4] Add health status pagination controls Made-with: Cursor --- .../ModelsAndEndpointsView.tsx | 39 +++++- .../HealthCheckComponent.test.tsx | 53 ++++++- .../model_dashboard/HealthCheckComponent.tsx | 131 +++++++++++++----- 3 files changed, 174 insertions(+), 49 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 514ae673d0..12d90c40f9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -62,6 +62,8 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te const [selectedModelId, setSelectedModelId] = useState(null); const [selectedTeamId, setSelectedTeamId] = useState(null); const [selectedTabIndex, setSelectedTabIndex] = useState(0); + const [healthCurrentPage, setHealthCurrentPage] = useState(1); + const healthPageSize = 50; const [showMissingProviderBanner, setShowMissingProviderBanner] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("hideMissingProviderBanner") !== "true"; @@ -71,6 +73,10 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te const queryClient = useQueryClient(); const { data: modelDataResponse, isLoading: isLoadingModels, refetch: refetchModels } = useModelsInfo(); + const { data: healthModelDataResponse, isLoading: isLoadingHealthModels } = useModelsInfo( + healthCurrentPage, + healthPageSize, + ); const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap(); const { data: credentialsResponse, isLoading: isLoadingCredentials } = useCredentials(); const credentialsList = credentialsResponse?.credentials || []; @@ -104,12 +110,12 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te return modelDataResponse.data.map((model: any) => model.model_name); }, [modelDataResponse?.data]); - const allModelIdsOnProxy = useMemo(() => { - if (!modelDataResponse?.data) return []; - return modelDataResponse.data + const healthModelIdsOnProxy = useMemo(() => { + if (!healthModelDataResponse?.data) return []; + return healthModelDataResponse.data .map((model: any) => model.model_info?.id) .filter((id: string | undefined): id is string => Boolean(id)); - }, [modelDataResponse?.data]); + }, [healthModelDataResponse?.data]); const getProviderFromModel = (model: string) => { if (modelCostMapData !== null && modelCostMapData !== undefined) { @@ -125,6 +131,20 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te return transformModelData(modelDataResponse, getProviderFromModel); }, [modelDataResponse?.data, getProviderFromModel]); + const processedHealthModelData = useMemo(() => { + if (!healthModelDataResponse?.data) return { data: [] }; + return transformModelData(healthModelDataResponse, getProviderFromModel); + }, [healthModelDataResponse?.data, getProviderFromModel]); + + const healthPaginationMeta = useMemo(() => { + return { + total_count: healthModelDataResponse?.total_count ?? 0, + current_page: healthModelDataResponse?.current_page ?? healthCurrentPage, + total_pages: healthModelDataResponse?.total_pages ?? 1, + size: healthModelDataResponse?.size ?? healthPageSize, + }; + }, [healthModelDataResponse, healthCurrentPage, healthPageSize]); + const isProxyAdmin = userRole && isProxyAdminRole(userRole); const isInternalUser = userRole && internalUserRoles.includes(userRole); const isUserTeamAdmin = userID && isUserTeamAdminForAnyTeam(teams, userID); @@ -166,7 +186,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te const handleRefreshClick = () => { const currentDate = new Date(); - setLastRefreshed(currentDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + setLastRefreshed(currentDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); queryClient.invalidateQueries({ queryKey: ["models", "list"] }); refetchModels(); }; @@ -441,11 +461,16 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te { expect(mockIndividualModelHealthCheckCall).not.toHaveBeenCalledWith("token-123", "gpt-4"); }); + it("should show pagination controls and request the next page", async () => { + const onPageChange = vi.fn(); + const modelData = { + data: [ + { + model_name: "gpt-4", + model_info: { id: "deployment-1" }, + litellm_model_name: "gpt-4", + }, + ], + }; + + render( + , + ); + + expect(screen.getByTestId("health-results-count")).toHaveTextContent("Showing 1 - 50 of 75 results"); + + await act(async () => { + screen.getByRole("button", { name: "Next" }).click(); + }); + + expect(onPageChange).toHaveBeenCalledWith(2); + }); + describe("latest_health_checks keyed by model id", () => { it("should show status from latest_health_checks when keys match model ids", async () => { const modelData = { data: [ - { - model_name: "gpt-4", - model_info: { id: "id-alpha" }, - litellm_model_name: "gpt-4", + { + model_name: "gpt-4", + model_info: { id: "id-alpha" }, + litellm_model_name: "gpt-4", }, - { - model_name: "gpt-4", - model_info: { id: "id-beta" }, + { + model_name: "gpt-4", + model_info: { id: "id-beta" }, litellm_model_name: "gpt-4", }, ], diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx index db71733b5b..c652e9a02b 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx @@ -26,6 +26,16 @@ interface HealthCheckComponentProps { getDisplayModelName: (model: any) => string; setSelectedModelId?: (modelId: string) => void; teams?: Team[] | null; + isLoading?: boolean; + paginationMeta?: { + total_count: number; + current_page: number; + total_pages: number; + size: number; + }; + currentPage?: number; + pageSize?: number; + onPageChange?: (page: number) => void; } const HealthCheckComponent: React.FC = ({ @@ -35,6 +45,11 @@ const HealthCheckComponent: React.FC = ({ getDisplayModelName, setSelectedModelId, teams, + isLoading = false, + paginationMeta, + currentPage = 1, + pageSize = 50, + onPageChange, }) => { const [modelHealthStatuses, setModelHealthStatuses] = useState<{ [key: string]: HealthStatus }>({}); const [selectedModelsForHealth, setSelectedModelsForHealth] = useState([]); @@ -95,19 +110,19 @@ const HealthCheckComponent: React.FC = ({ const fullError = checkData.error_message || undefined; healthStatusMap[modelId] = { - status: checkData.status || "unknown", - lastCheck: checkData.checked_at ? new Date(checkData.checked_at).toLocaleString() : "None", - lastSuccess: - checkData.status === "healthy" - ? checkData.checked_at - ? new Date(checkData.checked_at).toLocaleString() - : "None" - : "None", - loading: false, - error: fullError ? extractMeaningfulError(fullError) : undefined, - fullError: fullError, - successResponse: checkData.status === "healthy" ? checkData : undefined, - }; + status: checkData.status || "unknown", + lastCheck: checkData.checked_at ? new Date(checkData.checked_at).toLocaleString() : "None", + lastSuccess: + checkData.status === "healthy" + ? checkData.checked_at + ? new Date(checkData.checked_at).toLocaleString() + : "None" + : "None", + loading: false, + error: fullError ? extractMeaningfulError(fullError) : undefined, + fullError: fullError, + successResponse: checkData.status === "healthy" ? checkData : undefined, + }; }); } } catch (healthError) { @@ -448,6 +463,12 @@ const HealthCheckComponent: React.FC = ({ } }; + const handlePageChange = (page: number) => { + setSelectedModelsForHealth([]); + setAllModelsSelected(false); + onPageChange?.(page); + }; + const getStatusBadge = (status: string) => { switch (status) { case "healthy": @@ -490,6 +511,34 @@ const HealthCheckComponent: React.FC = ({ setSelectedSuccessDetails(null); }; + const healthTableData = (modelData?.data ?? []).map((model: any) => { + const modelId = model.model_info?.id; + const healthStatus = modelId ? modelHealthStatuses[modelId] : null; + const status = healthStatus || { + status: "none", + lastCheck: "None", + loading: false, + }; + return { + model_name: model.model_name, + model_info: model.model_info, + provider: model.provider, + litellm_model_name: model.litellm_model_name, + health_status: status.status, + last_check: status.lastCheck, + last_success: status.lastSuccess || "None", + health_loading: status.loading, + health_error: status.error, + health_full_error: status.fullError, + }; + }); + + const totalCount = paginationMeta?.total_count ?? healthTableData.length; + const totalPages = paginationMeta?.total_pages ?? 1; + const resultsStart = totalCount > 0 ? (currentPage - 1) * pageSize + 1 : 0; + const resultsEnd = Math.min(currentPage * pageSize, totalCount); + const shouldShowPagination = Boolean(paginationMeta && onPageChange); + return (
@@ -522,6 +571,38 @@ const HealthCheckComponent: React.FC = ({
+ {shouldShowPagination && ( +
+ + {totalCount > 0 + ? `Showing ${resultsStart} - ${resultsEnd} of ${totalCount} results` + : "Showing 0 results"} + + +
+ + +
+
+ )} = ({ setSelectedModelId, teams, )} - data={modelData.data.map((model: any) => { - const modelId = model.model_info?.id; - const healthStatus = modelId ? modelHealthStatuses[modelId] : null; - const status = healthStatus || { - status: "none", - lastCheck: "None", - loading: false, - }; - return { - model_name: model.model_name, - model_info: model.model_info, - provider: model.provider, - litellm_model_name: model.litellm_model_name, - health_status: status.status, - last_check: status.lastCheck, - last_success: status.lastSuccess || "None", - health_loading: status.loading, - health_error: status.error, - health_full_error: status.fullError, - }; - })} - isLoading={false} + data={healthTableData} + isLoading={isLoading} />
From 0b9d06a50948e4aac9337f714aca715120d47cdb Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 29 Apr 2026 16:25:57 -0700 Subject: [PATCH 2/4] Fix model e2e result count locator Made-with: Cursor --- .../e2e_tests/tests/modelsPage/addModel.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts index 8834724f76..0f11bb5b8d 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts @@ -141,7 +141,7 @@ test.describe("Add Model", () => { await page.waitForTimeout(1000); // Verify the model appears in the results count (not "Showing 0 results") - await expect(page.getByText(/Showing \d+ - \d+ of \d+ results/)).toBeVisible({ timeout: 15_000 }); + await expect(page.getByTestId("models-results-count")).toBeVisible({ timeout: 15_000 }); // Verify the model name appears in the table body const tableBody = page.locator("table tbody"); @@ -181,7 +181,7 @@ test.describe("Add Model", () => { await page.waitForTimeout(1000); // Verify the model appears in the results count (not "Showing 0 results") - await expect(page.getByText(/Showing \d+ - \d+ of \d+ results/)).toBeVisible({ timeout: 15_000 }); + await expect(page.getByTestId("models-results-count")).toBeVisible({ timeout: 15_000 }); // Verify the wildcard model appears in the table body (wildcard models show as "cohere/*") const tableBody = page.locator("table tbody"); From a7a3f0a19d69b1744b9b5c7c2a6b86b0339f8f24 Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 29 Apr 2026 16:49:25 -0700 Subject: [PATCH 3/4] Address health pagination review feedback Made-with: Cursor --- .../e2e_tests/tests/modelsPage/addModel.spec.ts | 8 ++++++-- .../models-and-endpoints/ModelsAndEndpointsView.tsx | 11 ++++++----- .../model_dashboard/HealthCheckComponent.tsx | 11 +++++++---- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts b/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts index 0f11bb5b8d..c3bd848902 100644 --- a/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts +++ b/ui/litellm-dashboard/e2e_tests/tests/modelsPage/addModel.spec.ts @@ -141,7 +141,9 @@ test.describe("Add Model", () => { await page.waitForTimeout(1000); // Verify the model appears in the results count (not "Showing 0 results") - await expect(page.getByTestId("models-results-count")).toBeVisible({ timeout: 15_000 }); + await expect(page.getByTestId("models-results-count")).toHaveText(/Showing \d+ - \d+ of \d+ results/, { + timeout: 15_000, + }); // Verify the model name appears in the table body const tableBody = page.locator("table tbody"); @@ -181,7 +183,9 @@ test.describe("Add Model", () => { await page.waitForTimeout(1000); // Verify the model appears in the results count (not "Showing 0 results") - await expect(page.getByTestId("models-results-count")).toBeVisible({ timeout: 15_000 }); + await expect(page.getByTestId("models-results-count")).toHaveText(/Showing \d+ - \d+ of \d+ results/, { + timeout: 15_000, + }); // Verify the wildcard model appears in the table body (wildcard models show as "cohere/*") const tableBody = page.locator("table tbody"); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index 12d90c40f9..c51df7f814 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -46,6 +46,8 @@ interface GlobalRetryPolicyObject { [retryPolicyKey: string]: number; } +const HEALTH_PAGE_SIZE = 50; + const ModelsAndEndpointsView: React.FC = ({ premiumUser, teams }) => { const { accessToken, token, userRole, userId: userID } = useAuthorized(); const [addModelForm] = Form.useForm(); @@ -63,7 +65,6 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te const [selectedTeamId, setSelectedTeamId] = useState(null); const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [healthCurrentPage, setHealthCurrentPage] = useState(1); - const healthPageSize = 50; const [showMissingProviderBanner, setShowMissingProviderBanner] = useState(() => { if (typeof window !== "undefined") { return localStorage.getItem("hideMissingProviderBanner") !== "true"; @@ -75,7 +76,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te const { data: modelDataResponse, isLoading: isLoadingModels, refetch: refetchModels } = useModelsInfo(); const { data: healthModelDataResponse, isLoading: isLoadingHealthModels } = useModelsInfo( healthCurrentPage, - healthPageSize, + HEALTH_PAGE_SIZE, ); const { data: modelCostMapData, isLoading: isLoadingModelCostMap } = useModelCostMap(); const { data: credentialsResponse, isLoading: isLoadingCredentials } = useCredentials(); @@ -141,9 +142,9 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te total_count: healthModelDataResponse?.total_count ?? 0, current_page: healthModelDataResponse?.current_page ?? healthCurrentPage, total_pages: healthModelDataResponse?.total_pages ?? 1, - size: healthModelDataResponse?.size ?? healthPageSize, + size: healthModelDataResponse?.size ?? HEALTH_PAGE_SIZE, }; - }, [healthModelDataResponse, healthCurrentPage, healthPageSize]); + }, [healthModelDataResponse, healthCurrentPage]); const isProxyAdmin = userRole && isProxyAdminRole(userRole); const isInternalUser = userRole && internalUserRoles.includes(userRole); @@ -469,7 +470,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te isLoading={isLoadingHealthModels} paginationMeta={healthPaginationMeta} currentPage={healthCurrentPage} - pageSize={healthPageSize} + pageSize={HEALTH_PAGE_SIZE} onPageChange={setHealthCurrentPage} /> diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx index c652e9a02b..d9b7fdd149 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.tsx @@ -466,6 +466,7 @@ const HealthCheckComponent: React.FC = ({ const handlePageChange = (page: number) => { setSelectedModelsForHealth([]); setAllModelsSelected(false); + setModelHealthStatuses({}); onPageChange?.(page); }; @@ -533,11 +534,13 @@ const HealthCheckComponent: React.FC = ({ }; }); - const totalCount = paginationMeta?.total_count ?? healthTableData.length; - const totalPages = paginationMeta?.total_pages ?? 1; - const resultsStart = totalCount > 0 ? (currentPage - 1) * pageSize + 1 : 0; - const resultsEnd = Math.min(currentPage * pageSize, totalCount); const shouldShowPagination = Boolean(paginationMeta && onPageChange); + const totalCount = paginationMeta?.total_count ?? 0; + const totalPages = paginationMeta?.total_pages ?? 1; + const pageForDisplay = paginationMeta?.current_page ?? currentPage; + const pageSizeForDisplay = paginationMeta?.size ?? pageSize; + const resultsStart = shouldShowPagination && totalCount > 0 ? (pageForDisplay - 1) * pageSizeForDisplay + 1 : 0; + const resultsEnd = shouldShowPagination ? Math.min(pageForDisplay * pageSizeForDisplay, totalCount) : 0; return (
From 181e99b9962ae3998ac91338b45bfbcdad6be60c Mon Sep 17 00:00:00 2001 From: shivam Date: Wed, 29 Apr 2026 17:16:32 -0700 Subject: [PATCH 4/4] Fix health pagination review issues Made-with: Cursor --- .../ModelsAndEndpointsView.tsx | 1 + .../HealthCheckComponent.test.tsx | 39 +++++++++++-------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx index c51df7f814..7c162e2056 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/ModelsAndEndpointsView.tsx @@ -188,6 +188,7 @@ const ModelsAndEndpointsView: React.FC = ({ premiumUser, te const handleRefreshClick = () => { const currentDate = new Date(); setLastRefreshed(currentDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); + setHealthCurrentPage(1); queryClient.invalidateQueries({ queryKey: ["models", "list"] }); refetchModels(); }; diff --git a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx index 738dab9d54..1a4c0ed9ff 100644 --- a/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx +++ b/ui/litellm-dashboard/src/components/model_dashboard/HealthCheckComponent.test.tsx @@ -104,23 +104,28 @@ describe("HealthCheckComponent", () => { ], }; - render( - , - ); + await act(async () => { + render( + , + ); + }); + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); expect(screen.getByTestId("health-results-count")).toHaveTextContent("Showing 1 - 50 of 75 results");