mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(ui): restore cache control router fields
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
7377f6d021
commit
d418752267
6 changed files with 322 additions and 7 deletions
|
|
@ -0,0 +1,70 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import CacheControlInjectionPointsSection from "./CacheControlInjectionPointsSection";
|
||||
|
||||
describe("CacheControlInjectionPointsSection", () => {
|
||||
it("should render the frontend-owned field without exposing the raw default params input", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<CacheControlInjectionPointsSection value={{}} onChange={onChange} />);
|
||||
|
||||
expect(screen.getByText("Cache Control Injection Points")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Default LiteLLM Params")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("cache-control-location-select-0")).not.toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should render persisted injection point fields", () => {
|
||||
render(
|
||||
<CacheControlInjectionPointsSection
|
||||
value={{ cache_control_injection_points: [{ location: "message", role: "system", index: -2 }] }}
|
||||
onChange={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("cache-control-location-select-0")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("cache-control-index-input-0")).toHaveValue("-2");
|
||||
});
|
||||
|
||||
it("should add injection points without replacing other default params", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<CacheControlInjectionPointsSection value={{ timeout: 30 }} onChange={onChange} />);
|
||||
|
||||
await user.click(screen.getByRole("switch", { name: "Cache Control Injection Points" }));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({
|
||||
timeout: 30,
|
||||
cache_control_injection_points: [{ location: "message" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("should remove injection points without replacing other default params", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CacheControlInjectionPointsSection
|
||||
value={{ timeout: 30, cache_control_injection_points: [{ location: "message" }] }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("switch", { name: "Cache Control Injection Points" }));
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith({ timeout: 30 });
|
||||
});
|
||||
|
||||
it("should preserve unsupported injection point values", () => {
|
||||
const onChange = vi.fn();
|
||||
render(
|
||||
<CacheControlInjectionPointsSection
|
||||
value={{ cache_control_injection_points: [{ location: "tool_config" }] }}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("switch", { name: "Cache Control Injection Points" })).toBeDisabled();
|
||||
expect(screen.getByText(/will be preserved unchanged/i)).toBeInTheDocument();
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React from "react";
|
||||
import { Switch } from "antd";
|
||||
import CacheControlInjectionPointsEditor, {
|
||||
CacheControlInjectionPoint,
|
||||
} from "../shared/cache_control_injection_points_editor";
|
||||
|
||||
interface CacheControlInjectionPointsSectionProps {
|
||||
value: Record<string, unknown>;
|
||||
onChange: (value: Record<string, unknown>) => void;
|
||||
}
|
||||
|
||||
const CACHE_CONTROL_ROLES = ["user", "system", "assistant"] as const;
|
||||
type CacheControlRole = (typeof CACHE_CONTROL_ROLES)[number];
|
||||
|
||||
const isCacheControlRole = (value: unknown): value is CacheControlRole =>
|
||||
CACHE_CONTROL_ROLES.some((role) => role === value);
|
||||
|
||||
const parseInjectionPoint = (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 hasRole = role !== undefined && role !== null;
|
||||
if (hasRole && !isCacheControlRole(role)) {
|
||||
return undefined;
|
||||
}
|
||||
const hasIndex = index !== undefined && index !== null;
|
||||
if (hasIndex && typeof index !== "number") {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof index === "number" && !Number.isInteger(index)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
location: "message",
|
||||
...(isCacheControlRole(role) ? { role } : {}),
|
||||
...(typeof index === "number" ? { index } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
const parseInjectionPoints = (value: unknown): CacheControlInjectionPoint[] | undefined => {
|
||||
if (value === undefined) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const points = value.map(parseInjectionPoint);
|
||||
return points.every((point) => point !== undefined)
|
||||
? points.filter((point): point is CacheControlInjectionPoint => point !== undefined)
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const CacheControlInjectionPointsSection: React.FC<CacheControlInjectionPointsSectionProps> = ({ value, onChange }) => {
|
||||
const injectionPoints = React.useMemo(
|
||||
() => parseInjectionPoints(value.cache_control_injection_points),
|
||||
[value.cache_control_injection_points],
|
||||
);
|
||||
const enabled = injectionPoints !== undefined && injectionPoints.length > 0;
|
||||
|
||||
const handlePointsChange = (points: CacheControlInjectionPoint[]) => {
|
||||
onChange({ ...value, cache_control_injection_points: points });
|
||||
};
|
||||
|
||||
const handleToggle = (checked: boolean) => {
|
||||
if (checked) {
|
||||
handlePointsChange([{ location: "message" }]);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = Object.fromEntries(
|
||||
Object.entries(value).filter(([key]) => key !== "cache_control_injection_points"),
|
||||
);
|
||||
onChange(nextValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 max-w-3xl">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
|
||||
Cache Control Injection Points
|
||||
</span>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
Choose message locations where LiteLLM should inject cache-control markers.{" "}
|
||||
<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>
|
||||
</div>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
disabled={injectionPoints === undefined}
|
||||
onChange={handleToggle}
|
||||
aria-label="Cache Control Injection Points"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{injectionPoints === undefined && (
|
||||
<p className="text-xs text-amber-700">
|
||||
The configured injection points use values this editor does not support and will be preserved unchanged.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{enabled && <CacheControlInjectionPointsEditor value={injectionPoints} onChange={handlePointsChange} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CacheControlInjectionPointsSection;
|
||||
|
|
@ -156,14 +156,51 @@ describe("RouterSettingsForm", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("should not render Default LiteLLM Params when persisted values exist", () => {
|
||||
it("should render cache-control fields without rendering the raw Default LiteLLM Params input", () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
value: { ...defaultValue, routerSettings: { default_litellm_params: { timeout: 30 } } },
|
||||
value: {
|
||||
...defaultValue,
|
||||
routerSettings: {
|
||||
default_litellm_params: {
|
||||
timeout: 30,
|
||||
cache_control_injection_points: [{ location: "message", role: "system", index: -1 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
render(<RouterSettingsForm {...props} />);
|
||||
|
||||
expect(screen.queryByText("Default LiteLLM Params")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Cache Control Injection Points")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Cache Control Injection Points")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("cache-control-index-input-0")).toHaveValue("-1");
|
||||
});
|
||||
|
||||
it("should mark default_litellm_params as modified when cache control changes", async () => {
|
||||
const onChange = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
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: {
|
||||
default_litellm_params: {
|
||||
timeout: 30,
|
||||
cache_control_injection_points: [{ location: "message" }],
|
||||
},
|
||||
},
|
||||
modifiedRouterSettings: ["default_litellm_params"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import CacheControlInjectionPointsSection from "./CacheControlInjectionPointsSection";
|
||||
import LatencyBasedConfiguration from "./LatencyBasedConfiguration";
|
||||
import OptionalPreCallChecksSelector from "./OptionalPreCallChecksSelector";
|
||||
import ReliabilityRetriesSection from "./ReliabilityRetriesSection";
|
||||
|
|
@ -9,6 +10,7 @@ export interface RouterSettingsFormValue {
|
|||
routerSettings: { [key: string]: any };
|
||||
selectedStrategy: string | null;
|
||||
enableTagFiltering: boolean;
|
||||
modifiedRouterSettings?: string[];
|
||||
}
|
||||
|
||||
interface RouterSettingsFormProps {
|
||||
|
|
@ -19,6 +21,9 @@ interface RouterSettingsFormProps {
|
|||
routingStrategyDescriptions: { [key: string]: string };
|
||||
}
|
||||
|
||||
const markSettingModified = (modifiedSettings: string[] | undefined, setting: string): string[] =>
|
||||
modifiedSettings?.includes(setting) ? modifiedSettings : [...(modifiedSettings || []), setting];
|
||||
|
||||
const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -44,6 +49,15 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
|
|||
onChange({
|
||||
...value,
|
||||
routerSettings: { ...value.routerSettings, optional_pre_call_checks: checks },
|
||||
modifiedRouterSettings: markSettingModified(value.modifiedRouterSettings, "optional_pre_call_checks"),
|
||||
});
|
||||
};
|
||||
|
||||
const handleCacheControlInjectionPointsChange = (defaultLitellmParams: Record<string, unknown>) => {
|
||||
onChange({
|
||||
...value,
|
||||
routerSettings: { ...value.routerSettings, default_litellm_params: defaultLitellmParams },
|
||||
modifiedRouterSettings: markSettingModified(value.modifiedRouterSettings, "default_litellm_params"),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -88,6 +102,13 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
|
|||
|
||||
<div className="border-t border-gray-200" />
|
||||
|
||||
<CacheControlInjectionPointsSection
|
||||
value={value.routerSettings.default_litellm_params || {}}
|
||||
onChange={handleCacheControlInjectionPointsChange}
|
||||
/>
|
||||
|
||||
<div className="border-t border-gray-200" />
|
||||
|
||||
{/* Other Settings */}
|
||||
<ReliabilityRetriesSection routerSettings={value.routerSettings} routerFieldsMetadata={routerFieldsMetadata} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ describe("RouterSettings", () => {
|
|||
expect(screen.getByTestId("strategy-select")).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText("Default LiteLLM Params")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Cache Control Injection Points")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Cache Control Injection Points")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ describe("RouterSettings", () => {
|
|||
expect(NotificationsManager.success).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should round-trip default_litellm_params and optional_pre_call_checks unmodified on save", async () => {
|
||||
it("should preserve untouched default_litellm_params by omitting it from an unrelated save", async () => {
|
||||
vi.mocked(getCallbacksCall).mockResolvedValue({
|
||||
router_settings: {
|
||||
...mockCallbacksResponse.router_settings,
|
||||
|
|
@ -227,15 +227,76 @@ describe("RouterSettings", () => {
|
|||
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(setCallbacksCall).toHaveBeenCalledWith(
|
||||
"test-token",
|
||||
expect.objectContaining({
|
||||
router_settings: expect.not.objectContaining({
|
||||
default_litellm_params: expect.anything(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should save frontend-owned cache-control fields after an explicit edit", async () => {
|
||||
vi.mocked(getCallbacksCall).mockResolvedValue({
|
||||
router_settings: {
|
||||
...mockCallbacksResponse.router_settings,
|
||||
default_litellm_params: { timeout: 30 },
|
||||
},
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("switch", { name: "Cache Control Injection Points" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("switch", { name: "Cache Control Injection Points" }));
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(setCallbacksCall).toHaveBeenCalledWith(
|
||||
"test-token",
|
||||
expect.objectContaining({
|
||||
router_settings: expect.objectContaining({
|
||||
default_litellm_params: {
|
||||
cache_control_injection_points: [{ location: "message", role: "system", index: 2 }],
|
||||
timeout: 30,
|
||||
cache_control_injection_points: [{ location: "message" }],
|
||||
},
|
||||
optional_pre_call_checks: ["prompt_caching"],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it("should remove cache-control fields after they are explicitly disabled", async () => {
|
||||
vi.mocked(getCallbacksCall).mockResolvedValue({
|
||||
router_settings: {
|
||||
...mockCallbacksResponse.router_settings,
|
||||
default_litellm_params: {
|
||||
timeout: 30,
|
||||
cache_control_injection_points: [{ location: "message", index: -1 }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<RouterSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("cache-control-index-input-0")).toHaveValue("-1");
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("switch", { name: "Cache Control Injection Points" }));
|
||||
await user.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(setCallbacksCall).toHaveBeenCalledWith(
|
||||
"test-token",
|
||||
expect.objectContaining({
|
||||
router_settings: expect.objectContaining({
|
||||
default_litellm_params: { timeout: 30 },
|
||||
}),
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
|
|||
}
|
||||
|
||||
const router_settings = formValue.routerSettings;
|
||||
const modifiedRouterSettings = formValue.modifiedRouterSettings || [];
|
||||
|
||||
const numberKeys = new Set(["allowed_fails", "cooldown_time", "num_retries", "timeout", "retry_after"]);
|
||||
const jsonKeys = new Set(["model_group_alias"]);
|
||||
|
|
@ -131,6 +132,9 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
|
|||
if (tabOwnedKeys.has(key)) {
|
||||
return null;
|
||||
}
|
||||
if (key === "default_litellm_params" && !modifiedRouterSettings.includes(key)) {
|
||||
return null;
|
||||
}
|
||||
if (key !== "routing_strategy_args" && key !== "routing_strategy" && key !== "enable_tag_filtering") {
|
||||
const inputEl = document.querySelector(`input[name="${key}"]`) as HTMLInputElement | null;
|
||||
const parsed = parseInputValue(key, inputEl?.value, value);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue