fix(ui): hide default litellm params

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Krrish Dholakia 2026-07-14 16:19:43 +00:00
parent 6fc1718a4a
commit 7377f6d021
5 changed files with 5 additions and 340 deletions

View file

@ -1,139 +0,0 @@
import { describe, it, expect, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import DefaultLitellmParamsSection from "./DefaultLitellmParamsSection";
describe("DefaultLitellmParamsSection", () => {
it("should render the non-cache-control keys as JSON in the textarea", () => {
render(<DefaultLitellmParamsSection value={{ timeout: 30, max_retries: 0 }} onChange={vi.fn()} />);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
expect(textarea.value).toContain('"timeout": 30');
expect(textarea.value).toContain('"max_retries": 0');
});
it("should not show the cache control editor when no injection points are set", () => {
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={vi.fn()} />);
expect(screen.queryByTestId("cache-control-location-select-0")).not.toBeInTheDocument();
});
it("should show the cache control editor pre-populated when injection points are already set", () => {
render(
<DefaultLitellmParamsSection
value={{ cache_control_injection_points: [{ location: "message", role: "system" }] }}
onChange={vi.fn()}
/>,
);
expect(screen.getByTestId("cache-control-location-select-0")).toBeInTheDocument();
});
it("should normalize nullable cache control fields from persisted settings", () => {
render(
<DefaultLitellmParamsSection
value={{ cache_control_injection_points: [{ location: "message", role: null, index: null }] }}
onChange={vi.fn()}
/>,
);
expect(screen.getByTestId("cache-control-location-select-0")).toBeInTheDocument();
expect(screen.getByTestId("cache-control-index-input-0")).toHaveValue("");
});
it("should preserve unsupported cache control points in the JSON editor", () => {
render(
<DefaultLitellmParamsSection
value={{ cache_control_injection_points: [{ location: "tool_config" }] }}
onChange={vi.fn()}
/>,
);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
expect(textarea.value).toContain('"location": "tool_config"');
expect(screen.queryByTestId("cache-control-location-select-0")).not.toBeInTheDocument();
});
it("should call onChange with cache_control_injection_points added when the toggle is switched on", async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={onChange} />);
await user.click(screen.getByRole("switch"));
expect(onChange).toHaveBeenCalledWith({ timeout: 30, cache_control_injection_points: [{ location: "message" }] });
});
it("should call onChange with cache_control_injection_points removed when the toggle is switched off", async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(
<DefaultLitellmParamsSection
value={{ timeout: 30, cache_control_injection_points: [{ location: "message", role: "system" }] }}
onChange={onChange}
/>,
);
await user.click(screen.getByRole("switch"));
expect(onChange).toHaveBeenCalledWith({ timeout: 30 });
});
it("should merge edited JSON textarea content with cache_control_injection_points on blur", async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(
<DefaultLitellmParamsSection
value={{ timeout: 30, cache_control_injection_points: [{ location: "message" }] }}
onChange={onChange}
/>,
);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
await user.clear(textarea);
await user.type(textarea, '{{"timeout": 60}');
await user.tab();
expect(onChange).toHaveBeenCalledWith({ timeout: 60, cache_control_injection_points: [{ location: "message" }] });
});
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 }} onChange={onChange} />);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
await user.clear(textarea);
await user.type(textarea, "not json");
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 }} 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");
});
it("should reject valid JSON that is not an object", async () => {
const onChange = vi.fn();
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={onChange} />);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
fireEvent.change(textarea, { target: { value: "[]" } });
fireEvent.blur(textarea);
expect(onChange).not.toHaveBeenCalled();
expect(textarea).toHaveClass("ant-input-status-error");
});
});

View file

@ -1,166 +0,0 @@
import React from "react";
import { Input, Switch } from "antd";
import CacheControlInjectionPointsEditor, {
CacheControlInjectionPoint,
} from "../shared/cache_control_injection_points_editor";
import NotificationsManager from "../molecules/notifications_manager";
interface DefaultLitellmParamsSectionProps {
value: Record<string, unknown>;
onChange: (value: Record<string, unknown>) => void;
}
type ParsedDefaultParams = { status: "valid"; value: Record<string, unknown> } | { status: "invalid"; message: string };
const CACHE_CONTROL_ROLES = ["user", "system", "assistant"] as const;
type CacheControlRole = (typeof CACHE_CONTROL_ROLES)[number];
const parseDefaultParams = (text: string): ParsedDefaultParams => {
try {
const parsed: unknown = JSON.parse(text || "{}");
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
return { status: "invalid", message: "Expected a JSON object" };
}
return { status: "valid", value: parsed as Record<string, unknown> };
} catch (error) {
return { status: "invalid", message: error instanceof Error ? error.message : "Invalid JSON" };
}
};
const isCacheControlRole = (value: unknown): value is CacheControlRole =>
CACHE_CONTROL_ROLES.some((validRole) => validRole === value);
const parseCacheControlInjectionPoint = (value: unknown): CacheControlInjectionPoint | undefined => {
if (typeof value !== "object" || value === null) {
return undefined;
}
if (!("location" in value) || value.location !== "message") {
return undefined;
}
const role = "role" in value ? value.role : undefined;
const index = "index" in value ? value.index : undefined;
const hasInvalidRole = role !== undefined && role !== null && !isCacheControlRole(role);
if (hasInvalidRole) {
return undefined;
}
const hasIndex = index !== undefined && index !== null;
const hasInvalidIndex = hasIndex && (typeof index !== "number" || !Number.isInteger(index));
if (hasInvalidIndex) {
return undefined;
}
return {
location: "message",
...(isCacheControlRole(role) ? { role } : {}),
...(typeof index === "number" ? { index } : {}),
};
};
const parseCacheControlInjectionPoints = (value: unknown): CacheControlInjectionPoint[] | undefined => {
if (!Array.isArray(value)) {
return undefined;
}
const points = value.map(parseCacheControlInjectionPoint);
return points.every((point) => point !== undefined)
? points.filter((point): point is CacheControlInjectionPoint => point !== undefined)
: undefined;
};
const DefaultLitellmParamsSection: React.FC<DefaultLitellmParamsSectionProps> = ({ value, onChange }) => {
const cacheControlInjectionPoints = React.useMemo(
() => parseCacheControlInjectionPoints(value.cache_control_injection_points),
[value.cache_control_injection_points],
);
const otherParams = React.useMemo(
() =>
Object.fromEntries(
Object.entries(value).filter(
([key]) => key !== "cache_control_injection_points" || cacheControlInjectionPoints === undefined,
),
),
[value, cacheControlInjectionPoints],
);
const [otherParamsText, setOtherParamsText] = React.useState(() => JSON.stringify(otherParams, null, 2));
const [hasInvalidJson, setHasInvalidJson] = React.useState(false);
const showCacheControl = (cacheControlInjectionPoints?.length ?? 0) > 0;
const handleOtherParamsBlur = () => {
const parsed = parseDefaultParams(otherParamsText);
if (parsed.status === "invalid") {
setHasInvalidJson(true);
NotificationsManager.warning(`Default LiteLLM Params is not valid JSON: ${parsed.message}`);
return;
}
setHasInvalidJson(false);
onChange(
cacheControlInjectionPoints
? { ...parsed.value, cache_control_injection_points: cacheControlInjectionPoints }
: parsed.value,
);
};
const handleCacheControlPointsChange = (points: CacheControlInjectionPoint[]) => {
const parsed = parseDefaultParams(otherParamsText);
const base = parsed.status === "valid" ? parsed.value : otherParams;
onChange(points.length > 0 ? { ...base, cache_control_injection_points: points } : base);
};
const handleCacheControlToggle = (checked: boolean) => {
handleCacheControlPointsChange(checked ? [{ location: "message" }] : []);
};
return (
<div className="space-y-6">
<div className="max-w-3xl space-y-2">
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">Default LiteLLM Params</span>
<p className="text-xs text-gray-500 mt-0.5 mb-2">
Default parameters for Router.chat.completion.create. Set cache control injection points to enable prompt
caching for every model on this proxy.{" "}
<a
href="https://docs.litellm.ai/docs/tutorials/claude_code_prompt_cache_routing"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
Learn more
</a>
</p>
<Input.TextArea
value={otherParamsText}
onChange={(e) => {
setOtherParamsText(e.target.value);
setHasInvalidJson(false);
}}
onBlur={handleOtherParamsBlur}
status={hasInvalidJson ? "error" : undefined}
autoSize={{ minRows: 2 }}
className="font-mono text-sm w-full"
/>
</div>
<div className="max-w-3xl">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
Cache Control Injection Points
</span>
<Switch
checked={showCacheControl}
onChange={handleCacheControlToggle}
className="bg-gray-600"
aria-label="Cache Control Injection Points"
/>
</div>
{showCacheControl && (
<div className="ml-6 pl-4 border-l-2 border-gray-200">
<CacheControlInjectionPointsEditor
value={cacheControlInjectionPoints || [{ location: "message" }]}
onChange={handleCacheControlPointsChange}
/>
</div>
)}
</div>
</div>
);
};
export default DefaultLitellmParamsSection;

View file

@ -156,29 +156,14 @@ describe("RouterSettingsForm", () => {
);
});
it("should show frontend-defined Default LiteLLM Params without backend field metadata", () => {
render(<RouterSettingsForm {...baseProps} />);
expect(screen.getByText("Cache Control Injection Points")).toBeInTheDocument();
});
it("should call onChange with the updated default_litellm_params when the section changes", async () => {
const onChange = vi.fn();
const user = userEvent.setup();
it("should not render Default LiteLLM Params when persisted values exist", () => {
const props = {
...baseProps,
onChange,
value: { ...defaultValue, routerSettings: { default_litellm_params: { timeout: 30 } } },
};
render(<RouterSettingsForm {...props} />);
await user.click(screen.getByRole("switch", { name: "Cache Control Injection Points" }));
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
routerSettings: expect.objectContaining({
default_litellm_params: { timeout: 30, cache_control_injection_points: [{ location: "message" }] },
}),
}),
);
expect(screen.queryByText("Default LiteLLM Params")).not.toBeInTheDocument();
expect(screen.queryByText("Cache Control Injection Points")).not.toBeInTheDocument();
});
});

View file

@ -1,5 +1,4 @@
import React from "react";
import DefaultLitellmParamsSection from "./DefaultLitellmParamsSection";
import LatencyBasedConfiguration from "./LatencyBasedConfiguration";
import OptionalPreCallChecksSelector from "./OptionalPreCallChecksSelector";
import ReliabilityRetriesSection from "./ReliabilityRetriesSection";
@ -48,13 +47,6 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
});
};
const handleDefaultLitellmParamsChange = (params: Record<string, unknown>) => {
onChange({
...value,
routerSettings: { ...value.routerSettings, default_litellm_params: params },
});
};
return (
<div className="w-full space-y-8 py-2">
{/* Routing Settings Section */}
@ -89,20 +81,11 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
/>
</div>
{/* Divider */}
<div className="border-t border-gray-200" />
{/* Strategy-Specific Args - Show immediately after strategy if latency-based */}
{value.selectedStrategy === "latency-based-routing" && (
<LatencyBasedConfiguration routingStrategyArgs={value.routerSettings["routing_strategy_args"]} />
)}
{/* Default LiteLLM Params */}
<DefaultLitellmParamsSection
key={JSON.stringify(value.routerSettings.default_litellm_params || {})}
value={value.routerSettings.default_litellm_params || {}}
onChange={handleDefaultLitellmParamsChange}
/>
<div className="border-t border-gray-200" />
{/* Other Settings */}

View file

@ -139,6 +139,8 @@ describe("RouterSettings", () => {
await waitFor(() => {
expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
});
expect(screen.queryByText("Default LiteLLM Params")).not.toBeInTheDocument();
expect(screen.queryByText("Cache Control Injection Points")).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /save changes/i }));