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).
This commit is contained in:
Krrish Dholakia 2026-07-15 21:41:45 -07:00 committed by GitHub
parent 906897bebf
commit f6516e7be5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 260 additions and 8 deletions

View file

@ -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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { wrapper });
await waitFor(() => {

View file

@ -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<string, unknown>): Record<string, unknown> =>
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<Record<string, boolean>>({});
const [isAutoRouterModalOpen, setIsAutoRouterModalOpen] = useState(false);
const [isAutoRouterTestModalOpen, setIsAutoRouterTestModalOpen] = useState(false);
const [autoRouterTestId, setAutoRouterTestId] = useState(0);
const [autoRouterTestTargets, setAutoRouterTestTargets] = useState<AutoRouterTestTarget[]>([]);
const [guardrailsList, setGuardrailsList] = useState<string[]>([]);
const [tagsList, setTagsList] = useState<Record<string, Tag>>({});
const [credentialsList, setCredentialsList] = useState<CredentialItem[]>([]);
@ -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({
</div>
</div>
<div className="flex gap-2">
<Button
icon={<RefreshIcon className="h-4 w-4" />}
onClick={handleTestConnection}
className="flex items-center gap-2"
data-testid="test-connection-button"
>
Test Connection
</Button>
{(!isAutoRouter || isComplexityRouter) && (
<Button
icon={<RefreshIcon className="h-4 w-4" />}
onClick={handleTestConnection}
className="flex items-center gap-2"
data-testid="test-connection-button"
>
Test Connection
</Button>
)}
<Button
icon={<KeyIcon className="h-4 w-4" />}
@ -1433,6 +1514,22 @@ export default function ModelInfoView({
accessToken={accessToken || ""}
userRole={userRole || ""}
/>
<Modal
title="Connection Test Results"
open={isAutoRouterTestModalOpen}
onCancel={() => setIsAutoRouterTestModalOpen(false)}
footer={[
<Button key="close" onClick={() => setIsAutoRouterTestModalOpen(false)}>
Close
</Button>,
]}
width={700}
>
{isAutoRouterTestModalOpen && accessToken && (
<AutoRouterConnectionTest key={autoRouterTestId} accessToken={accessToken} targets={autoRouterTestTargets} />
)}
</Modal>
</div>
);
}