mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
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:
parent
6fc1718a4a
commit
7377f6d021
5 changed files with 5 additions and 340 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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 }));
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue