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} />