From 340d30867e005d749f2be4ffbe39b9cf501e1a1d Mon Sep 17 00:00:00 2001 From: tin-berri Date: Tue, 18 Aug 2026 13:13:21 -0700 Subject: [PATCH] feat(ui): plan-mode override tier in the auto-router create and edit forms (#37319) * feat(ui): plan-mode override tier in the auto-router create and edit forms The backend plan_mode_min_tier field (#37230) was API-only. Both forms now carry an Advanced: Plan-Mode Override panel in the shared complexity config component: a toggle derived from field presence, so on writes the highest tier that has models and off deletes the key, and a tier select limited to tiers with models because the backend rejects a floor at an empty tier. The edit modal manages the key on every save, so clearing it actually clears the stored config instead of the preserved copy resurrecting it, while unmanaged keys like plan_mode_patterns still round-trip untouched * refactor(ui): hoist the eligible plan-mode tier list out of the panel JSX * refactor(ui): move tierOptions into complexity_router_tiers, the shared tier-utility module --- .../add_model/ComplexityRouterConfig.test.tsx | 58 +++++++++++++++++++ .../add_model/ComplexityRouterConfig.tsx | 50 +++++++++++++++- .../components/add_model/KeywordTierRules.tsx | 15 +---- .../add_model/add_auto_router_tab.test.tsx | 47 +++++++++++++++ .../add_model/add_auto_router_tab.tsx | 3 + .../build_complexity_router_config.test.ts | 34 +++++++++++ .../build_complexity_router_config.ts | 14 +++++ .../add_model/complexity_router_tiers.ts | 15 +++++ ...d_updated_complexity_router_config.test.ts | 23 ++++++++ .../edit_auto_router_modal.test.tsx | 57 ++++++++++++++++++ .../edit_auto_router_modal.tsx | 8 +++ 11 files changed, 309 insertions(+), 15 deletions(-) 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 199acfe8769..217d3ca0989 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx @@ -785,3 +785,61 @@ describe("ComplexityRouterConfig default model", () => { expect(screen.getByRole("radio", { name: /Route to the default model \(claude-3-opus\)/ })).toBeInTheDocument(); }); }); + +describe("plan-mode override", () => { + const openPanel = () => fireEvent.click(screen.getByText("Advanced: Plan-Mode Override")); + const switchName = "Route plan-mode requests to a minimum tier"; + + it("toggling on floors at the highest tier that has models", async () => { + const onChange = vi.fn(); + renderWithProviders(); + openPanel(); + fireEvent.click(await screen.findByRole("switch", { name: switchName })); + expect(onChange.mock.calls.at(-1)?.[0].plan_mode_min_tier).toBe("REASONING"); + }); + + it("toggling off drops the key entirely instead of storing an empty value", async () => { + const onChange = vi.fn(); + renderWithProviders( + , + ); + openPanel(); + const control = await screen.findByRole("switch", { name: switchName }); + expect(control).toBeChecked(); + fireEvent.click(control); + const updated = onChange.mock.calls.at(-1)?.[0]; + expect(updated.plan_mode_min_tier).toBeUndefined(); + }); + + it("only offers tiers that have models, since the backend rejects a floor at an empty tier", async () => { + renderWithProviders( + , + ); + openPanel(); + fireEvent.mouseDown(await screen.findByRole("combobox", { name: "Plan-mode minimum tier" })); + expect(await screen.findByTitle("Medium")).toBeInTheDocument(); + expect(screen.queryByTitle("Reasoning")).not.toBeInTheDocument(); + }); + + it("disables the toggle until some tier has models", async () => { + renderWithProviders( + , + ); + openPanel(); + expect(await screen.findByRole("switch", { name: switchName })).toBeDisabled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx index 84d07facbf0..3d4845ecf39 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx @@ -4,7 +4,7 @@ import React from "react"; import { ModelGroup } from "@/components/llm_calls/fetch_models"; import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig"; import ClassificationMethodConfig from "./ClassificationMethodConfig"; -import { resolveComplexityDefaultModel } from "./complexity_router_tiers"; +import { resolveComplexityDefaultModel, tierOptions } from "./complexity_router_tiers"; import EscalationKeywords from "./EscalationKeywords"; import KeywordTierRules, { KeywordTierRule } from "./KeywordTierRules"; import SemanticKeywordMatching from "./SemanticKeywordMatching"; @@ -121,6 +121,8 @@ export interface ComplexityRouterConfigValue { classifier_fallback?: ClassifierFallback; session_affinity?: boolean; deployment_affinity?: boolean; + /** Tier floor for coding-agent plan-mode requests. Unset means detection is off, matching the backend. */ + plan_mode_min_tier?: string; adaptive?: boolean; adaptive_weights?: AdaptiveRouterWeights; tier_distance_penalty?: number; @@ -187,6 +189,10 @@ export const TIER_KEYS = Object.keys(TIER_DESCRIPTIONS) as Array tierLabels?.[tier]?.trim() || TIER_DESCRIPTIONS[tier].label; +/** Tiers the plan-mode floor may name: the backend rejects a floor whose tier has no models. */ +export const planModeEligibleTiers = (tiers: ComplexityTiers): Array => + TIER_KEYS.filter((tier) => (tiers[tier] ?? []).length > 0); + const ComplexityRouterConfig: React.FC = ({ modelInfo, value, @@ -205,6 +211,7 @@ const ComplexityRouterConfig: React.FC = ({ onEscalationKeywordsChange, showValidationErrors = false, }) => { + const planModeTiers = planModeEligibleTiers(value.tiers); const derivedDefaultModel = resolveComplexityDefaultModel(value.tiers); const defaultModel = resolveComplexityDefaultModel(value.tiers, value.default_model); @@ -417,6 +424,47 @@ const ComplexityRouterConfig: React.FC = ({ ), }, + { + key: "plan-mode", + label: ( + + Advanced: Plan-Mode Override + + ), + children: ( + <> +
+ + onChange({ ...value, plan_mode_min_tier: enabled ? planModeTiers.at(-1) : undefined }) + } + aria-label="Route plan-mode requests to a minimum tier" + /> + Route plan-mode requests to a minimum tier +
+ + Requests from coding agents in plan mode (Claude Code, GitHub Copilot) route to at least this tier. + The classifier still wins when it picks higher, and the override only lasts while plan mode is active. + {planModeTiers.length === 0 && " Add models to a tier to enable this."} + + {value.plan_mode_min_tier !== undefined && ( +
+ + (planModeTiers as string[]).includes(option.value), + )} + onChange={(tier: string) => onChange({ ...value, plan_mode_min_tier: tier })} + /> +
+ )} + + ), + }, { key: "response", label: ( diff --git a/ui/litellm-dashboard/src/components/add_model/KeywordTierRules.tsx b/ui/litellm-dashboard/src/components/add_model/KeywordTierRules.tsx index fb89e67a2b3..72d6e9f4fef 100644 --- a/ui/litellm-dashboard/src/components/add_model/KeywordTierRules.tsx +++ b/ui/litellm-dashboard/src/components/add_model/KeywordTierRules.tsx @@ -3,6 +3,7 @@ import { Button, Card, Empty, Select as AntdSelect, Tooltip, Typography } from " import React from "react"; import { emptyKeywordTierRuleIndexes } from "./complexity_router_keywords"; +import { tierOptions } from "./complexity_router_tiers"; const { Text } = Typography; @@ -20,20 +21,6 @@ interface KeywordTierRulesProps { tierLabels?: Partial>; } -const DEFAULT_TIER_LABELS: Record = { - SIMPLE: "Simple", - MEDIUM: "Medium", - COMPLEX: "Complex", - REASONING: "Reasoning", -}; - -const TIER_ORDER: ComplexityTier[] = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"]; - -export const tierOptions = ( - tierLabels: Partial> | undefined, -): { value: ComplexityTier; label: string }[] => - TIER_ORDER.map((tier) => ({ value: tier, label: tierLabels?.[tier]?.trim() || DEFAULT_TIER_LABELS[tier] })); - // A row exists only because the caller asked for it, so it reports its own gap straight away // rather than waiting for a submit; the submit button is disabled while one is outstanding, so // there is no failed attempt left to surface it. 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 988bae46bb4..5643dafc0f5 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 @@ -665,6 +665,53 @@ describe("AddAutoRouterTab", () => { }); }); + describe("plan-mode override", () => { + beforeEach(() => { + mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS); + }); + + const waitForPresetEnabled = async (label: string) => { + openTemplateDropdown(); + await waitFor(() => { + expect(isOptionDisabled(optionByLabel(label)!)).toBe(false); + }); + }; + + it("omits plan_mode_min_tier from the payload when never touched", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await waitForPresetEnabled("Anthropic Family"); + fireEvent.click(optionByLabel("Anthropic Family")!); + await user.type(screen.getByPlaceholderText(/smart_router/i), "no-plan-router"); + 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( + "plan_mode_min_tier", + ); + }); + + it("carries the enabled override through to the create payload", async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await waitForPresetEnabled("Anthropic Family"); + fireEvent.click(optionByLabel("Anthropic Family")!); + expandDetailedConfiguration(); + await user.click(screen.getByText("Advanced: Plan-Mode Override")); + await user.click(await screen.findByRole("switch", { name: "Route plan-mode requests to a minimum tier" })); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "plan-router"); + 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({ + plan_mode_min_tier: "REASONING", + }); + }); + }); + describe("deployment-matched presets", () => { const renamedDeploymentsFor = (presetKey: string) => [...getRequiredModelsInPreset(getPresetByKey(presetKey)!)].map((model, index) => ({ 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 7537c91849e..e35b9581d9c 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 @@ -35,6 +35,7 @@ import { buildComplexityRouterConfig, getKeywordTierRulesError, getMissingTiersError, + getPlanModeTierError, getSemanticConfigError, getTierLabelsError, } from "./build_complexity_router_config"; @@ -127,6 +128,7 @@ const getSubmitBlockedReason = ( ): string | null => getMissingTiersError(config.tiers) ?? getTierLabelsError(config.tier_labels) ?? + getPlanModeTierError(config.plan_mode_min_tier, config.tiers) ?? getKeywordTierRulesError(keywordTierRules) ?? getReferencedModelsError(referencedModelsParams, availability); @@ -335,6 +337,7 @@ const AddAutoRouterTab: React.FC = ({ const complexityRouterConfigParams: BuildComplexityRouterConfigParams = { tiers: complexityRouterConfig.tiers, defaultModel: complexityRouterConfig.default_model, + planModeMinTier: complexityRouterConfig.plan_mode_min_tier, tierLabels: complexityRouterConfig.tier_labels, classifierType: complexityRouterConfig.classifier_type, classifierLlmConfig: complexityRouterConfig.classifier_llm_config, 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 703231ba5ee..81d54a7b773 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 @@ -1,5 +1,6 @@ import { buildComplexityRouterConfig, + getPlanModeTierError, normalizeClassifierLlmConfig, getKeywordTierRulesError, getMissingTiersError, @@ -601,3 +602,36 @@ describe("buildComplexityRouterConfig scorer knobs", () => { expect(buildComplexityRouterConfig(llmWithDefaultFallback)).not.toHaveProperty("tier_boundaries"); }); }); + +describe("plan-mode minimum tier", () => { + it("omits plan_mode_min_tier when unset, so the backend default (off) is preserved", () => { + const config = buildComplexityRouterConfig({ ...baseParams, planModeMinTier: undefined }); + expect(config).not.toHaveProperty("plan_mode_min_tier"); + }); + + it("writes the selected tier", () => { + const config = buildComplexityRouterConfig({ ...baseParams, planModeMinTier: "COMPLEX" }); + expect(config.plan_mode_min_tier).toBe("COMPLEX"); + }); + + it("never writes an empty string, which the backend rejects instead of treating as off", () => { + const config = buildComplexityRouterConfig({ ...baseParams, planModeMinTier: " " }); + expect(config).not.toHaveProperty("plan_mode_min_tier"); + }); +}); + +describe("getPlanModeTierError", () => { + const tiersWithEmptyComplex = { SIMPLE: ["m1"], MEDIUM: ["m1"], COMPLEX: [], REASONING: [] }; + + it("passes when the override is off", () => { + expect(getPlanModeTierError(undefined, tiersWithEmptyComplex)).toBeNull(); + }); + + it("passes when the named tier has models", () => { + expect(getPlanModeTierError("MEDIUM", tiersWithEmptyComplex)).toBeNull(); + }); + + it("blocks a tier whose models were removed, which the backend would reject with a 400", () => { + expect(getPlanModeTierError("COMPLEX", tiersWithEmptyComplex)).toContain("COMPLEX"); + }); +}); 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 3ee3b6f0180..0ab2db8c16f 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 @@ -71,6 +71,7 @@ const scorerKnobPayload = ({ export interface BuildComplexityRouterConfigParams { tiers: ComplexityTiers; defaultModel: string | undefined; + planModeMinTier: string | undefined; tierLabels: ComplexityTierLabels | undefined; classifierType: ClassifierType; classifierLlmConfig: ClassifierLLMConfig | undefined; @@ -99,6 +100,7 @@ export interface BuildComplexityRouterConfigParams { export interface ComplexityRouterConfigPayload { tiers: ComplexityTiers; default_model?: string; + plan_mode_min_tier?: string; tier_labels?: ComplexityTierLabels; classifier_type: ClassifierType; classifier_llm_config?: ClassifierLLMConfig; @@ -172,6 +174,16 @@ export const getMissingTiersError = (tiers: ComplexityTiers): string | null => { return `Select a model for the following tier(s): ${missing.join(", ")}`; }; +// The backend rejects a plan-mode floor naming a tier with no models. The create form's +// getMissingTiersError makes this unreachable there; the edit modal allows partially filled +// tiers, so both gates call this to keep the two forms symmetric. +export const getPlanModeTierError = (planModeMinTier: string | undefined, tiers: ComplexityTiers): string | null => { + if (!planModeMinTier) return null; + const models = tiers[planModeMinTier as keyof ComplexityTiers] ?? []; + if (models.length > 0) return null; + return `The plan-mode minimum tier (${planModeMinTier}) has no models. Add one or turn the override off.`; +}; + export const getKeywordTierRulesError = (keywordTierRules: KeywordTierRule[]): string | null => { const emptyRows = emptyKeywordTierRuleIndexes(keywordTierRules); if (emptyRows.length === 0) return null; @@ -194,6 +206,7 @@ export const getSemanticConfigError = ({ export const buildComplexityRouterConfig = ({ tiers, defaultModel, + planModeMinTier, tierLabels, classifierType, classifierLlmConfig, @@ -227,6 +240,7 @@ export const buildComplexityRouterConfig = ({ return { tiers, ...(defaultModel?.trim() && { default_model: defaultModel }), + ...(planModeMinTier?.trim() && { plan_mode_min_tier: planModeMinTier }), ...(cleanedTierLabels && { tier_labels: cleanedTierLabels }), classifier_type: classifierType, ...(classifierType === "llm" && diff --git a/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts b/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts index 2e7859964bc..6f5eb7f877b 100644 --- a/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts +++ b/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts @@ -1,4 +1,5 @@ import type { ComplexityTiers } from "./ComplexityRouterConfig"; +import type { ComplexityTier } from "./KeywordTierRules"; /** * A complexity tier maps to `str | list[str]` on the backend @@ -22,3 +23,17 @@ export const normalizeTierModels = (value: unknown): string[] => { */ export const resolveComplexityDefaultModel = (tiers: ComplexityTiers, pinned?: string): string | undefined => pinned?.trim() || tiers.MEDIUM[0] || tiers.SIMPLE[0]; + +export const DEFAULT_TIER_LABELS: Record = { + SIMPLE: "Simple", + MEDIUM: "Medium", + COMPLEX: "Complex", + REASONING: "Reasoning", +}; + +export const TIER_ORDER: ComplexityTier[] = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"]; + +export const tierOptions = ( + tierLabels: Partial> | undefined, +): { value: ComplexityTier; label: string }[] => + TIER_ORDER.map((tier) => ({ value: tier, label: tierLabels?.[tier]?.trim() || DEFAULT_TIER_LABELS[tier] })); 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 8acae918c9b..c62955b6683 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 @@ -314,3 +314,26 @@ describe("buildUpdatedComplexityRouterConfig scorer knobs", () => { expect(buildUpdatedComplexityRouterConfig(STORED, FORM_VALUE)).not.toHaveProperty("tier_boundaries"); }); }); + +describe("buildUpdatedComplexityRouterConfig plan-mode minimum tier", () => { + it("round-trips a stored tier through an untouched open-and-save", () => { + const result = buildUpdatedComplexityRouterConfig( + { ...STORED, plan_mode_min_tier: "COMPLEX" }, + { ...FORM_VALUE, plan_mode_min_tier: "COMPLEX" }, + ); + expect(result.plan_mode_min_tier).toBe("COMPLEX"); + }); + + it("stops a stored tier from surviving a save that turned the override off", () => { + const result = buildUpdatedComplexityRouterConfig({ ...STORED, plan_mode_min_tier: "COMPLEX" }, FORM_VALUE); + expect(result).not.toHaveProperty("plan_mode_min_tier"); + }); + + it("writes a newly selected tier over the stored one", () => { + const result = buildUpdatedComplexityRouterConfig( + { ...STORED, plan_mode_min_tier: "COMPLEX" }, + { ...FORM_VALUE, plan_mode_min_tier: "MEDIUM" }, + ); + expect(result.plan_mode_min_tier).toBe("MEDIUM"); + }); +}); 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 019f60aecd9..9702fa85db0 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 @@ -748,3 +748,60 @@ describe("EditAutoRouterModal default model", () => { expect(savedConfig()).not.toHaveProperty("default_model"); }); }); + +describe("EditAutoRouterModal plan-mode minimum tier", () => { + beforeEach(() => { + modelPatchUpdateCall.mockClear(); + }); + + const renderWithStoredTier = (plan_mode_min_tier?: string) => + renderWithProviders( + , + ); + + const openPlanModePanel = async (user: ReturnType) => { + await user.click(await screen.findByText("Advanced: Plan-Mode Override")); + }; + + it("shows a stored tier as an enabled override, so the saved value is not a hidden one", async () => { + const user = userEvent.setup(); + renderWithStoredTier("MEDIUM"); + await openPlanModePanel(user); + expect(await screen.findByRole("switch", { name: "Route plan-mode requests to a minimum tier" })).toBeChecked(); + }); + + it("preserves a stored tier through an untouched open-and-save", async () => { + const user = userEvent.setup(); + renderWithStoredTier("MEDIUM"); + + await user.click(await screen.findByRole("button", { name: /save changes/i })); + + await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled()); + expect(savedConfig()).toMatchObject({ plan_mode_min_tier: "MEDIUM" }); + }); + + it("turning the override off removes the stored tier from the saved config", async () => { + const user = userEvent.setup(); + renderWithStoredTier("MEDIUM"); + await openPlanModePanel(user); + await user.click(await screen.findByRole("switch", { name: "Route plan-mode requests to a minimum tier" })); + + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled()); + expect(savedConfig()).not.toHaveProperty("plan_mode_min_tier"); + }); +}); 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 d5e3aad4f8d..54f992b08f4 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 @@ -19,6 +19,7 @@ import { isComplexityRouter } from "../add_model/auto_router_strategies"; import { getKeywordTierRulesError, getSemanticConfigError, + getPlanModeTierError, getTierLabelsError, hydrateTierLabels, normalizeClassifierLlmConfig, @@ -65,6 +66,7 @@ interface EditAutoRouterModalProps { const MANAGED_COMPLEXITY_ROUTER_KEYS = new Set([ "tiers", "default_model", + "plan_mode_min_tier", "tier_labels", "classifier_type", "classifier_llm_config", @@ -149,6 +151,7 @@ export const buildUpdatedComplexityRouterConfig = ( ...preservedConfig, tiers: value.tiers, ...(value.default_model?.trim() && { default_model: value.default_model }), + ...(value.plan_mode_min_tier?.trim() && { plan_mode_min_tier: value.plan_mode_min_tier }), ...(serializedTierLabels && { tier_labels: serializedTierLabels }), classifier_type: value.classifier_type, ...(value.classifier_type === "llm" && value.classifier_llm_config @@ -279,6 +282,7 @@ const EditAutoRouterModal: React.FC = ({ ? "Please select at least one model for a complexity tier" : null) ?? getTierLabelsError(complexityRouterConfig.tier_labels) ?? + getPlanModeTierError(complexityRouterConfig.plan_mode_min_tier, complexityRouterConfig.tiers) ?? getKeywordTierRulesError(keywordTierRules); useEffect(() => { @@ -337,6 +341,10 @@ const EditAutoRouterModal: React.FC = ({ modelData.litellm_params?.complexity_router_default_model, hydratedTiers, ), + plan_mode_min_tier: + typeof parsedConfig.plan_mode_min_tier === "string" && parsedConfig.plan_mode_min_tier.trim() !== "" + ? parsedConfig.plan_mode_min_tier + : undefined, tier_labels: hydrateTierLabels(parsedConfig.tier_labels), classifier_type: parsedConfig.classifier_type || "heuristic", classifier_llm_config: parsedConfig.classifier_llm_config,