From af5af4d0d732adb3491a5c77d817e812fc48864e Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Mon, 3 Aug 2026 18:08:25 -0700 Subject: [PATCH] fix(ui): thread session_affinity and classifier context fields through preset selection handlePresetChange builds a fresh ComplexityRouterConfigValue from the preset's config on every selection; four fields added to that payload since the object literal was written (session_affinity, classifier_context_window_size, classifier_context_per_turn_chars, classifier_context_include_assistant_turns) were never added to it. Since setComplexityRouterConfig replaces the whole value rather than merging, applying a preset silently dropped all four to undefined regardless of what the preset specified or what the caller had set manually; for session_affinity specifically that resolved to false at submit time via the existing destructuring default, so both bundled presets (already false) masked it. Verified directly: toggling session affinity on and then applying a preset reverted the switch to off and submitted false, not the caller's prior choice. Thread all four fields through from the preset's config, matching how every other optional field here is already carried over. Extended the existing falsy-preset test to assert session_affinity survives when a preset sets it true, and added a dedicated regression for the reported flow: toggle on, then apply a preset, and confirm the preset's own value wins over the stale manual edit rather than being silently discarded. --- .../add_model/add_auto_router_tab.test.tsx | 34 +++++++++++++++++++ .../add_model/add_auto_router_tab.tsx | 4 +++ 2 files changed, 38 insertions(+) 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 892950600a5..ecc15453e84 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 @@ -79,6 +79,7 @@ const { FALSY_PRESET } = vi.hoisted(() => ({ match_threshold: 0, keyword_tier_rules: [{ keywords: ["foo"], tier: "SIMPLE" as const }], escalation_keywords: [], + session_affinity: true, }, }, })); @@ -380,6 +381,7 @@ describe("AddAutoRouterTab", () => { const payload = mockHandleAddAutoRouterSubmit.mock.calls.at(-1)?.[0]; expect(payload.complexity_router_config.match_threshold).toBe(0); expect(payload.complexity_router_config.escalation_keywords).toEqual([]); + expect(payload.complexity_router_config.session_affinity).toBe(true); }); it("offers no team selector to a proxy admin, who may create an unscoped router", () => { @@ -474,4 +476,36 @@ describe("AddAutoRouterTab", () => { session_affinity: true, }); }); + + // handlePresetChange fully replaces complexityRouterConfig, so a field missing from the object + // it builds isn't "carried forward" from a prior manual edit, it's silently dropped to whatever + // the destructuring default resolves to downstream. Turn affinity on via Custom, then apply a + // preset whose own value is off, and confirm the preset's off wins in both the UI and the + // payload, the same authoritative-replace behavior every other preset field already has. + it("applies the preset's own session affinity instead of a previously toggled-on value", async () => { + const user = userEvent.setup(); + mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS); + + renderWithProviders(); + + openTemplateDropdown(); + fireEvent.click(optionByLabel("Custom Configuration")!); + await user.click(screen.getByText("Advanced: Session Affinity")); + await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" })); + expect(screen.getByRole("switch", { name: "Pin a session to its first model" })).toBeChecked(); + + openTemplateDropdown(); + await waitFor(() => expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(false)); + fireEvent.click(optionByLabel("Anthropic Family")!); + + expect(screen.getByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked(); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-then-preset-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + + await waitFor(() => expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalled()); + expect(mockHandleAddAutoRouterSubmit.mock.calls.at(-1)?.[0].complexity_router_config).toMatchObject({ + session_affinity: false, + }); + }); }); diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx index 58718d8e8da..ea29e04d372 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.tsx @@ -171,6 +171,10 @@ const AddAutoRouterTab: React.FC = ({ tiers: config.tiers, classifier_type: config.classifier_type, classifier_llm_config: config.classifier_llm_config, + classifier_context_window_size: config.classifier_context_window_size, + classifier_context_per_turn_chars: config.classifier_context_per_turn_chars, + classifier_context_include_assistant_turns: config.classifier_context_include_assistant_turns, + session_affinity: config.session_affinity ?? DEFAULT_SESSION_AFFINITY, adaptive: config.adaptive, adaptive_weights: config.adaptive_weights, tier_distance_penalty: config.tier_distance_penalty,