diff --git a/ui/litellm-dashboard/src/components/add_model/ContextWindowEscalationConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ContextWindowEscalationConfig.tsx index 7271750aadc..c0a65076d20 100644 --- a/ui/litellm-dashboard/src/components/add_model/ContextWindowEscalationConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ContextWindowEscalationConfig.tsx @@ -8,13 +8,16 @@ const ContextWindowEscalationConfig: React.FC<{ onChange: (value: ComplexityRouterConfigValue) => void; }> = ({ value, onChange }) => { const enabled = value.enable_context_window_escalation ?? true; + // A number input renders Number("0.") as "0", so a decimal cannot be typed without a local draft. const [bufferDraft, setBufferDraft] = React.useState(null); - // min/max are inert on a text input, and a plain number input renders Number("0.") as "0" so a - // decimal cannot be typed. Hence the local draft plus an explicit clamp on commit. const commitBuffer = (raw: string) => { setBufferDraft(null); + if (raw.trim() === "") { + onChange({ ...value, context_window_escalation_buffer: undefined }); + return; + } const parsed = Number(raw); - if (raw.trim() === "" || !Number.isFinite(parsed)) return; + if (!Number.isFinite(parsed)) return; onChange({ ...value, context_window_escalation_buffer: Math.min(1, Math.max(0.01, parsed)) }); }; return ( diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx index e08952d5889..71b454dbb06 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx @@ -394,8 +394,6 @@ describe("AddAutoRouterTab", () => { }); }); - // A plain number input renders Number("1.5") fine but the clamp is ours: values above 1 must commit as 1, - // and an untouched buffer must stay out of the payload so the router tracks the backend default. it("clamps the context-window buffer to 1 and keeps an untouched buffer out of the payload", async () => { const user = userEvent.setup(); vi.mocked(getMissingTiersError).mockReturnValue(null); @@ -417,6 +415,29 @@ describe("AddAutoRouterTab", () => { expect(config).not.toHaveProperty("enable_context_window_escalation"); }); + it("clearing the buffer removes it from the payload so the router tracks the backend default", async () => { + const user = userEvent.setup(); + vi.mocked(getMissingTiersError).mockReturnValue(null); + + renderWithProviders(); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "ctx-clear-router"); + expandDetailedConfiguration(); + await user.click(screen.getByText("Advanced: Context Window Escalation")); + const buffer = await screen.findByLabelText("Window fit buffer"); + fireEvent.change(buffer, { target: { value: "0.8" } }); + fireEvent.blur(buffer, { target: { value: "0.8" } }); + fireEvent.change(buffer, { target: { value: "" } }); + fireEvent.blur(buffer, { target: { value: "" } }); + + await user.click(screen.getByRole("button", { name: /add auto router/i })); + + await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled()); + expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0].complexity_router_config).not.toHaveProperty( + "context_window_escalation_buffer", + ); + }); + // The scalar floor is the one scorer knob with no group dict behind it, so its wiring into the create // payload is only proven end to end. 0 is the case a truthy check would silently drop. it("carries a reasoning override floor of 0 through to the create payload", async () => { diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts index 159fdf957e4..feddaaa0eac 100644 --- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts +++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts @@ -59,8 +59,6 @@ describe("buildComplexityRouterConfig", () => { expect(config).toEqual(expected); }); - // False is the value a truthy guard would silently drop, and it is the whole point of the toggle: - // an untouched form tracks the backend default (enabled), an explicit false is a real opt-out. it("carries an explicit context-window escalation opt-out and buffer, false included", () => { const config = buildComplexityRouterConfig({ ...baseParams, diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts index 506411bb7da..f14d6279e32 100644 --- a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts +++ b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts @@ -573,8 +573,6 @@ describe("autorouter_presets", () => { expect(prefill.escalationKeywords).toEqual([]); }); - // The prefill mapping is a hand-written field list, so a new config key is silently dropped - // unless mapped; false is the value a `||` default would erase (the #38453 class). it("carries a preset's context-window escalation opt-out and buffer through the prefill", () => { const prefill = buildPresetPrefill( {