fix(ui): repair router settings controls

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Krrish Dholakia 2026-07-14 13:19:50 +00:00
parent 221183b2f4
commit e1e233f88a
12 changed files with 236 additions and 282 deletions

View file

@ -224,36 +224,9 @@ ROUTER_SETTINGS_FIELDS: List[RouterSettingsField] = [
field_name="default_litellm_params",
field_type="Dictionary",
field_value=None,
field_description=(
"Default parameters for Router.chat.completion.create. E.g. set "
"cache_control_injection_points here to enable Anthropic/Bedrock "
"prompt caching for every model on this proxy."
),
field_description="Default parameters for Router.chat.completion.create",
field_default=None,
ui_field_name="Default LiteLLM Params",
link="https://docs.litellm.ai/docs/tutorials/claude_code_prompt_cache_routing",
),
RouterSettingsField(
field_name="optional_pre_call_checks",
field_type="List",
field_value=None,
field_description=(
"Extra checks the router runs before picking a deployment. Add "
"'prompt_caching' to route repeat requests back to the deployment "
"that cached the prompt."
),
field_default=[],
options=[
"prompt_caching",
"router_budget_limiting",
"responses_api_deployment_check",
"deployment_affinity",
"session_affinity",
"enforce_model_rate_limits",
"encrypted_content_affinity",
],
ui_field_name="Optional Pre-call Checks",
link="https://docs.litellm.ai/docs/tutorials/claude_code_prompt_cache_routing",
),
RouterSettingsField(
field_name="set_verbose",

View file

@ -78,46 +78,6 @@ class TestRouterSettingsEndpoints:
assert isinstance(routing_strategy_field["options"], list)
assert len(routing_strategy_field["options"]) > 0
@pytest.mark.asyncio
async def test_get_router_fields_includes_optional_pre_call_checks(self):
"""
Regression test: `optional_pre_call_checks` (e.g. "prompt_caching", used for
Claude Code prompt cache routing) must be exposed as a configurable field so
the Admin UI can render and save it, not just `default_litellm_params`.
"""
response = client.get(
"/router/fields", headers={"Authorization": "Bearer sk-1234"}
)
assert response.status_code == 200
fields = response.json()["fields"]
field = next(
(f for f in fields if f["field_name"] == "optional_pre_call_checks"), None
)
assert field is not None
assert "prompt_caching" in field["options"]
@pytest.mark.asyncio
async def test_get_router_fields_excludes_unimplemented_forward_client_headers_check(self):
"""
Regression test: "forward_client_headers_by_model_group" is a literal in the
OptionalPreCallChecks type union, but Router.add_optional_pre_call_checks has
no handler for it - selecting it from the Admin UI's multi-select would save
successfully and show as enabled while doing nothing. It must not be offered
as an option until it's actually implemented.
"""
response = client.get(
"/router/fields", headers={"Authorization": "Bearer sk-1234"}
)
assert response.status_code == 200
fields = response.json()["fields"]
field = next(
(f for f in fields if f["field_name"] == "optional_pre_call_checks"), None
)
assert field is not None
assert "forward_client_headers_by_model_group" not in field["options"]
@pytest.mark.asyncio
async def test_get_router_settings_includes_routing_groups_from_live_router(
self, monkeypatch

View file

@ -1,24 +1,18 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
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 }}
routerFieldsMetadata={{}}
onChange={vi.fn()}
/>,
);
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 }} routerFieldsMetadata={{}} onChange={vi.fn()} />);
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={vi.fn()} />);
expect(screen.queryByTestId("cache-control-location-select-0")).not.toBeInTheDocument();
});
@ -26,17 +20,29 @@ describe("DefaultLitellmParamsSection", () => {
render(
<DefaultLitellmParamsSection
value={{ cache_control_injection_points: [{ location: "message", role: "system" }] }}
routerFieldsMetadata={{}}
onChange={vi.fn()}
/>,
);
expect(screen.getByTestId("cache-control-location-select-0")).toBeInTheDocument();
});
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 }} routerFieldsMetadata={{}} onChange={onChange} />);
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={onChange} />);
await user.click(screen.getByRole("switch"));
@ -49,7 +55,6 @@ describe("DefaultLitellmParamsSection", () => {
render(
<DefaultLitellmParamsSection
value={{ timeout: 30, cache_control_injection_points: [{ location: "message", role: "system" }] }}
routerFieldsMetadata={{}}
onChange={onChange}
/>,
);
@ -65,7 +70,6 @@ describe("DefaultLitellmParamsSection", () => {
render(
<DefaultLitellmParamsSection
value={{ timeout: 30, cache_control_injection_points: [{ location: "message" }] }}
routerFieldsMetadata={{}}
onChange={onChange}
/>,
);
@ -81,7 +85,7 @@ describe("DefaultLitellmParamsSection", () => {
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} />);
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={onChange} />);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
await user.clear(textarea);
@ -95,7 +99,7 @@ describe("DefaultLitellmParamsSection", () => {
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} />);
render(<DefaultLitellmParamsSection value={{ timeout: 30 }} onChange={onChange} />);
const textarea = screen.getByRole("textbox") as HTMLTextAreaElement;
await user.clear(textarea);
@ -108,4 +112,16 @@ describe("DefaultLitellmParamsSection", () => {
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

@ -6,59 +6,104 @@ import CacheControlInjectionPointsEditor, {
import NotificationsManager from "../molecules/notifications_manager";
interface DefaultLitellmParamsSectionProps {
value: { [key: string]: any };
routerFieldsMetadata: { [key: string]: any };
onChange: (value: { [key: string]: any }) => void;
value: Record<string, unknown>;
onChange: (value: Record<string, unknown>) => void;
}
const DefaultLitellmParamsSection: React.FC<DefaultLitellmParamsSectionProps> = ({
value,
routerFieldsMetadata,
onChange,
}) => {
const meta = routerFieldsMetadata["default_litellm_params"];
const { cache_control_injection_points, ...otherParams } = value || {};
type ParsedDefaultParams = { status: "valid"; value: Record<string, unknown> } | { status: "invalid"; message: string };
const CACHE_CONTROL_ROLES = ["user", "system", "assistant"] as const;
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 isCacheControlInjectionPoint = (value: unknown): value is CacheControlInjectionPoint => {
if (typeof value !== "object" || value === null) {
return false;
}
if (!("location" in value) || value.location !== "message") {
return false;
}
const role = "role" in value ? value.role : undefined;
const index = "index" in value ? value.index : undefined;
const hasValidRole = role === undefined || CACHE_CONTROL_ROLES.some((validRole) => validRole === role);
const hasValidIndex = index === undefined || (typeof index === "number" && Number.isInteger(index));
return hasValidRole && hasValidIndex;
};
const DefaultLitellmParamsSection: React.FC<DefaultLitellmParamsSectionProps> = ({ value, onChange }) => {
const cacheControlInjectionPoints = React.useMemo(
() =>
Array.isArray(value.cache_control_injection_points) &&
value.cache_control_injection_points.every(isCacheControlInjectionPoint)
? value.cache_control_injection_points
: undefined,
[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 [showCacheControl, setShowCacheControl] = React.useState((cache_control_injection_points?.length ?? 0) > 0);
const [hasInvalidJson, setHasInvalidJson] = React.useState(false);
const parseOtherParams = (): { [key: string]: any } => {
try {
return JSON.parse(otherParamsText || "{}");
} catch {
return otherParams;
}
};
const showCacheControl = (cacheControlInjectionPoints?.length ?? 0) > 0;
const handleOtherParamsBlur = () => {
try {
const parsed = JSON.parse(otherParamsText || "{}");
setHasInvalidJson(false);
onChange({ ...parsed, cache_control_injection_points });
} catch (error) {
const parsed = parseDefaultParams(otherParamsText);
if (parsed.status === "invalid") {
setHasInvalidJson(true);
NotificationsManager.warning(`Default LiteLLM Params is not valid JSON, change not saved: ${error}`);
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 base = parseOtherParams();
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) => {
setShowCacheControl(checked);
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">
{meta?.ui_field_name || "default_litellm_params"}
</span>
<p className="text-xs text-gray-500 mt-0.5 mb-2">{meta?.field_description || ""}</p>
<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) => {
@ -87,7 +132,7 @@ const DefaultLitellmParamsSection: React.FC<DefaultLitellmParamsSectionProps> =
{showCacheControl && (
<div className="ml-6 pl-4 border-l-2 border-gray-200">
<CacheControlInjectionPointsEditor
value={cache_control_injection_points || [{ location: "message" }]}
value={cacheControlInjectionPoints || [{ location: "message" }]}
onChange={handleCacheControlPointsChange}
/>
</div>

View file

@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import OptionalPreCallChecksSelector from "./OptionalPreCallChecksSelector";
import OptionalPreCallChecksSelector, { OPTIONAL_PRE_CALL_CHECK_OPTIONS } from "./OptionalPreCallChecksSelector";
vi.mock("antd", async (importOriginal) => {
const actual = await importOriginal<typeof import("antd")>();
@ -24,57 +24,29 @@ vi.mock("antd", async (importOriginal) => {
};
});
const baseMetadata = {
optional_pre_call_checks: {
ui_field_name: "Optional Pre-call Checks",
field_description: "Extra checks the router runs before picking a deployment",
link: null,
},
};
const options = ["prompt_caching", "router_budget_limiting", "session_affinity"];
describe("OptionalPreCallChecksSelector", () => {
it("should render one option per entry in options", () => {
render(<OptionalPreCallChecksSelector value={[]} options={options} routerFieldsMetadata={{}} onChange={vi.fn()} />);
it("should render the frontend-defined options", () => {
render(<OptionalPreCallChecksSelector value={[]} onChange={vi.fn()} />);
const select = screen.getByTestId("optional-pre-call-checks-select") as HTMLSelectElement;
expect(Array.from(select.options).map((o) => o.value)).toEqual(options);
expect(Array.from(select.options).map((o) => o.value)).toEqual(OPTIONAL_PRE_CALL_CHECK_OPTIONS);
});
it("should display default label when no metadata is provided", () => {
render(<OptionalPreCallChecksSelector value={[]} options={options} routerFieldsMetadata={{}} onChange={vi.fn()} />);
it("should display the frontend-defined label and description", () => {
render(<OptionalPreCallChecksSelector value={[]} onChange={vi.fn()} />);
expect(screen.getByText("Optional Pre-call Checks")).toBeInTheDocument();
expect(screen.getByText(/extra checks the router runs before picking a deployment/i)).toBeInTheDocument();
});
it("should display the label and description from metadata when provided", () => {
render(
<OptionalPreCallChecksSelector
value={[]}
options={options}
routerFieldsMetadata={baseMetadata}
onChange={vi.fn()}
/>,
);
expect(screen.getByText("Extra checks the router runs before picking a deployment")).toBeInTheDocument();
});
it("should render a Learn more link when metadata provides one", () => {
const metadata = {
optional_pre_call_checks: { ...baseMetadata.optional_pre_call_checks, link: "https://docs.example.com/checks" },
};
render(
<OptionalPreCallChecksSelector value={[]} options={options} routerFieldsMetadata={metadata} onChange={vi.fn()} />,
);
it("should render the prompt caching documentation link", () => {
render(<OptionalPreCallChecksSelector value={[]} onChange={vi.fn()} />);
const link = screen.getByRole("link", { name: /learn more/i });
expect(link).toHaveAttribute("href", "https://docs.example.com/checks");
expect(link).toHaveAttribute("href", "https://docs.litellm.ai/docs/tutorials/claude_code_prompt_cache_routing");
});
it("should call onChange with the selected checks", async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(
<OptionalPreCallChecksSelector value={[]} options={options} routerFieldsMetadata={{}} onChange={onChange} />,
);
render(<OptionalPreCallChecksSelector value={[]} onChange={onChange} />);
await user.selectOptions(screen.getByTestId("optional-pre-call-checks-select"), "prompt_caching");
@ -82,14 +54,7 @@ describe("OptionalPreCallChecksSelector", () => {
});
it("should reflect an already-selected value", () => {
render(
<OptionalPreCallChecksSelector
value={["router_budget_limiting"]}
options={options}
routerFieldsMetadata={{}}
onChange={vi.fn()}
/>,
);
render(<OptionalPreCallChecksSelector value={["router_budget_limiting"]} onChange={vi.fn()} />);
const select = screen.getByTestId("optional-pre-call-checks-select") as HTMLSelectElement;
const selected = Array.from(select.selectedOptions).map((o) => o.value);
expect(selected).toEqual(["router_budget_limiting"]);

View file

@ -1,48 +1,43 @@
import React from "react";
import { Select } from "antd";
export const OPTIONAL_PRE_CALL_CHECK_OPTIONS = [
"prompt_caching",
"router_budget_limiting",
"responses_api_deployment_check",
"deployment_affinity",
"session_affinity",
"enforce_model_rate_limits",
"encrypted_content_affinity",
] as const;
interface OptionalPreCallChecksSelectorProps {
value: string[];
options: string[];
routerFieldsMetadata: { [key: string]: any };
onChange: (value: string[]) => void;
}
const OptionalPreCallChecksSelector: React.FC<OptionalPreCallChecksSelectorProps> = ({
value,
options,
routerFieldsMetadata,
onChange,
}) => {
const meta = routerFieldsMetadata["optional_pre_call_checks"];
const OptionalPreCallChecksSelector: React.FC<OptionalPreCallChecksSelectorProps> = ({ value, onChange }) => {
return (
<div className="space-y-2 max-w-3xl">
<label className="block">
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">
{meta?.ui_field_name || "Optional Pre-call Checks"}
</span>
<span className="text-xs font-medium text-gray-700 uppercase tracking-wide">Optional Pre-call Checks</span>
<p className="text-xs text-gray-500 mt-0.5 mb-2">
{meta?.field_description || ""}
{meta?.link && (
<>
{" "}
<a
href={meta.link}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 underline"
>
Learn more
</a>
</>
)}
Extra checks the router runs before picking a deployment. Add &apos;prompt_caching&apos; to route repeat
requests back to the deployment that cached the prompt.{" "}
<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>
<Select
mode="multiple"
value={value}
onChange={onChange}
options={options.map((option) => ({ value: option, label: option }))}
options={OPTIONAL_PRE_CALL_CHECK_OPTIONS.map((option) => ({ value: option, label: option }))}
placeholder="No pre-call checks enabled"
className="w-full"
data-testid="optional-pre-call-checks-select"

View file

@ -119,7 +119,7 @@ describe("RouterSettingsForm", () => {
const user = userEvent.setup();
render(<RouterSettingsForm {...baseProps} onChange={onChange} />);
await user.click(screen.getByRole("switch"));
await user.click(screen.getAllByRole("switch")[0]);
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ enableTagFiltering: true }));
});
@ -129,26 +129,13 @@ describe("RouterSettingsForm", () => {
expect(screen.getByText("Reliability & Retries")).toBeInTheDocument();
});
it("should not show the optional pre-call checks selector when no options are known", () => {
it("should show frontend-defined optional pre-call checks without backend field metadata", () => {
render(<RouterSettingsForm {...baseProps} />);
expect(screen.queryByTestId("optional-pre-call-checks-select")).not.toBeInTheDocument();
});
it("should show the optional pre-call checks selector populated from field metadata options", () => {
const props = {
...baseProps,
routerFieldsMetadata: {
optional_pre_call_checks: {
ui_field_name: "Optional Pre-call Checks",
options: ["prompt_caching", "router_budget_limiting"],
},
},
};
render(<RouterSettingsForm {...props} />);
const select = screen.getByTestId("optional-pre-call-checks-select") as HTMLSelectElement;
const optionValues = Array.from(select.options).map((o) => o.value);
expect(optionValues).toEqual(["prompt_caching", "router_budget_limiting"]);
expect(optionValues).toContain("prompt_caching");
expect(optionValues).toContain("router_budget_limiting");
});
it("should call onChange with the updated optional_pre_call_checks when the selector changes", async () => {
@ -157,9 +144,6 @@ describe("RouterSettingsForm", () => {
const props = {
...baseProps,
onChange,
routerFieldsMetadata: {
optional_pre_call_checks: { ui_field_name: "Optional Pre-call Checks", options: ["prompt_caching"] },
},
};
render(<RouterSettingsForm {...props} />);
@ -172,17 +156,8 @@ describe("RouterSettingsForm", () => {
);
});
it("should not show the Default LiteLLM Params section before router_settings has loaded", () => {
it("should show frontend-defined Default LiteLLM Params without backend field metadata", () => {
render(<RouterSettingsForm {...baseProps} />);
expect(screen.queryByText("Cache Control Injection Points")).not.toBeInTheDocument();
});
it("should show the Default LiteLLM Params section once router_settings has loaded", () => {
const props = {
...baseProps,
value: { ...defaultValue, routerSettings: { default_litellm_params: { timeout: 30 } } },
};
render(<RouterSettingsForm {...props} />);
expect(screen.getByText("Cache Control Injection Points")).toBeInTheDocument();
});

View file

@ -48,15 +48,13 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
});
};
const handleDefaultLitellmParamsChange = (params: { [key: string]: any }) => {
const handleDefaultLitellmParamsChange = (params: Record<string, unknown>) => {
onChange({
...value,
routerSettings: { ...value.routerSettings, default_litellm_params: params },
});
};
const optionalPreCallCheckOptions: string[] = routerFieldsMetadata["optional_pre_call_checks"]?.options || [];
return (
<div className="w-full space-y-8 py-2">
{/* Routing Settings Section */}
@ -85,14 +83,10 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
/>
{/* Optional Pre-call Checks */}
{optionalPreCallCheckOptions.length > 0 && (
<OptionalPreCallChecksSelector
value={value.routerSettings.optional_pre_call_checks || []}
options={optionalPreCallCheckOptions}
routerFieldsMetadata={routerFieldsMetadata}
onChange={handleOptionalPreCallChecksChange}
/>
)}
<OptionalPreCallChecksSelector
value={value.routerSettings.optional_pre_call_checks || []}
onChange={handleOptionalPreCallChecksChange}
/>
</div>
{/* Divider */}
@ -104,16 +98,12 @@ const RouterSettingsForm: React.FC<RouterSettingsFormProps> = ({
)}
{/* Default LiteLLM Params */}
{"default_litellm_params" in value.routerSettings && (
<>
<DefaultLitellmParamsSection
value={value.routerSettings.default_litellm_params || {}}
routerFieldsMetadata={routerFieldsMetadata}
onChange={handleDefaultLitellmParamsChange}
/>
<div className="border-t border-gray-200" />
</>
)}
<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 */}
<ReliabilityRetriesSection routerSettings={value.routerSettings} routerFieldsMetadata={routerFieldsMetadata} />

View file

@ -210,7 +210,9 @@ describe("RouterSettings", () => {
vi.mocked(getCallbacksCall).mockResolvedValue({
router_settings: {
...mockCallbacksResponse.router_settings,
default_litellm_params: { cache_control_injection_points: [{ location: "message", role: "system" }] },
default_litellm_params: {
cache_control_injection_points: [{ location: "message", role: "system", index: 2 }],
},
optional_pre_call_checks: ["prompt_caching"],
},
});
@ -228,7 +230,9 @@ describe("RouterSettings", () => {
"test-token",
expect.objectContaining({
router_settings: expect.objectContaining({
default_litellm_params: { cache_control_injection_points: [{ location: "message", role: "system" }] },
default_litellm_params: {
cache_control_injection_points: [{ location: "message", role: "system", index: 2 }],
},
optional_pre_call_checks: ["prompt_caching"],
}),
}),
@ -243,19 +247,6 @@ describe("RouterSettings", () => {
optional_pre_call_checks: [],
},
});
vi.mocked(getRouterSettingsCall).mockResolvedValue({
...mockRouterSettingsResponse,
fields: [
...mockRouterSettingsResponse.fields,
{
field_name: "optional_pre_call_checks",
ui_field_name: "Optional Pre-call Checks",
field_description: "Extra checks the router runs before picking a deployment",
options: ["prompt_caching", "router_budget_limiting"],
link: null,
},
],
});
const user = userEvent.setup();
renderWithProviders(<RouterSettings {...defaultProps} />);

View file

@ -50,7 +50,6 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
fieldsMap[field.field_name] = {
ui_field_name: field.ui_field_name,
field_description: field.field_description,
field_type: field.field_type,
options: field.options,
link: field.link,
};

View file

@ -1,3 +1,4 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
@ -52,10 +53,44 @@ describe("CacheControlInjectionPointsEditor", () => {
/>,
);
const removeIcons = document.querySelectorAll(".anticon-minus-circle");
expect(removeIcons).toHaveLength(2);
await user.click(removeIcons[0] as HTMLElement);
const removeButtons = screen.getAllByRole("button", { name: /remove injection point/i });
expect(removeButtons).toHaveLength(2);
await user.click(removeButtons[0]);
expect(onChange).toHaveBeenCalledWith([{ location: "message", role: "user" }]);
});
it("should accept an integer index", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const ControlledEditor = () => {
const [points, setPoints] = React.useState([{ location: "message" as const }]);
const handleChange = (updatedPoints: typeof points) => {
setPoints(updatedPoints);
onChange(updatedPoints);
};
return <CacheControlInjectionPointsEditor value={points} onChange={handleChange} />;
};
render(<ControlledEditor />);
const indexInput = screen.getByTestId("cache-control-index-input-0");
await user.type(indexInput, "-1");
expect(indexInput).toHaveValue("-1");
expect(onChange).toHaveBeenLastCalledWith([{ location: "message", index: -1 }]);
});
it("should increment the index with the stepper", async () => {
const onChange = vi.fn();
const user = userEvent.setup();
render(<CacheControlInjectionPointsEditor value={[{ location: "message", index: 0 }]} onChange={onChange} />);
const incrementButton = document.querySelector(".ant-input-number-handler-up");
expect(incrementButton).not.toBeNull();
await user.click(incrementButton as HTMLElement);
expect(onChange).toHaveBeenLastCalledWith([{ location: "message", index: 1 }]);
});
});

View file

@ -1,7 +1,8 @@
import React from "react";
import { Select } from "antd";
import { Button, Flex, InputNumber, Select, Typography } from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import NumericalInput from "./numerical_input";
const { Text } = Typography;
export interface CacheControlInjectionPoint {
location: "message";
@ -24,9 +25,11 @@ const CacheControlInjectionPointsEditor: React.FC<CacheControlInjectionPointsEdi
return (
<>
{points.map((point, index) => (
<div key={index} className="flex items-center mb-4 gap-4">
<div style={{ width: "180px" }}>
<span className="text-xs text-gray-500">Type</span>
<Flex key={index} align="end" gap="middle" wrap className="mb-4">
<div className="flex-1 min-w-40">
<Text type="secondary" className="block text-xs mb-1">
Type
</Text>
<Select
disabled
value="message"
@ -36,8 +39,10 @@ const CacheControlInjectionPointsEditor: React.FC<CacheControlInjectionPointsEdi
/>
</div>
<div style={{ width: "180px" }}>
<span className="text-xs text-gray-500">Role</span>
<div className="flex-1 min-w-40">
<Text type="secondary" className="block text-xs mb-1">
Role
</Text>
<Select
placeholder="Select a role"
allowClear
@ -53,36 +58,41 @@ const CacheControlInjectionPointsEditor: React.FC<CacheControlInjectionPointsEdi
/>
</div>
<div style={{ width: "180px" }}>
<span className="text-xs text-gray-500">Index</span>
<NumericalInput
type="number"
<div className="flex-1 min-w-40">
<Text type="secondary" className="block text-xs mb-1">
Index
</Text>
<InputNumber
placeholder="Optional"
step={1}
precision={0}
value={point.index}
onChange={(newIndex: string) =>
updatePoint(index, { index: newIndex === "" ? undefined : Number(newIndex) })
}
onChange={(newIndex) => updatePoint(index, { index: newIndex ?? undefined })}
className="w-full"
data-testid={`cache-control-index-input-${index}`}
/>
</div>
{points.length > 1 && (
<MinusCircleOutlined
className="text-red-500 cursor-pointer text-lg ml-12"
<Button
type="text"
danger
aria-label={`Remove injection point ${index + 1}`}
icon={<MinusCircleOutlined />}
onClick={() => onChange(points.filter((_, i) => i !== index))}
/>
)}
</div>
</Flex>
))}
<button
type="button"
className="flex items-center justify-center w-full border border-dashed border-gray-300 py-2 px-4 text-gray-600 hover:text-blue-600 hover:border-blue-300 transition-all rounded"
<Button
type="dashed"
block
icon={<PlusOutlined />}
onClick={() => onChange([...points, { location: "message" as const }])}
>
<PlusOutlined className="mr-2" />
Add Injection Point
</button>
</Button>
</>
);
};