diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 77cc137b73..09cf4732b1 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -83,7 +83,7 @@ const defaultServerRootPath = "/"; export let serverRootPath = defaultServerRootPath; export let proxyBaseUrl = defaultProxyBaseUrl; if (isLocal != true) { - console.log = function () { }; + console.log = function () {}; } const getWindowLocation = () => { @@ -2009,12 +2009,35 @@ export const regenerateKeyCall = async (accessToken: string, keyToRegenerate: st let ModelListerrorShown = false; let errorTimer: NodeJS.Timeout | null = null; -export const modelInfoCall = async (accessToken: string, userID: string, userRole: string, page: number = 1, size: number = 50, search?: string, modelId?: string, teamId?: string, sortBy?: string, sortOrder?: string) => { +export const modelInfoCall = async ( + accessToken: string, + userID: string, + userRole: string, + page: number = 1, + size: number = 50, + search?: string, + modelId?: string, + teamId?: string, + sortBy?: string, + sortOrder?: string, +) => { /** * Get all models on proxy */ try { - console.log("modelInfoCall:", accessToken, userID, userRole, page, size, search, modelId, teamId, sortBy, sortOrder); + console.log( + "modelInfoCall:", + accessToken, + userID, + userRole, + page, + size, + search, + modelId, + teamId, + sortBy, + sortOrder, + ); let url = proxyBaseUrl ? `${proxyBaseUrl}/v2/model/info` : `/v2/model/info`; const params = new URLSearchParams(); params.append("include_team_models", "true"); @@ -2118,6 +2141,10 @@ export const modelHubPublicModelsCall = async () => { "Content-Type": "application/json", }, }); + if (!response.ok) { + console.error(`modelHubPublicModelsCall failed with status ${response.status}`); + return []; + } return response.json(); }; @@ -2129,6 +2156,10 @@ export const agentHubPublicModelsCall = async () => { "Content-Type": "application/json", }, }); + if (!response.ok) { + console.error(`agentHubPublicModelsCall failed with status ${response.status}`); + return []; + } return response.json(); }; @@ -2140,6 +2171,10 @@ export const mcpHubPublicServersCall = async () => { "Content-Type": "application/json", }, }); + if (!response.ok) { + console.error(`mcpHubPublicServersCall failed with status ${response.status}`); + return []; + } return response.json(); }; @@ -2474,7 +2509,7 @@ export const modelAvailableCall = async ( teamID: string | null = null, include_model_access_groups: boolean = false, only_model_access_groups: boolean = false, - scope?: string + scope?: string, ) => { /** * Get all the models user has access to @@ -5407,9 +5442,7 @@ export const getMCPSemanticFilterSettings = async (accessToken: string) => { * Get MCP semantic filter configuration */ try { - const url = proxyBaseUrl - ? `${proxyBaseUrl}/get/mcp_semantic_filter_settings` - : `/get/mcp_semantic_filter_settings`; + const url = proxyBaseUrl ? `${proxyBaseUrl}/get/mcp_semantic_filter_settings` : `/get/mcp_semantic_filter_settings`; const response = await fetch(url, { method: "GET", headers: { @@ -5433,10 +5466,7 @@ export const getMCPSemanticFilterSettings = async (accessToken: string) => { } }; -export const updateMCPSemanticFilterSettings = async ( - accessToken: string, - settings: Record -) => { +export const updateMCPSemanticFilterSettings = async (accessToken: string, settings: Record) => { /** * Update MCP semantic filter settings * Settings will be applied across all pods within 10 seconds @@ -5469,11 +5499,7 @@ export const updateMCPSemanticFilterSettings = async ( } }; -export const testMCPSemanticFilter = async ( - accessToken: string, - model: string, - query: string -) => { +export const testMCPSemanticFilter = async (accessToken: string, model: string, query: string) => { /** * Test MCP semantic filter by making a responses API call * Returns both the response data and headers containing filter information @@ -5518,7 +5544,7 @@ export const testMCPSemanticFilter = async ( } const data = await response.json(); - + // Return both data and headers return { data, @@ -5778,7 +5804,9 @@ export const createPolicyAttachmentCall = async (accessToken: string, attachment export const deletePolicyAttachmentCall = async (accessToken: string, attachmentId: string) => { try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` : `/policies/attachments/${attachmentId}`; + const url = proxyBaseUrl + ? `${proxyBaseUrl}/policies/attachments/${attachmentId}` + : `/policies/attachments/${attachmentId}`; const response = await fetch(url, { method: "DELETE", headers: { @@ -5804,7 +5832,9 @@ export const deletePolicyAttachmentCall = async (accessToken: string, attachment export const getResolvedGuardrails = async (accessToken: string, policyId: string) => { try { - const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails` : `/policies/${policyId}/resolved-guardrails`; + const url = proxyBaseUrl + ? `${proxyBaseUrl}/policies/${policyId}/resolved-guardrails` + : `/policies/${policyId}/resolved-guardrails`; const response = await fetch(url, { method: "GET", headers: { @@ -7158,7 +7188,7 @@ export const ragIngestCall = async ( vectorStoreId?: string, vectorStoreName?: string, vectorStoreDescription?: string, - providerSpecificParams?: Record + providerSpecificParams?: Record, ): Promise => { try { let url = proxyBaseUrl ? `${proxyBaseUrl}/rag/ingest` : `/rag/ingest`; @@ -7779,12 +7809,10 @@ export interface TestCustomCodeGuardrailResponse { export const testCustomCodeGuardrail = async ( accessToken: string, - request: TestCustomCodeGuardrailRequest + request: TestCustomCodeGuardrailRequest, ): Promise => { try { - const url = proxyBaseUrl - ? `${proxyBaseUrl}/guardrails/test_custom_code` - : `/guardrails/test_custom_code`; + const url = proxyBaseUrl ? `${proxyBaseUrl}/guardrails/test_custom_code` : `/guardrails/test_custom_code`; const response = await fetch(url, { method: "POST", @@ -8920,9 +8948,7 @@ export const updateUiSettings = async (accessToken: string, settings: Record { try { const proxyBaseUrl = getProxyBaseUrl(); - const url = proxyBaseUrl - ? `${proxyBaseUrl}/claude-code/marketplace.json` - : `/claude-code/marketplace.json`; + const url = proxyBaseUrl ? `${proxyBaseUrl}/claude-code/marketplace.json` : `/claude-code/marketplace.json`; const response = await fetch(url, { method: "GET", @@ -8951,10 +8977,7 @@ export const getClaudeCodeMarketplace = async () => { * @param accessToken - Admin access token * @param enabledOnly - If true, only return enabled plugins (default: false) */ -export const getClaudeCodePluginsList = async ( - accessToken: string, - enabledOnly: boolean = false -) => { +export const getClaudeCodePluginsList = async (accessToken: string, enabledOnly: boolean = false) => { try { const proxyBaseUrl = getProxyBaseUrl(); const url = proxyBaseUrl @@ -8989,10 +9012,7 @@ export const getClaudeCodePluginsList = async ( * @param accessToken - Admin access token * @param pluginName - Name of the plugin */ -export const getClaudeCodePluginDetails = async ( - accessToken: string, - pluginName: string -) => { +export const getClaudeCodePluginDetails = async (accessToken: string, pluginName: string) => { try { const proxyBaseUrl = getProxyBaseUrl(); const url = proxyBaseUrl @@ -9038,13 +9058,11 @@ export const registerClaudeCodePlugin = async ( homepage?: string; keywords?: string[]; category?: string; - } + }, ) => { try { const proxyBaseUrl = getProxyBaseUrl(); - const url = proxyBaseUrl - ? `${proxyBaseUrl}/claude-code/plugins` - : `/claude-code/plugins`; + const url = proxyBaseUrl ? `${proxyBaseUrl}/claude-code/plugins` : `/claude-code/plugins`; const response = await fetch(url, { method: "POST", @@ -9075,10 +9093,7 @@ export const registerClaudeCodePlugin = async ( * @param accessToken - Admin access token * @param pluginName - Name of the plugin to enable */ -export const enableClaudeCodePlugin = async ( - accessToken: string, - pluginName: string -) => { +export const enableClaudeCodePlugin = async (accessToken: string, pluginName: string) => { try { const proxyBaseUrl = getProxyBaseUrl(); const url = proxyBaseUrl @@ -9113,10 +9128,7 @@ export const enableClaudeCodePlugin = async ( * @param accessToken - Admin access token * @param pluginName - Name of the plugin to disable */ -export const disableClaudeCodePlugin = async ( - accessToken: string, - pluginName: string -) => { +export const disableClaudeCodePlugin = async (accessToken: string, pluginName: string) => { try { const proxyBaseUrl = getProxyBaseUrl(); const url = proxyBaseUrl @@ -9151,10 +9163,7 @@ export const disableClaudeCodePlugin = async ( * @param accessToken - Admin access token * @param pluginName - Name of the plugin to delete */ -export const deleteClaudeCodePlugin = async ( - accessToken: string, - pluginName: string -) => { +export const deleteClaudeCodePlugin = async (accessToken: string, pluginName: string) => { try { const proxyBaseUrl = getProxyBaseUrl(); const url = proxyBaseUrl 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 8e874989e4..9979fce5eb 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.test.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.test.tsx @@ -38,10 +38,10 @@ beforeAll(() => { matches: false, media: query, onchange: null, - addListener: () => { }, - removeListener: () => { }, - addEventListener: () => { }, - removeEventListener: () => { }, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, dispatchEvent: () => false, }), }); @@ -171,4 +171,18 @@ describe("PublicModelHub", () => { expect(unknownStatus).toBeInTheDocument(); }); }); + it("handles non-array response gracefully (regression test for e.filter crash)", async () => { + const networkingModule = await import("./networking"); + // Mock the API to return an object (like an error response) instead of an array + vi.mocked(networkingModule.modelHubPublicModelsCall).mockResolvedValue({ + detail: "No models configured", + } as any); + + render(); + + await waitFor(() => { + expect(screen.getByTestId("navbar")).toBeInTheDocument(); + expect(screen.getByText("Model Hub")).toBeInTheDocument(); + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/public_model_hub.tsx b/ui/litellm-dashboard/src/components/public_model_hub.tsx index db316d4410..0e42b658e5 100644 --- a/ui/litellm-dashboard/src/components/public_model_hub.tsx +++ b/ui/litellm-dashboard/src/components/public_model_hub.tsx @@ -250,7 +250,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded }; const filteredData = useMemo(() => { - if (!modelHubData) return []; + if (!modelHubData || !Array.isArray(modelHubData)) return []; let searchResults = modelHubData; @@ -324,7 +324,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded }, [modelHubData, searchTerm, selectedProviders, selectedModes, selectedFeatures]); const filteredAgentData = useMemo(() => { - if (!agentHubData) return []; + if (!agentHubData || !Array.isArray(agentHubData)) return []; let searchResults = agentHubData; @@ -375,7 +375,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded }, [agentHubData, agentSearchTerm, selectedAgentSkills]); const filteredMcpData = useMemo(() => { - if (!mcpHubData) return []; + if (!mcpHubData || !Array.isArray(mcpHubData)) return []; let searchResults = mcpHubData; @@ -696,18 +696,29 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded 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"; + 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"}
; + return ( + +
{responseTimeLabel}
+
{lastCheckedLabel}
+ + } + > + + {original.health_status ?? "Unknown"} + +
+ ); }, size: 100, }, @@ -963,7 +974,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded accessToken={accessToken || null} isPublicPage={true} isDarkMode={false} - toggleDarkMode={() => { }} + toggleDarkMode={() => {}} /> )} @@ -1092,6 +1103,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded }} > {modelHubData && + Array.isArray(modelHubData) && getUniqueProviders(modelHubData).map((provider) => ( {provider} @@ -1111,6 +1123,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded allowClear > {modelHubData && + Array.isArray(modelHubData) && getUniqueModes(modelHubData).map((mode) => ( {mode} @@ -1130,6 +1143,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded allowClear > {modelHubData && + Array.isArray(modelHubData) && getUniqueFeatures(modelHubData).map((feature) => ( {feature} @@ -1154,7 +1168,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded {/* Agents Tab */} - {agentHubData && agentHubData.length > 0 && ( + {agentHubData && Array.isArray(agentHubData) && agentHubData.length > 0 && (
Available Agents @@ -1192,6 +1206,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded allowClear > {agentHubData && + Array.isArray(agentHubData) && getUniqueAgentSkills(agentHubData).map((skill) => ( {skill} @@ -1217,7 +1232,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded )} {/* MCP Servers Tab */} - {mcpHubData && mcpHubData.length > 0 && ( + {mcpHubData && Array.isArray(mcpHubData) && mcpHubData.length > 0 && (
Available MCP Servers @@ -1255,6 +1270,7 @@ const PublicModelHub: React.FC = ({ accessToken, isEmbedded allowClear > {mcpHubData && + Array.isArray(mcpHubData) && getUniqueMcpTransports(mcpHubData).map((transport) => ( {transport}