mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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:
parent
906897bebf
commit
f6516e7be5
2 changed files with 260 additions and 8 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue