diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/FusionModelsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/FusionModelsPanel.test.tsx
index f847d1e1b9f..057b9a5bd7b 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/FusionModelsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/FusionModelsPanel.test.tsx
@@ -10,6 +10,8 @@ const modelPatchUpdateCall = vi.fn();
const modelDeleteCall = vi.fn();
const invalidate = vi.fn().mockResolvedValue(undefined);
const useFusionRouters = vi.fn();
+const toastSuccess = vi.fn();
+const toastFromError = vi.fn();
vi.mock("@/components/networking", () => ({
modelCreateCall: (...args: unknown[]) => modelCreateCall(...args),
@@ -58,9 +60,21 @@ vi.mock("@/components/common_components/team_dropdown", () => ({
),
}));
-vi.mock("@/components/common_components/DeleteResourceModal", () => ({ default: () => null }));
+vi.mock("@/components/common_components/DeleteResourceModal", () => ({
+ default: ({ title, message, onOk }: { title: string; message: string; onOk: () => void }) => (
+
+
{message}
+
+
+ ),
+}));
vi.mock("@/lib/toast", () => ({
- toast: { success: vi.fn(), fromError: vi.fn() },
+ toast: {
+ success: (...args: unknown[]) => toastSuccess(...args),
+ fromError: (...args: unknown[]) => toastFromError(...args),
+ },
}));
const existingDeployment = {
@@ -101,6 +115,7 @@ describe("FusionModelsPanel", () => {
useFusionRouters.mockReturnValue({ data: [], isLoading: false });
modelCreateCall.mockResolvedValue({});
modelPatchUpdateCall.mockResolvedValue({});
+ modelDeleteCall.mockResolvedValue({});
});
it("creates an auto Fusion model with an ordinary model/new payload", async () => {
@@ -137,6 +152,25 @@ describe("FusionModelsPanel", () => {
});
});
+ it("keeps the form open and shows the backend error when creation fails", async () => {
+ modelCreateCall.mockRejectedValue(new Error("backend unavailable"));
+ const user = userEvent.setup();
+ renderPanel();
+
+ await user.click(screen.getByRole("button", { name: "Add Fusion Model" }));
+ await user.type(screen.getByLabelText("Model name"), "fusion/coding");
+ await user.click(screen.getByRole("button", { name: "Choose panel models" }));
+ await user.click(screen.getByLabelText("Outer model"));
+ await user.click(await screen.findByRole("option", { name: "outer" }, { timeout: 5000 }));
+ await user.click(screen.getByRole("button", { name: "Load search tools" }));
+ await user.click(screen.getByRole("button", { name: "Create Fusion Model" }));
+
+ expect(await screen.findByText("backend unavailable")).toBeVisible();
+ expect(screen.getByRole("dialog", { name: "Add Fusion Model" })).toBeVisible();
+ expect(screen.getByRole("button", { name: "Create Fusion Model" })).toBeEnabled();
+ expect(invalidate).not.toHaveBeenCalled();
+ });
+
it("shows readable, full-width deliberation presets", async () => {
const user = userEvent.setup();
renderPanel();
@@ -200,4 +234,32 @@ describe("FusionModelsPanel", () => {
"fusion-id",
);
});
+
+ it("deletes a stored Fusion model and refreshes the list", async () => {
+ useFusionRouters.mockReturnValue({ data: [existingDeployment], isLoading: false });
+ const user = userEvent.setup();
+ renderPanel();
+
+ await user.click(screen.getByRole("button", { name: "Delete fusion/existing" }));
+ expect(screen.getByRole("dialog", { name: "Delete Fusion Model" })).toHaveTextContent("fusion/existing");
+ await user.click(screen.getByRole("button", { name: "Confirm delete" }));
+
+ await waitFor(() => expect(modelDeleteCall).toHaveBeenCalledWith("token", "fusion-id"));
+ await waitFor(() => expect(invalidate).toHaveBeenCalled());
+ expect(toastSuccess).toHaveBeenCalledWith("Deleted Fusion model: fusion/existing");
+ });
+
+ it("keeps the delete dialog open when deletion fails", async () => {
+ useFusionRouters.mockReturnValue({ data: [existingDeployment], isLoading: false });
+ modelDeleteCall.mockRejectedValue(new Error("backend unavailable"));
+ const user = userEvent.setup();
+ renderPanel();
+
+ await user.click(screen.getByRole("button", { name: "Delete fusion/existing" }));
+ await user.click(screen.getByRole("button", { name: "Confirm delete" }));
+
+ await waitFor(() => expect(toastFromError).toHaveBeenCalled());
+ expect(screen.getByRole("dialog", { name: "Delete Fusion Model" })).toBeVisible();
+ expect(invalidate).not.toHaveBeenCalled();
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.test.ts
index 68af9c7d4a1..e73dc92607f 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.test.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.test.ts
@@ -74,6 +74,15 @@ describe("Fusion model configuration", () => {
).toMatchObject({ search_tool_name: "web-search", max_tool_calls: 4 });
});
+ it("rejects non-finite and fractional numeric settings before sending them", () => {
+ expect(fusionConfigError(validValue({ panel_timeout_seconds: Number.NaN }), false)).toMatch(/timeout/);
+ expect(fusionConfigError(validValue({ max_candidate_chars: 1000.5 }), false)).toMatch(/Candidate limit/);
+ expect(fusionConfigError(validValue({ temperature: Number.NaN }), false)).toMatch(/temperature/);
+ expect(fusionConfigError(validValue({ web_access_enabled: true, search_tool_name: " " }), false)).toMatch(
+ /Search Tool/,
+ );
+ });
+
it("includes team scope only when required", () => {
expect(fusionModelPayload(validValue({ team_id: "team-1" }), true).model_info).toEqual({ team_id: "team-1" });
expect(fusionConfigError(validValue(), true)).toBe("Select a team to continue.");
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.ts
index 626c73ec71c..e4534e86b27 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.ts
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/FusionModels/fusionModelConfig.ts
@@ -47,7 +47,13 @@ const numberOr = (value: unknown, fallback: number): number =>
const REASONING_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]);
const webAccessConfigError = (value: FusionFormValue): string | null =>
- value.web_access_enabled && !value.search_tool_name ? "Select a Search Tool or turn Web access off." : null;
+ value.web_access_enabled && !value.search_tool_name.trim() ? "Select a Search Tool or turn Web access off." : null;
+
+const isFiniteNumberInRange = (value: number, minimum: number, maximum: number): boolean =>
+ Number.isFinite(value) && value >= minimum && value <= maximum;
+
+const isIntegerInRange = (value: number, minimum: number, maximum: number): boolean =>
+ Number.isInteger(value) && value >= minimum && value <= maximum;
export const parseFusionConfig = (value: unknown): FusionRouterConfigValue => {
const config = asRecord(value);
@@ -77,26 +83,24 @@ export const parseFusionConfig = (value: unknown): FusionRouterConfigValue => {
export const fusionConfigError = (value: FusionFormValue, requiresTeamScope: boolean): string | null => {
if (!value.model_name.trim()) return "Fusion model name is required.";
if (requiresTeamScope && !value.team_id) return "Select a team to continue.";
- if (!value.outer_model) return "Select the outer model.";
+ if (!value.outer_model.trim()) return "Select the outer model.";
if (value.panel_models.length < 1) return "Select at least one panel model.";
if (value.panel_models.length > 8) return "A Fusion panel can contain at most eight models.";
const webAccessError = webAccessConfigError(value);
if (webAccessError) return webAccessError;
- if (value.panel_timeout_seconds <= 0 || value.panel_timeout_seconds > 600) {
+ if (!isFiniteNumberInRange(value.panel_timeout_seconds, Number.MIN_VALUE, 600)) {
return "Panel and analyst timeout must be between 1 and 600 seconds.";
}
- if (value.max_candidate_chars < 1000 || value.max_candidate_chars > 50000) {
+ if (!isIntegerInRange(value.max_candidate_chars, 1000, 50000)) {
return "Candidate limit must be between 1,000 and 50,000 characters.";
}
- if (
- !Number.isInteger(value.max_completion_tokens) ||
- value.max_completion_tokens < 1 ||
- value.max_completion_tokens > 128000
- ) {
+ if (!isIntegerInRange(value.max_completion_tokens, 1, 128000)) {
return "Internal output tokens must be between 1 and 128,000.";
}
- if (value.temperature < 0 || value.temperature > 2) return "Panel temperature must be between 0 and 2.";
- if (!Number.isInteger(value.max_tool_calls) || value.max_tool_calls < 1 || value.max_tool_calls > 16) {
+ if (!isFiniteNumberInRange(value.temperature, 0, 2)) {
+ return "Panel temperature must be between 0 and 2.";
+ }
+ if (!isIntegerInRange(value.max_tool_calls, 1, 16)) {
return "Tool calls must be between 1 and 16.";
}
return null;