fix(ui): surface a visible error when default_litellm_params JSON is invalid

Invalid JSON typed into the Default LiteLLM Params textarea was silently
swallowed on blur (console.error only, no on-screen indication), so an admin
could think their edit was saved when it was actually discarded. Now flags
the field with an error state and shows a warning notification, and clears
both once the field is edited again.
This commit is contained in:
Krrish Dholakia 2026-07-13 21:04:12 -07:00
parent f6d67f025d
commit 221183b2f4
2 changed files with 29 additions and 3 deletions

View file

@ -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(<DefaultLitellmParamsSection value={{ timeout: 30 }} routerFieldsMetadata={{}} onChange={onChange} />);
@ -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(<DefaultLitellmParamsSection value={{ timeout: 30 }} routerFieldsMetadata={{}} onChange={onChange} />);
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");
});
});

View file

@ -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<DefaultLitellmParamsSectionProps> =
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<DefaultLitellmParamsSectionProps> =
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<DefaultLitellmParamsSectionProps> =
<p className="text-xs text-gray-500 mt-0.5 mb-2">{meta?.field_description || ""}</p>
<Input.TextArea
value={otherParamsText}
onChange={(e) => 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"
/>