From 6f36bee6bae42fa4a7708d77b898653ba7dff664 Mon Sep 17 00:00:00 2001 From: tin-berri Date: Mon, 10 Aug 2026 19:31:20 -0700 Subject: [PATCH] feat(ui): deployment affinity toggle for the auto-router (#36302) --- .../src/autorouter_presets.json | 6 +- .../add_model/ComplexityRouterConfig.test.tsx | 29 ++++++++ .../add_model/ComplexityRouterConfig.tsx | 24 +++++-- .../add_model/add_auto_router_tab.test.tsx | 44 +++++++++++- .../add_model/add_auto_router_tab.tsx | 2 + .../build_complexity_router_config.test.ts | 2 + .../build_complexity_router_config.ts | 4 ++ ...d_updated_complexity_router_config.test.ts | 25 +++++++ .../edit_auto_router_modal.test.ts | 2 + .../edit_auto_router_modal.test.tsx | 69 +++++++++++++++++-- .../edit_auto_router_modal.tsx | 7 ++ .../src/lib/autorouter_presets.test.ts | 8 +++ .../src/lib/autorouter_presets.ts | 2 + 13 files changed, 210 insertions(+), 14 deletions(-) diff --git a/ui/litellm-dashboard/src/autorouter_presets.json b/ui/litellm-dashboard/src/autorouter_presets.json index 58a087d4009..db46da08a3d 100644 --- a/ui/litellm-dashboard/src/autorouter_presets.json +++ b/ui/litellm-dashboard/src/autorouter_presets.json @@ -11,7 +11,8 @@ }, "classifier_type": "heuristic", "escalation_keywords": ["LITELLM ESCALATE"], - "session_affinity": false + "session_affinity": false, + "deployment_affinity": true } }, "openai_family": { @@ -26,7 +27,8 @@ }, "classifier_type": "heuristic", "escalation_keywords": ["LITELLM ESCALATE"], - "session_affinity": false + "session_affinity": false, + "deployment_affinity": true } } } diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx index 1e1bb5f9bf4..92dfeab8a7d 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx @@ -602,3 +602,32 @@ describe("ComplexityRouterConfig tier labels", () => { expect(screen.getByTitle("Deep")).toBeInTheDocument(); }); }); + +describe("ComplexityRouterConfig affinity panel", () => { + it("holds both affinity switches with their backend defaults", () => { + renderWithProviders(); + fireEvent.click(screen.getByText("Advanced: Affinity")); + + expect(screen.getByRole("switch", { name: "Pin a session to one deployment per model group" })).toBeChecked(); + expect(screen.getByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked(); + }); + + it("writes deployment_affinity through onChange without touching other keys", () => { + const onChange = vi.fn(); + renderWithProviders(); + fireEvent.click(screen.getByText("Advanced: Affinity")); + + fireEvent.click(screen.getByRole("switch", { name: "Pin a session to one deployment per model group" })); + + expect(onChange).toHaveBeenCalledWith({ ...defaultValue, deployment_affinity: false }); + }); + + it("renders a stored deployment_affinity=false as off", () => { + renderWithProviders( + , + ); + fireEvent.click(screen.getByText("Advanced: Affinity")); + + expect(screen.getByRole("switch", { name: "Pin a session to one deployment per model group" })).not.toBeChecked(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx index ba805c23c1f..39e6e97bf1d 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx @@ -15,6 +15,7 @@ export const DEFAULT_TIER_DISTANCE_PENALTY = 0.5; export const DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE = 3; export const DEFAULT_CLASSIFIER_CONTEXT_PER_TURN_CHARS = 200; export const DEFAULT_SESSION_AFFINITY = false; +export const DEFAULT_DEPLOYMENT_AFFINITY = true; export interface ComplexityTiers { SIMPLE: string[]; @@ -56,6 +57,7 @@ export interface ComplexityRouterConfigValue { classifier_context_include_assistant_turns?: boolean; classifier_fallback?: ClassifierFallback; session_affinity?: boolean; + deployment_affinity?: boolean; adaptive?: boolean; adaptive_weights?: AdaptiveRouterWeights; tier_distance_penalty?: number; @@ -277,14 +279,26 @@ const ComplexityRouterConfig: React.FC = ({ children: , }, { - key: "session-affinity", + key: "affinity", label: ( - Advanced: Session Affinity + Advanced: Affinity ), children: ( <> +
+ onChange({ ...value, deployment_affinity: deploymentAffinity })} + aria-label="Pin a session to one deployment per model group" + /> + Pin a session to one deployment per model group +
+ + Keeps a session on the same deployment within a group, so provider prompt caches stay warm. Turn off + to load-balance every turn. +
= ({ Pin a session to its first model
- Off by default: every turn is classified on its own merits and routed to the cheapest adequate tier. - Turn this on to reuse the model chosen on a session's first turn for every later turn, which - preserves provider prompt caches and avoids cross-model conversation-history errors, at the cost of - keeping the whole session on the first turn's tier. + Keeps a session on its first turn's model instead of re-classifying each turn. Also pins the + deployment. ), 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 6a5a1e0f159..7d42d85ccfb 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 @@ -273,7 +273,7 @@ describe("AddAutoRouterTab", () => { await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router"); expandDetailedConfiguration(); - await user.click(screen.getByText("Advanced: Session Affinity")); + await user.click(screen.getByText("Advanced: Affinity")); expect(await screen.findByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked(); await user.click(screen.getByRole("button", { name: /add auto router/i })); @@ -292,7 +292,7 @@ describe("AddAutoRouterTab", () => { await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router"); expandDetailedConfiguration(); - await user.click(screen.getByText("Advanced: Session Affinity")); + await user.click(screen.getByText("Advanced: Affinity")); await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" })); await user.click(screen.getByRole("button", { name: /add auto router/i })); @@ -303,6 +303,46 @@ describe("AddAutoRouterTab", () => { }); }); + it("defaults a new router to deployment affinity on, matching the backend field default", async () => { + const user = userEvent.setup(); + vi.mocked(getMissingTiersError).mockReturnValue(null); + + renderWithProviders(); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router"); + expandDetailedConfiguration(); + await user.click(screen.getByText("Advanced: Affinity")); + expect( + await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }), + ).toBeChecked(); + + 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).toMatchObject({ + deployment_affinity: true, + }); + }); + + it("carries deployment affinity turned off through to the create payload", async () => { + const user = userEvent.setup(); + vi.mocked(getMissingTiersError).mockReturnValue(null); + + renderWithProviders(); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router"); + expandDetailedConfiguration(); + await user.click(screen.getByText("Advanced: Affinity")); + await user.click(await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" })); + + 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).toMatchObject({ + deployment_affinity: false, + }); + }); + // Custom is the escape hatch, not the headline choice, so it's listed after every bundled preset // rather than first. it("lists Custom Configuration after the bundled presets", () => { 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 08041413d1f..7b6403f250c 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 @@ -15,6 +15,7 @@ import ComplexityRouterConfig, { ComplexityTiers, DEFAULT_ADAPTIVE_WEIGHTS, DEFAULT_SESSION_AFFINITY, + DEFAULT_DEPLOYMENT_AFFINITY, DEFAULT_TIER_DISTANCE_PENALTY, } from "./ComplexityRouterConfig"; import { KeywordTierRule } from "./KeywordTierRules"; @@ -290,6 +291,7 @@ const AddAutoRouterTab: React.FC = ({ classifierContextIncludeAssistantTurns: complexityRouterConfig.classifier_context_include_assistant_turns, classifierFallback: complexityRouterConfig.classifier_fallback, sessionAffinity: complexityRouterConfig.session_affinity ?? DEFAULT_SESSION_AFFINITY, + deploymentAffinity: complexityRouterConfig.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY, customTechnicalKeywords, keywordTierRules, semanticMatchingEnabled, 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 34f7354d412..1a2276f302d 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 @@ -26,6 +26,7 @@ const baseParams: BuildComplexityRouterConfigParams = { classifierContextIncludeAssistantTurns: undefined, classifierFallback: undefined, sessionAffinity: false, + deploymentAffinity: true, customTechnicalKeywords: [], keywordTierRules: [], semanticMatchingEnabled: false, @@ -46,6 +47,7 @@ describe("buildComplexityRouterConfig", () => { tiers, classifier_type: "heuristic", session_affinity: false, + deployment_affinity: true, escalation_keywords: ["LITELLM ESCALATE"], }); }); diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts index 9ed1b98db7b..503390aa238 100644 --- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts +++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts @@ -30,6 +30,7 @@ export interface BuildComplexityRouterConfigParams { classifierContextIncludeAssistantTurns: boolean | undefined; classifierFallback: ClassifierFallback | undefined; sessionAffinity: boolean; + deploymentAffinity: boolean; customTechnicalKeywords: string[]; keywordTierRules: KeywordTierRule[]; semanticMatchingEnabled: boolean; @@ -53,6 +54,7 @@ export interface ComplexityRouterConfigPayload { classifier_context_include_assistant_turns?: boolean; classifier_fallback?: ClassifierFallback; session_affinity: boolean; + deployment_affinity: boolean; custom_technical_keywords?: string[]; keyword_tier_rules?: { keywords: string[]; tier: KeywordTierRule["tier"] }[]; semantic_keyword_matching?: boolean; @@ -137,6 +139,7 @@ export const buildComplexityRouterConfig = ({ classifierContextIncludeAssistantTurns, classifierFallback, sessionAffinity, + deploymentAffinity, customTechnicalKeywords, keywordTierRules, semanticMatchingEnabled, @@ -173,6 +176,7 @@ export const buildComplexityRouterConfig = ({ classifier_context_include_assistant_turns: classifierContextIncludeAssistantTurns, }), session_affinity: sessionAffinity, + deployment_affinity: deploymentAffinity, ...(customTechnicalKeywords.length > 0 && { custom_technical_keywords: customTechnicalKeywords }), ...(cleanedKeywordTierRules.length > 0 && { keyword_tier_rules: cleanedKeywordTierRules }), escalation_keywords: cleanedEscalationKeywords, diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts index 30859b5baea..b56ec734aeb 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts +++ b/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts @@ -228,6 +228,31 @@ describe("buildUpdatedComplexityRouterConfig session affinity", () => { }); }); +describe("buildUpdatedComplexityRouterConfig deployment affinity", () => { + it("writes deployment_affinity=false when the toggle is off", () => { + const result = buildUpdatedComplexityRouterConfig(STORED, { ...FORM_VALUE, deployment_affinity: false }); + expect(result.deployment_affinity).toBe(false); + }); + + it("writes deployment_affinity=true when the toggle is on", () => { + const result = buildUpdatedComplexityRouterConfig(STORED, { ...FORM_VALUE, deployment_affinity: true }); + expect(result.deployment_affinity).toBe(true); + }); + + it("re-asserts the backend's on-by-default when the form value is absent, rather than dropping the key", () => { + const result = buildUpdatedComplexityRouterConfig({ ...STORED, deployment_affinity: false }, FORM_VALUE); + expect(result.deployment_affinity).toBe(true); + }); + + it("stops a stored deployment_affinity=false from surviving a save that turned the toggle back on", () => { + const result = buildUpdatedComplexityRouterConfig( + { ...STORED, deployment_affinity: false }, + { ...FORM_VALUE, deployment_affinity: true }, + ); + expect(result.deployment_affinity).toBe(true); + }); +}); + describe("buildUpdatedComplexityRouterConfig tier labels", () => { const RENAMED = { ...STORED, tier_labels: { SIMPLE: "Cheap", REASONING: "Deep" } }; diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.ts b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.ts index eb5bb46f0e8..027e01a9351 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.ts +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.ts @@ -48,6 +48,7 @@ const expectedClassifiedTierConfig = { embedding_model: "voyage-4-large", match_threshold: 0.65, session_affinity: false, + deployment_affinity: true, adaptive: true, adaptive_weights: { quality: 0.4, cost: 0.6 }, adaptive_eligible: "classified_tier", @@ -68,6 +69,7 @@ const expectedAdaptiveDisabledConfig = { embedding_model: "voyage-4-large", match_threshold: 0.65, session_affinity: false, + deployment_affinity: true, }; describe("buildUpdatedComplexityRouterConfig", () => { diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx index 29101b38e24..74dea8cc2ed 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.test.tsx @@ -365,7 +365,7 @@ describe("EditAutoRouterModal session affinity", () => { const user = userEvent.setup(); renderWithStoredConfig(STORED_CONFIG); - await user.click(await screen.findByText("Advanced: Session Affinity")); + await user.click(await screen.findByText("Advanced: Affinity")); expect(await screen.findByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked(); await user.click(screen.getByRole("button", { name: /save changes/i })); @@ -378,7 +378,7 @@ describe("EditAutoRouterModal session affinity", () => { const user = userEvent.setup(); renderWithStoredConfig({ ...STORED_CONFIG, session_affinity: true }); - await user.click(await screen.findByText("Advanced: Session Affinity")); + await user.click(await screen.findByText("Advanced: Affinity")); expect(await screen.findByRole("switch", { name: "Pin a session to its first model" })).toBeChecked(); await user.click(screen.getByRole("button", { name: /save changes/i })); @@ -391,7 +391,7 @@ describe("EditAutoRouterModal session affinity", () => { const user = userEvent.setup(); renderWithStoredConfig(STORED_CONFIG); - await user.click(await screen.findByText("Advanced: Session Affinity")); + await user.click(await screen.findByText("Advanced: Affinity")); await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" })); await user.click(screen.getByRole("button", { name: /save changes/i })); @@ -404,7 +404,7 @@ describe("EditAutoRouterModal session affinity", () => { const user = userEvent.setup(); renderWithStoredConfig({ ...STORED_CONFIG, session_affinity: true }); - await user.click(await screen.findByText("Advanced: Session Affinity")); + await user.click(await screen.findByText("Advanced: Affinity")); await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" })); await user.click(screen.getByRole("button", { name: /save changes/i })); @@ -414,6 +414,67 @@ describe("EditAutoRouterModal session affinity", () => { }); }); +describe("EditAutoRouterModal deployment affinity", () => { + beforeEach(() => { + modelPatchUpdateCall.mockClear(); + }); + + const renderWithStoredConfig = (complexity_router_config: Record) => + renderWithProviders( + , + ); + + it("shows a stored config with no deployment_affinity key as on, matching the backend default", async () => { + const user = userEvent.setup(); + renderWithStoredConfig(STORED_CONFIG); + + await user.click(await screen.findByText("Advanced: Affinity")); + expect( + await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }), + ).toBeChecked(); + + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled()); + expect(savedConfig().deployment_affinity).toBe(true); + }); + + it("shows a stored deployment_affinity=false as off and preserves it through an untouched save", async () => { + const user = userEvent.setup(); + renderWithStoredConfig({ ...STORED_CONFIG, deployment_affinity: false }); + + await user.click(await screen.findByText("Advanced: Affinity")); + expect( + await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }), + ).not.toBeChecked(); + + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled()); + expect(savedConfig().deployment_affinity).toBe(false); + }); + + it("persists turning deployment affinity off", async () => { + const user = userEvent.setup(); + renderWithStoredConfig(STORED_CONFIG); + + await user.click(await screen.findByText("Advanced: Affinity")); + await user.click(await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" })); + + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled()); + expect(savedConfig().deployment_affinity).toBe(false); + }); +}); + describe("EditAutoRouterModal custom classifier prompt and fallback", () => { beforeEach(() => { modelPatchUpdateCall.mockClear(); diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx index aba0ee9f58a..6818d3b850c 100644 --- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx +++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx @@ -21,6 +21,7 @@ import ComplexityRouterConfig, { ComplexityRouterConfigValue, DEFAULT_ADAPTIVE_WEIGHTS, DEFAULT_SESSION_AFFINITY, + DEFAULT_DEPLOYMENT_AFFINITY, DEFAULT_TIER_DISTANCE_PENALTY, } from "../add_model/ComplexityRouterConfig"; import NotificationsManager from "../molecules/notifications_manager"; @@ -55,6 +56,7 @@ const MANAGED_COMPLEXITY_ROUTER_KEYS = new Set([ "classifier_context_include_assistant_turns", "classifier_fallback", "session_affinity", + "deployment_affinity", "adaptive", "adaptive_weights", "tier_distance_penalty", @@ -127,6 +129,7 @@ export const buildUpdatedComplexityRouterConfig = ( classifier_context_include_assistant_turns: value.classifier_context_include_assistant_turns, }), session_affinity: value.session_affinity ?? DEFAULT_SESSION_AFFINITY, + deployment_affinity: value.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY, ...(customTechnicalKeywords && customTechnicalKeywords.length > 0 && { custom_technical_keywords: customTechnicalKeywords, @@ -263,6 +266,10 @@ const EditAutoRouterModal: React.FC = ({ typeof parsedConfig.session_affinity === "boolean" ? parsedConfig.session_affinity : DEFAULT_SESSION_AFFINITY, + deployment_affinity: + typeof parsedConfig.deployment_affinity === "boolean" + ? parsedConfig.deployment_affinity + : DEFAULT_DEPLOYMENT_AFFINITY, adaptive: parsedConfig.adaptive || false, adaptive_weights: parsedConfig.adaptive_weights, tier_distance_penalty: parsedConfig.tier_distance_penalty, diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts index fca8420966f..23ae905d9db 100644 --- a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts +++ b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts @@ -111,6 +111,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: [presetModel], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, }; expect(getMissingModels(config, availability)).toEqual([]); expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual([group]); @@ -153,6 +154,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: ["claude-opus-5"], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, }; expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual(["a-group"]); }); @@ -166,6 +168,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: ["claude-opus-5"], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, }; expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-opus-5"]); }); @@ -198,6 +201,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: [presetModel], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, }); it("resolves a preset model to a group expanded from a wildcard deployment", () => { @@ -440,6 +444,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, match_threshold: 0, escalation_keywords: [], }; @@ -454,6 +459,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic", session_affinity: false, + deployment_affinity: true, }, groupsOnly(["gpt-5-nano"]), ); @@ -469,6 +475,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, }; const labeled = buildPresetPrefill( { ...base, tier_labels: { SIMPLE: "Cheap", REASONING: "Deep" } }, @@ -483,6 +490,7 @@ describe("autorouter_presets", () => { tiers: { SIMPLE: ["claude-sonnet-4-5"], MEDIUM: [], COMPLEX: [], REASONING: [] }, classifier_type: "heuristic" as const, session_affinity: false, + deployment_affinity: true, }; const prefill = buildPresetPrefill(config, groupsOnly(["claude-sonnet-4.5"])); expect(prefill.complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-sonnet-4.5"]); diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.ts index ae3f30c90f5..5caff6fe714 100644 --- a/ui/litellm-dashboard/src/lib/autorouter_presets.ts +++ b/ui/litellm-dashboard/src/lib/autorouter_presets.ts @@ -8,6 +8,7 @@ import { ClassifierType, ClassifierLLMConfig, DEFAULT_SESSION_AFFINITY, + DEFAULT_DEPLOYMENT_AFFINITY, } from "@/components/add_model/ComplexityRouterConfig"; import { KeywordTierRule } from "@/components/add_model/KeywordTierRules"; import { hydrateKeywordTierRules } from "@/components/add_model/complexity_router_keywords"; @@ -260,6 +261,7 @@ export const buildPresetPrefill = ( 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, + deployment_affinity: config.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY, adaptive: config.adaptive, adaptive_weights: config.adaptive_weights, tier_distance_penalty: config.tier_distance_penalty,