From f6516e7be5256fa69f1ff29f7a1ec53e566f8ecc Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 15 Jul 2026 21:41:45 -0700 Subject: [PATCH] fix(ui): stop sending the complexity-router pseudo-model to /health/test_connection (#33498) * fix(ui): stop sending the complexity-router pseudo-model to /health/test_connection Test Connection on a saved auto-router model sent the raw "auto_router/complexity_router" model string to the generic health-check endpoint, which always failed with "Unmapped LLM provider" since it's a routing-strategy config, not a real completion endpoint. Reuse the per-tier connection test already built for the Add Auto Router wizard: for complexity-router models, test each configured tier's underlying model group instead of the router pseudo-model. Semantic-type auto routers (auto_router_config) have no equivalent tier-based test yet, so the button is hidden for them instead of guaranteed to fail. * fix(ui): address review feedback on auto-router test connection fix Type the complexity-router config parsing instead of using `any`, use NotificationsManager.warning instead of fromBackend for the client-generated "no tiers configured" message, remove comments added in the previous commit, and also test the deployment's configured complexity_router_default_model as a fallback target when it isn't already covered by a configured tier (matches the fallback Router itself uses for unconfigured tiers). --- .../src/components/model_info_view.test.tsx | 155 ++++++++++++++++++ .../src/components/model_info_view.tsx | 113 ++++++++++++- 2 files changed, 260 insertions(+), 8 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_info_view.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.test.tsx index 2546601b4db..a496bb05b91 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -16,6 +16,7 @@ vi.mock("./molecules/notifications_manager", () => ({ success: vi.fn(), error: vi.fn(), info: vi.fn(), + warning: vi.fn(), fromBackend: vi.fn(), }, })); @@ -27,6 +28,7 @@ vi.mock("./networking", () => ({ getGuardrailsList: vi.fn(), tagListCall: vi.fn(), testConnectionRequest: vi.fn(), + testModelGroupConnection: vi.fn(), modelPatchUpdateCall: vi.fn(), modelDeleteCall: vi.fn(), credentialCreateCall: vi.fn(), @@ -52,6 +54,7 @@ const mockCredentialListCall = vi.mocked(networking.credentialListCall); const mockGetGuardrailsList = vi.mocked(networking.getGuardrailsList); const mockTagListCall = vi.mocked(networking.tagListCall); const mockTestConnectionRequest = vi.mocked(networking.testConnectionRequest); +const mockTestModelGroupConnection = vi.mocked(networking.testModelGroupConnection); const mockModelPatchUpdateCall = vi.mocked(networking.modelPatchUpdateCall); const mockModelDeleteCall = vi.mocked(networking.modelDeleteCall); const mockCredentialCreateCall = vi.mocked(networking.credentialCreateCall); @@ -732,6 +735,158 @@ describe("ModelInfoView", () => { }); }); + it("does not offer Test Connection for semantic auto router models (no tier-based test exists yet)", async () => { + const semanticAutoRouterModelData = { + ...defaultModelData, + litellm_params: { + ...defaultModelData.litellm_params, + auto_router_config: {}, + }, + }; + + mockUseModelsInfo.mockReturnValue({ + data: { + data: [semanticAutoRouterModelData], + }, + isLoading: false, + error: null, + }); + + render(, { wrapper }); + await waitFor(() => { + expect(screen.getByText("Model Settings")).toBeInTheDocument(); + }); + expect(screen.queryByTestId("test-connection-button")).not.toBeInTheDocument(); + }); + + it("tests each complexity tier's model group instead of sending the router pseudo-model to /health/test_connection (regression: raw test previously threw 'Unmapped LLM provider... model=complexity_router')", async () => { + const complexityRouterModelData = { + ...defaultModelData, + litellm_params: { + ...defaultModelData.litellm_params, + model: "auto_router/complexity_router", + complexity_router_config: { + tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: ["gpt-4o"], COMPLEX: [], REASONING: [] }, + }, + }, + }; + + mockUseModelsInfo.mockReturnValue({ + data: { + data: [complexityRouterModelData], + }, + isLoading: false, + error: null, + }); + mockTestModelGroupConnection.mockResolvedValue({ status: "success" }); + + render(, { wrapper }); + const testConnectionButton = await screen.findByTestId("test-connection-button"); + await userEvent.click(testConnectionButton); + + await waitFor(() => { + expect(mockTestModelGroupConnection).toHaveBeenCalledWith("test-token", "gpt-4o-mini", "chat"); + expect(mockTestModelGroupConnection).toHaveBeenCalledWith("test-token", "gpt-4o", "chat"); + }); + expect(mockTestConnectionRequest).not.toHaveBeenCalled(); + }); + + it("also tests the configured default model when an unconfigured tier would fall back to it in production", async () => { + const complexityRouterModelData = { + ...defaultModelData, + litellm_params: { + ...defaultModelData.litellm_params, + model: "auto_router/complexity_router", + complexity_router_config: { + tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: [], COMPLEX: [], REASONING: [] }, + }, + complexity_router_default_model: "gpt-4o", + }, + }; + + mockUseModelsInfo.mockReturnValue({ + data: { + data: [complexityRouterModelData], + }, + isLoading: false, + error: null, + }); + mockTestModelGroupConnection.mockResolvedValue({ status: "success" }); + + render(, { wrapper }); + const testConnectionButton = await screen.findByTestId("test-connection-button"); + await userEvent.click(testConnectionButton); + + await waitFor(() => { + expect(mockTestModelGroupConnection).toHaveBeenCalledWith("test-token", "gpt-4o-mini", "chat"); + expect(mockTestModelGroupConnection).toHaveBeenCalledWith("test-token", "gpt-4o", "chat"); + }); + }); + + it("does not duplicate the default model as a test target when it is already covered by a configured tier", async () => { + const complexityRouterModelData = { + ...defaultModelData, + litellm_params: { + ...defaultModelData.litellm_params, + model: "auto_router/complexity_router", + complexity_router_config: { + tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: ["gpt-4o"], COMPLEX: [], REASONING: [] }, + }, + complexity_router_default_model: "gpt-4o", + }, + }; + + mockUseModelsInfo.mockReturnValue({ + data: { + data: [complexityRouterModelData], + }, + isLoading: false, + error: null, + }); + mockTestModelGroupConnection.mockResolvedValue({ status: "success" }); + + render(, { wrapper }); + const testConnectionButton = await screen.findByTestId("test-connection-button"); + await userEvent.click(testConnectionButton); + + await waitFor(() => { + expect(mockTestModelGroupConnection).toHaveBeenCalledWith("test-token", "gpt-4o", "chat"); + }); + expect(mockTestModelGroupConnection).toHaveBeenCalledTimes(2); + }); + + it("warns instead of erroring when no complexity tiers are configured to test", async () => { + const complexityRouterModelData = { + ...defaultModelData, + litellm_params: { + ...defaultModelData.litellm_params, + model: "auto_router/complexity_router", + complexity_router_config: { + tiers: { SIMPLE: [], MEDIUM: [], COMPLEX: [], REASONING: [] }, + }, + }, + }; + + mockUseModelsInfo.mockReturnValue({ + data: { + data: [complexityRouterModelData], + }, + isLoading: false, + error: null, + }); + + render(, { wrapper }); + const testConnectionButton = await screen.findByTestId("test-connection-button"); + await userEvent.click(testConnectionButton); + + await waitFor(() => { + expect(mockNotificationsManager.warning).toHaveBeenCalledWith( + "No complexity tiers are configured yet, so there is nothing to test.", + ); + }); + expect(mockTestModelGroupConnection).not.toHaveBeenCalled(); + }); + it("should display model access groups field", async () => { render(, { wrapper }); await waitFor(() => { diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index aaad6e5a474..5b7f82c44b4 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -23,6 +23,8 @@ import { CheckIcon, CopyIcon } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { copyToClipboard as utilCopyToClipboard } from "../utils/dataUtils"; import { formItemValidateJSON, truncateString } from "../utils/textUtils"; +import AutoRouterConnectionTest from "./add_model/auto_router_connection_test"; +import { AutoRouterTestTarget, buildAutoRouterTestTargets } from "./add_model/build_auto_router_test_targets"; import CacheControlSettings from "./add_model/cache_control_settings"; import DeleteResourceModal from "./common_components/DeleteResourceModal"; import EditAutoRouterModal from "./edit_auto_router/edit_auto_router_modal"; @@ -68,6 +70,66 @@ const isMaskedSecret = (value: unknown): boolean => typeof value === "string" && const stripMaskedSecrets = (params: Record): Record => Object.fromEntries(Object.entries(params).filter(([, value]) => !isMaskedSecret(value))); +const normalizeTierModels = (value: unknown): string[] => { + if (Array.isArray(value)) return value; + if (typeof value === "string" && value) return [value]; + return []; +}; + +interface ComplexityRouterTierConfig { + tiers?: { + SIMPLE?: unknown; + MEDIUM?: unknown; + COMPLEX?: unknown; + REASONING?: unknown; + }; + semantic_keyword_matching?: boolean; + embedding_model?: string; +} + +interface ComplexityRouterModelData { + litellm_params?: { + complexity_router_config?: ComplexityRouterTierConfig | string; + complexity_router_default_model?: string; + }; +} + +const buildComplexityRouterTestTargets = ( + modelData: ComplexityRouterModelData | null | undefined, +): AutoRouterTestTarget[] => { + const rawConfig = modelData?.litellm_params?.complexity_router_config; + let config: ComplexityRouterTierConfig = {}; + if (typeof rawConfig === "string") { + try { + config = JSON.parse(rawConfig); + } catch { + config = {}; + } + } else if (rawConfig) { + config = rawConfig; + } + + const tierTargets = buildAutoRouterTestTargets({ + tiers: { + SIMPLE: normalizeTierModels(config.tiers?.SIMPLE), + MEDIUM: normalizeTierModels(config.tiers?.MEDIUM), + COMPLEX: normalizeTierModels(config.tiers?.COMPLEX), + REASONING: normalizeTierModels(config.tiers?.REASONING), + }, + semanticMatchingEnabled: Boolean(config.semantic_keyword_matching), + embeddingModel: config.embedding_model, + }); + + const defaultModel = modelData?.litellm_params?.complexity_router_default_model?.trim(); + if (!defaultModel || tierTargets.some((target) => target.modelGroup === defaultModel)) { + return tierTargets; + } + return [ + ...tierTargets, + { labels: ["Default (unconfigured tiers)"], modelGroup: defaultModel, mode: "chat" as const }, + ]; +}; + export default function ModelInfoView({ modelId, onClose, @@ -91,6 +153,9 @@ export default function ModelInfoView({ const [showCacheControl, setShowCacheControl] = useState(false); const [copiedStates, setCopiedStates] = useState>({}); const [isAutoRouterModalOpen, setIsAutoRouterModalOpen] = useState(false); + const [isAutoRouterTestModalOpen, setIsAutoRouterTestModalOpen] = useState(false); + const [autoRouterTestId, setAutoRouterTestId] = useState(0); + const [autoRouterTestTargets, setAutoRouterTestTargets] = useState([]); const [guardrailsList, setGuardrailsList] = useState([]); const [tagsList, setTagsList] = useState>({}); const [credentialsList, setCredentialsList] = useState([]); @@ -128,6 +193,9 @@ export default function ModelInfoView({ modelData?.litellm_params?.auto_router_config != null || modelData?.litellm_params?.complexity_router_config != null || modelData?.litellm_params?.model?.startsWith("auto_router/complexity_router"); + const isComplexityRouter = + modelData?.litellm_params?.complexity_router_config != null || + modelData?.litellm_params?.model?.startsWith("auto_router/complexity_router"); const usingExistingCredential = modelData?.litellm_params?.litellm_credential_name != null && @@ -435,6 +503,17 @@ export default function ModelInfoView({ const handleTestConnection = async () => { if (!accessToken) return; + if (isComplexityRouter) { + const targets = buildComplexityRouterTestTargets(localModelData ?? modelData); + if (targets.length === 0) { + NotificationsManager.warning("No complexity tiers are configured yet, so there is nothing to test."); + return; + } + setAutoRouterTestTargets(targets); + setAutoRouterTestId((id) => id + 1); + setIsAutoRouterTestModalOpen(true); + return; + } try { NotificationsManager.info("Testing connection..."); const response = await testConnectionRequest( @@ -536,14 +615,16 @@ export default function ModelInfoView({
- + {(!isAutoRouter || isComplexityRouter) && ( + + )} , + ]} + width={700} + > + {isAutoRouterTestModalOpen && accessToken && ( + + )} +
); }