diff --git a/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.test.tsx b/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.test.tsx index f95214d96d2..94a80d89cc7 100644 --- a/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.test.tsx +++ b/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.test.tsx @@ -78,7 +78,7 @@ describe("DefaultLitellmParamsSection", () => { expect(onChange).toHaveBeenCalledWith({ timeout: 60, cache_control_injection_points: [{ location: "message" }] }); }); - it("should not call onChange with invalid JSON left in the textarea on blur", async () => { + it("should not call onChange and should flag the field as invalid when the textarea has invalid JSON on blur", async () => { const onChange = vi.fn(); const user = userEvent.setup(); render(); @@ -89,5 +89,23 @@ describe("DefaultLitellmParamsSection", () => { await user.tab(); expect(onChange).not.toHaveBeenCalled(); + expect(textarea).toHaveClass("ant-input-status-error"); + }); + + it("should clear the invalid state once the textarea is edited again", async () => { + const onChange = vi.fn(); + const user = userEvent.setup(); + render(); + + const textarea = screen.getByRole("textbox") as HTMLTextAreaElement; + await user.clear(textarea); + await user.type(textarea, "not json"); + await user.tab(); + expect(textarea).toHaveClass("ant-input-status-error"); + + await user.click(textarea); + await user.type(textarea, "{{}}"); + + expect(textarea).not.toHaveClass("ant-input-status-error"); }); }); diff --git a/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.tsx b/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.tsx index 3795f80a768..273d0c65aed 100644 --- a/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.tsx +++ b/ui/litellm-dashboard/src/components/router_settings/DefaultLitellmParamsSection.tsx @@ -3,6 +3,7 @@ import { Input, Switch } from "antd"; import CacheControlInjectionPointsEditor, { CacheControlInjectionPoint, } from "../shared/cache_control_injection_points_editor"; +import NotificationsManager from "../molecules/notifications_manager"; interface DefaultLitellmParamsSectionProps { value: { [key: string]: any }; @@ -20,6 +21,7 @@ const DefaultLitellmParamsSection: React.FC = const [otherParamsText, setOtherParamsText] = React.useState(() => JSON.stringify(otherParams, null, 2)); const [showCacheControl, setShowCacheControl] = React.useState((cache_control_injection_points?.length ?? 0) > 0); + const [hasInvalidJson, setHasInvalidJson] = React.useState(false); const parseOtherParams = (): { [key: string]: any } => { try { @@ -32,9 +34,11 @@ const DefaultLitellmParamsSection: React.FC = const handleOtherParamsBlur = () => { try { const parsed = JSON.parse(otherParamsText || "{}"); + setHasInvalidJson(false); onChange({ ...parsed, cache_control_injection_points }); } catch (error) { - console.error("Error parsing default_litellm_params JSON:", error); + setHasInvalidJson(true); + NotificationsManager.warning(`Default LiteLLM Params is not valid JSON, change not saved: ${error}`); } }; @@ -57,8 +61,12 @@ const DefaultLitellmParamsSection: React.FC =

{meta?.field_description || ""}

setOtherParamsText(e.target.value)} + onChange={(e) => { + setOtherParamsText(e.target.value); + setHasInvalidJson(false); + }} onBlur={handleOtherParamsBlur} + status={hasInvalidJson ? "error" : undefined} autoSize={{ minRows: 2 }} className="font-mono text-sm w-full" />