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:
Krrish Dholakia 2026-07-14 17:12:30 +00:00
parent 7377f6d021
commit d418752267
6 changed files with 322 additions and 7 deletions

View file

@ -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();
});
});

View file

@ -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;

View file

@ -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"],
}),
);
});
});

View file

@ -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>

View file

@ -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 },
}),
}),
),

View file

@ -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);