diff --git a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx index 5b669e5d63e..8e874989e4b 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest"; -import { render } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import PublicModelHub from "./public_model_hub"; vi.mock("next/navigation", () => ({ @@ -38,10 +38,10 @@ beforeAll(() => { matches: false, media: query, onchange: null, - addListener: () => {}, - removeListener: () => {}, - addEventListener: () => {}, - removeEventListener: () => {}, + addListener: () => { }, + removeListener: () => { }, + addEventListener: () => { }, + removeEventListener: () => { }, dispatchEvent: () => false, }), }); @@ -64,4 +64,111 @@ describe("PublicModelHub", () => { const { container } = render(); expect(container).toBeInTheDocument(); }); + + it("displays health status correctly for models with health check information", async () => { + const mockModelsWithHealthChecks = [ + { + model_group: "gpt-4", + providers: ["openai"], + mode: "chat", + health_status: "healthy", + health_response_time: 150.5, + health_checked_at: "2024-01-15T10:30:00Z", + supports_function_calling: true, + supports_vision: false, + supports_parallel_function_calling: false, + }, + { + model_group: "claude-3", + providers: ["anthropic"], + mode: "chat", + health_status: "unhealthy", + health_response_time: 5000.0, + health_checked_at: "2024-01-15T10:25:00Z", + supports_function_calling: true, + supports_vision: false, + supports_parallel_function_calling: false, + }, + { + model_group: "gpt-3.5-turbo", + providers: ["openai"], + mode: "chat", + health_status: undefined, + health_response_time: undefined, + health_checked_at: undefined, + supports_function_calling: false, + supports_vision: false, + supports_parallel_function_calling: false, + }, + ]; + + const networkingModule = await import("./networking"); + vi.mocked(networkingModule.modelHubPublicModelsCall).mockResolvedValue(mockModelsWithHealthChecks); + + render(); + + // Wait for the component to load and render the table + await waitFor(() => { + expect(screen.getByText("gpt-4")).toBeInTheDocument(); + }); + + // Check that health status is displayed for healthy model (gpt-4) + // Find the row containing "gpt-4" and verify it has "healthy" status + await waitFor(() => { + const gpt4Cell = screen.getByText("gpt-4"); + const gpt4Row = gpt4Cell.closest("tr"); + expect(gpt4Row).toBeInTheDocument(); + + // Find all cells in the row + const cells = gpt4Row?.querySelectorAll("td"); + expect(cells).toBeTruthy(); + + // Find the cell containing "healthy" text (health status column) + // The health status is in a Tag component, so look for a Tag containing "healthy" + const healthyStatus = Array.from(cells || []).find((cell) => { + const tag = cell.querySelector('[class*="ant-tag"]'); + const text = tag?.textContent?.toLowerCase(); + return text === "healthy"; + }); + expect(healthyStatus).toBeInTheDocument(); + }); + + // Check that health status is displayed for unhealthy model (claude-3) + await waitFor(() => { + const claude3Cell = screen.getByText("claude-3"); + const claude3Row = claude3Cell.closest("tr"); + expect(claude3Row).toBeInTheDocument(); + + // Find all cells in the row + const cells = claude3Row?.querySelectorAll("td"); + expect(cells).toBeTruthy(); + + // Find the cell containing "unhealthy" text (health status column) + const unhealthyStatus = Array.from(cells || []).find((cell) => { + const tag = cell.querySelector('[class*="ant-tag"]'); + const text = tag?.textContent?.toLowerCase(); + return text === "unhealthy"; + }); + expect(unhealthyStatus).toBeInTheDocument(); + }); + + // Check that "Unknown" is displayed for model without health status (gpt-3.5-turbo) + await waitFor(() => { + const gpt35Cell = screen.getByText("gpt-3.5-turbo"); + const gpt35Row = gpt35Cell.closest("tr"); + expect(gpt35Row).toBeInTheDocument(); + + // Find all cells in the row + const cells = gpt35Row?.querySelectorAll("td"); + expect(cells).toBeTruthy(); + + // Find the cell containing "Unknown" text (health status column) + const unknownStatus = Array.from(cells || []).find((cell) => { + const tag = cell.querySelector('[class*="ant-tag"]'); + const text = tag?.textContent; + return text === "Unknown"; + }); + expect(unknownStatus).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/public_model_hub.tsx b/ui/litellm-dashboard/src/components/public_model_hub.tsx index f3ce3dff1d1..6f95904e9f8 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.tsx @@ -36,6 +36,9 @@ interface ModelGroupInfo { supports_vision: boolean; supports_function_calling: boolean; supported_openai_params?: string[]; + health_status?: string; + health_response_time?: number; + health_checked_at?: string; [key: string]: any; } @@ -687,6 +690,27 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded }, size: 120, }, + { + header: "Health Status", + accessorKey: "health_status", + enableSorting: true, + cell: ({ row }) => { + const original = row.original; + const tagColor = original.health_status === "healthy" ? "green" : original.health_status === "unhealthy" ? "red" : "default"; + const responseTimeLabel = original.health_response_time ? `Response Time: ${Number(original.health_response_time).toFixed(2)}ms` : "N/A"; + const lastCheckedLabel = original.health_checked_at ? `Last Checked: ${new Date(original.health_checked_at).toLocaleString()}` : "N/A"; + + return +
+ {responseTimeLabel} +
+
+ {lastCheckedLabel} +
+ }>{original.health_status ?? "Unknown"}
; + }, + size: 100, + }, { header: "Limits", accessorKey: "rpm",