From 9644032cb806a8bef55d1bcf4219ddec4886b758 Mon Sep 17 00:00:00 2001
From: yuneng
Date: Mon, 21 Sep 2026 19:37:45 +0000
Subject: [PATCH] refactor(ui): split complexity router form files and cover
advanced fields
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---
.../add_model/ClassificationMethodConfig.tsx | 118 +--------
.../ClassifierPluginTimeoutField.tsx | 54 ++++
.../add_model/ClassifierTypeRadios.tsx | 89 +++++++
.../ComplexityRouterAdvancedSections.tsx | 240 +++++++++++++++++
.../add_model/ComplexityRouterConfig.test.tsx | 58 +++++
.../add_model/ComplexityRouterConfig.tsx | 210 +++------------
.../components/add_model/ReminderMarkers.tsx | 4 +-
.../add_model/add_auto_router_tab.tsx | 59 +----
.../build_complexity_router_config.test.ts | 16 ++
.../build_complexity_router_config.ts | 17 +-
.../complexity_router_builder_params.ts | 74 ++++++
...dit_auto_router_modal.integration.test.tsx | 71 +++++
.../edit_auto_router_modal.tsx | 243 +-----------------
.../hydrate_complexity_router_config.ts | 183 +++++++++++++
14 files changed, 839 insertions(+), 597 deletions(-)
create mode 100644 ui/litellm-dashboard/src/components/add_model/ClassifierPluginTimeoutField.tsx
create mode 100644 ui/litellm-dashboard/src/components/add_model/ClassifierTypeRadios.tsx
create mode 100644 ui/litellm-dashboard/src/components/add_model/ComplexityRouterAdvancedSections.tsx
create mode 100644 ui/litellm-dashboard/src/components/add_model/complexity_router_builder_params.ts
create mode 100644 ui/litellm-dashboard/src/components/edit_auto_router/hydrate_complexity_router_config.ts
diff --git a/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx
index 2564137fb02..b7a0fd67443 100644
--- a/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx
@@ -19,10 +19,9 @@ import HeuristicScoringConfig from "./HeuristicScoringConfig";
import ClassifierReasoningEffortSelect from "./ClassifierReasoningEffortSelect";
import ClassifierCircuitBreakerConfig from "./ClassifierCircuitBreakerConfig";
import ClassifierVisionConfig from "./ClassifierVisionConfig";
-import {
- getClassifierPluginTimeoutError,
- getHeuristicV2SuccessThresholdError,
-} from "./build_complexity_router_config";
+import { getHeuristicV2SuccessThresholdError } from "./build_complexity_router_config";
+import ClassifierPluginTimeoutField from "./ClassifierPluginTimeoutField";
+import ClassifierTypeRadios from "./ClassifierTypeRadios";
import type { ReasoningEffort } from "./complexity_router_tiers";
import { useComplexityScorerDefaults } from "@/app/(dashboard)/hooks/autoRouter/useComplexityScorerDefaults";
import {
@@ -64,7 +63,6 @@ const CLASSIFIER_CONTEXT_WINDOW_SIZE_ID = "classifier-context-window-size";
const CLASSIFIER_CONTEXT_BUDGET_CHARS_ID = "classifier-context-budget-chars";
const HYBRID_BOUNDARY_MARGIN_ID = "hybrid-boundary-margin";
const HEURISTIC_V2_SUCCESS_THRESHOLD_ID = "heuristic-v2-success-threshold";
-const CLASSIFIER_PLUGIN_TIMEOUT_ID = "classifier-plugin-timeout-ms";
const CUSTOM_PROMPT_WITH_HEURISTIC_FALLBACK =
"This router classifies with your own prompt, so the tier comes from whatever rubric it states. The four tier " +
@@ -208,84 +206,6 @@ export const InactiveHeuristicV2Threshold: React.FC void;
-}> = ({ value, classifierType, onTypeChange }) => {
- const scorerLocked = Boolean(value.custom_tier_set);
- const scorerLockedReason = restrictedBy(value, "heuristicClassifier")?.reason;
- return (
- onTypeChange(classifierType as ClassifierType)}
- className="w-full"
- >
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
const ClassificationMethodConfig: React.FC = ({
value,
onChange,
@@ -472,37 +392,7 @@ const ClassificationMethodConfig: React.FC = ({
{classifierType === "custom" && (
-
-
- This router uses a custom classifier plugin set in config.yaml. Pick a classifier below to replace it.
-
-
-
- onChange({
- ...value,
- classifier_plugin_timeout_ms: event.target.value.trim() === "" ? undefined : Number(event.target.value),
- })
- }
- aria-invalid={Boolean(
- showValidationErrors && getClassifierPluginTimeoutError(classifierType, value.classifier_plugin_timeout_ms),
- )}
- />
-
- Time budget for the plugin call. On expiry the fallback path decides the tier.
-
- {showValidationErrors && getClassifierPluginTimeoutError(classifierType, value.classifier_plugin_timeout_ms) && (
-
- {getClassifierPluginTimeoutError(classifierType, value.classifier_plugin_timeout_ms)}
-
- )}
-
+
)}
{classifierType === "heuristic_v2" && (
diff --git a/ui/litellm-dashboard/src/components/add_model/ClassifierPluginTimeoutField.tsx b/ui/litellm-dashboard/src/components/add_model/ClassifierPluginTimeoutField.tsx
new file mode 100644
index 00000000000..45decf09a09
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/ClassifierPluginTimeoutField.tsx
@@ -0,0 +1,54 @@
+import React from "react";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
+import { getClassifierPluginTimeoutError } from "./build_complexity_router_config";
+
+const CLASSIFIER_PLUGIN_TIMEOUT_ID = "classifier-plugin-timeout-ms";
+
+interface ClassifierPluginTimeoutFieldProps {
+ value: ComplexityRouterConfigValue;
+ onChange: (value: ComplexityRouterConfigValue) => void;
+ showValidationErrors?: boolean;
+}
+
+const ClassifierPluginTimeoutField: React.FC = ({
+ value,
+ onChange,
+ showValidationErrors = false,
+}) => {
+ const error = getClassifierPluginTimeoutError("custom", value.classifier_plugin_timeout_ms);
+ return (
+
+
+ This router uses a custom classifier plugin set in config.yaml. Pick a classifier below to replace it.
+
+
+
+ onChange({
+ ...value,
+ classifier_plugin_timeout_ms: event.target.value.trim() === "" ? undefined : Number(event.target.value),
+ })
+ }
+ aria-invalid={Boolean(showValidationErrors && error)}
+ />
+
+ Time budget for the plugin call. On expiry the fallback path decides the tier.
+
+ {showValidationErrors && error && (
+
+ {error}
+
+ )}
+
+ );
+};
+
+export default ClassifierPluginTimeoutField;
diff --git a/ui/litellm-dashboard/src/components/add_model/ClassifierTypeRadios.tsx b/ui/litellm-dashboard/src/components/add_model/ClassifierTypeRadios.tsx
new file mode 100644
index 00000000000..1602e19069a
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/ClassifierTypeRadios.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { Label } from "@/components/ui/label";
+import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
+import { SimpleTooltip } from "@/components/ui/tooltip";
+import type { ClassifierType } from "./classifier_types";
+import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
+import { restrictedBy } from "./TierRestrictions";
+
+interface ClassifierTypeRadiosProps {
+ value: ComplexityRouterConfigValue;
+ classifierType: ClassifierType;
+ onTypeChange: (classifierType: ClassifierType) => void;
+}
+
+const ClassifierTypeRadios: React.FC = ({ value, classifierType, onTypeChange }) => {
+ const scorerLocked = Boolean(value.custom_tier_set);
+ const scorerLockedReason = restrictedBy(value, "heuristicClassifier")?.reason;
+ return (
+ onTypeChange(nextType as ClassifierType)}
+ className="w-full"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ClassifierTypeRadios;
diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterAdvancedSections.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterAdvancedSections.tsx
new file mode 100644
index 00000000000..dd822907735
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterAdvancedSections.tsx
@@ -0,0 +1,240 @@
+import React from "react";
+import { ChevronRight } from "lucide-react";
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
+import { Separator } from "@/components/ui/separator";
+import type { ModelGroup } from "@/components/llm_calls/fetch_models";
+import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
+import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig";
+import ClassificationMethodConfig from "./ClassificationMethodConfig";
+import ContextWindowEscalationConfig from "./ContextWindowEscalationConfig";
+import ResponseFormatControls from "./ResponseFormatControls";
+import StallEscalationConfig from "./StallEscalationConfig";
+import { Restricted, restrictedBy } from "./TierRestrictions";
+import EscalationKeywords from "./EscalationKeywords";
+import KeywordTierRules, { type KeywordTierRule } from "./KeywordTierRules";
+import SemanticKeywordMatching from "./SemanticKeywordMatching";
+import CompressionControls from "./CompressionControls";
+import PlanModeOverrideControls from "./PlanModeOverrideControls";
+import { AffinityControls } from "./AffinityControls";
+import { ModalityRoutingControls } from "./ModalityRoutingControls";
+import HeuristicKeywordOverrides from "./HeuristicKeywordOverrides";
+import HousekeepingRoutingControls from "./HousekeepingRoutingControls";
+import ReminderMarkers from "./ReminderMarkers";
+import type { AutoRouterCompressionState } from "./buildAutoRouterCompression";
+import { activeTierName, type TierRow } from "./tier_rows";
+
+interface ComplexityRouterAdvancedSectionsProps {
+ value: ComplexityRouterConfigValue;
+ onChange: (value: ComplexityRouterConfigValue) => void;
+ forecast: boolean;
+ modelOptions: { value: string; label: string }[];
+ classifierEffortOptionsByModel: Record;
+ customTechnicalKeywords?: string[];
+ onCustomTechnicalKeywordsChange?: (keywords: string[]) => void;
+ showValidationErrors: boolean;
+ defaultModel?: string;
+ planModeTierOptions: { value: string; label: string }[];
+ keywordTierRules: KeywordTierRule[];
+ onKeywordTierRulesChange?: (rules: KeywordTierRule[]) => void;
+ semanticMatchingEnabled: boolean;
+ onSemanticMatchingEnabledChange?: (enabled: boolean) => void;
+ embeddingModel?: string;
+ onEmbeddingModelChange: (model: string) => void;
+ matchThreshold: number;
+ onMatchThresholdChange: (threshold: number) => void;
+ escalationKeywords: string[];
+ onEscalationKeywordsChange?: (keywords: string[]) => void;
+ autoRouterCompression: AutoRouterCompressionState;
+ onAutoRouterCompressionChange?: (state: AutoRouterCompressionState) => void;
+ modelInfo: ModelGroup[];
+ tierRows: TierRow[];
+ customTierSet: ComplexityRouterConfigValue["custom_tier_set"];
+}
+
+const ComplexityRouterAdvancedSections: React.FC = ({
+ value,
+ onChange,
+ forecast,
+ modelOptions,
+ classifierEffortOptionsByModel,
+ customTechnicalKeywords,
+ onCustomTechnicalKeywordsChange,
+ showValidationErrors,
+ defaultModel,
+ planModeTierOptions,
+ keywordTierRules,
+ onKeywordTierRulesChange,
+ semanticMatchingEnabled,
+ onSemanticMatchingEnabledChange,
+ embeddingModel,
+ onEmbeddingModelChange,
+ matchThreshold,
+ onMatchThresholdChange,
+ escalationKeywords,
+ onEscalationKeywordsChange,
+ autoRouterCompression,
+ onAutoRouterCompressionChange,
+ modelInfo,
+ tierRows,
+ customTierSet,
+}) => {
+ const sections = [
+ ...(!forecast
+ ? [
+ {
+ key: "classifier",
+ label: Advanced: Classification Method,
+ children: (
+
+ ),
+ },
+ ]
+ : []),
+ ...(!forecast
+ ? [
+ {
+ key: "keyword-overrides",
+ label: Advanced: Heuristic Keyword Overrides,
+ children: ,
+ },
+ ]
+ : []),
+ {
+ key: "adaptive",
+ label: Advanced: Adaptive Routing,
+ children: (
+
+
+
+ ),
+ },
+ {
+ key: "affinity",
+ label: Advanced: Affinity,
+ children: ,
+ },
+ {
+ key: "modality",
+ label: Advanced: Modality Routing,
+ children: ,
+ },
+ {
+ key: "plan-mode",
+ label: Advanced: Plan-Mode Override,
+ children: ,
+ },
+ {
+ key: "housekeeping",
+ label: Advanced: Housekeeping Routing,
+ children: ,
+ },
+ {
+ key: "reminder-markers",
+ label: Advanced: Reminder Markers,
+ children: ,
+ },
+ {
+ key: "context-window",
+ label: Advanced: Context Window Escalation,
+ children: ,
+ },
+ {
+ key: "stall-escalation",
+ label: Advanced: Stalled Task Escalation,
+ children: (
+
+
+
+ ),
+ },
+ {
+ key: "response",
+ label: Advanced: Response Format,
+ children: ,
+ },
+ ...(onEscalationKeywordsChange
+ ? [
+ {
+ key: "escalation",
+ label: Advanced: Escalation Keywords,
+ children: (
+
+
+
+ ),
+ },
+ ]
+ : []),
+ ...(onAutoRouterCompressionChange
+ ? [
+ {
+ key: "compression",
+ label: Advanced: Compression,
+ children: ,
+ },
+ ]
+ : []),
+ ...(onKeywordTierRulesChange || onSemanticMatchingEnabledChange
+ ? [
+ {
+ key: "keyword-semantic",
+ label: Advanced: Keyword/Semantic Matching,
+ children: (
+ <>
+ {onKeywordTierRulesChange && (
+
+ )}
+ {onKeywordTierRulesChange && onSemanticMatchingEnabledChange && }
+ {onSemanticMatchingEnabledChange && (
+
+ )}
+ >
+ ),
+ },
+ ]
+ : []),
+ ];
+
+ return (
+ <>
+ {sections
+ .filter(({ key }) => !forecast || !["adaptive", "context-window", "escalation"].includes(key))
+ .map(({ key, label, children }) => (
+
+
+
+ {label}
+
+ {children}
+
+ ))}
+ >
+ );
+};
+
+export default ComplexityRouterAdvancedSections;
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 70658b787f0..9a8577100df 100644
--- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx
@@ -96,6 +96,64 @@ describe("ComplexityRouterConfig", () => {
expect(screen.queryByText("Classifier Model")).not.toBeInTheDocument();
});
+ it("shows heuristic advanced sections and hides keyword overrides for capability classifiers", () => {
+ const { rerender } = renderWithProviders();
+
+ expect(screen.getByText("Advanced: Heuristic Keyword Overrides")).toBeInTheDocument();
+ expect(screen.getByText("Advanced: Housekeeping Routing")).toBeInTheDocument();
+ expect(screen.getByText("Advanced: Reminder Markers")).toBeInTheDocument();
+
+ const capabilityValue = { ...defaultValue, classifier_type: "capability" as const };
+ rerender();
+ expect(screen.queryByText("Advanced: Heuristic Keyword Overrides")).not.toBeInTheDocument();
+
+ });
+
+ it.each([
+ ["custom", true],
+ ["heuristic", false],
+ ] as const)("shows plugin timeout only for %s classifiers", (classifierType, visible) => {
+ renderWithProviders(
+ ,
+ );
+ fireEvent.click(screen.getByText("Advanced: Classification Method"));
+ if (visible) {
+ expect(screen.getByLabelText("Classifier plugin timeout (ms)")).toBeInTheDocument();
+ } else {
+ expect(screen.queryByLabelText("Classifier plugin timeout (ms)")).not.toBeInTheDocument();
+ }
+ });
+
+ it.each([true, false])("shows reminder marker validation only when requested: %s", (showValidationErrors) => {
+ const value = { ...defaultValue, reminder_markers: [{ open: "", close: "x" }] };
+ renderWithProviders(
+ ,
+ );
+ fireEvent.click(screen.getByText("Advanced: Reminder Markers"));
+ const validation = screen.queryByText(/needs both/i);
+ if (showValidationErrors) {
+ expect(validation).toBeInTheDocument();
+ } else {
+ expect(validation).not.toBeInTheDocument();
+ }
+ });
+
+ it("disables housekeeping sentinels when cheapest-tier routing is off", () => {
+ renderWithProviders(
+ ,
+ );
+ fireEvent.click(screen.getByText("Advanced: Housekeeping Routing"));
+ const sentinelInput = screen.getByRole("combobox", { name: "e.g., conversation title" });
+ expect(sentinelInput).toBeDisabled();
+ });
+
it("should toggle returning the raw model name", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx
index a97e7a77a21..acf6b62a95a 100644
--- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx
@@ -2,21 +2,17 @@ import RoutingOptions from "./RoutingOptions";
import type { JevClassifierConfig } from "./jev_classifier_config";
import { type ClassifierType } from "./classifier_types";
export { type ClassifierType, usesLlmClassifier, usesClassifierContext } from "./classifier_types";
-import PlanModeOverrideControls from "./PlanModeOverrideControls";
import ForecastClassifierConfig, { ForecastSolverModels } from "./ForecastClassifierConfig";
import { isForecastClassifier, type CapabilitySettings, type FuseSettings } from "./forecast_classifier_config";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { MultiSelect } from "@/components/shared/MultiSelect";
import DefaultModelField from "./DefaultModelField";
-import { ChevronRight, Info, Plus, Trash2, X } from "lucide-react";
+import { Info, Plus, Trash2, X } from "lucide-react";
-import { AffinityControls } from "./AffinityControls";
import NonReasoningTierToggle from "./NonReasoningTierToggle";
import TierConfigIntro from "./TierConfigIntro";
import TierRowSelect from "./TierRowSelect";
-import { ModalityRoutingControls } from "./ModalityRoutingControls";
import { Card, CardContent } from "@/components/ui/card";
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
@@ -39,12 +35,8 @@ import {
} from "./tier_rows";
import React from "react";
import { ModelGroup } from "@/components/llm_calls/fetch_models";
-import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig";
-import ClassificationMethodConfig, { InactiveHeuristicV2Threshold } from "./ClassificationMethodConfig";
-import ContextWindowEscalationConfig from "./ContextWindowEscalationConfig";
-import ResponseFormatControls from "./ResponseFormatControls";
-import StallEscalationConfig from "./StallEscalationConfig";
-import { Restricted, restrictedBy } from "./TierRestrictions";
+import { InactiveHeuristicV2Threshold } from "./ClassificationMethodConfig";
+import ComplexityRouterAdvancedSections from "./ComplexityRouterAdvancedSections";
import { type TierSetAction, applyTierSetAction, setFallbackTier } from "./tier_set_actions";
import {
ReasoningEffort,
@@ -56,16 +48,10 @@ import {
tierRowLabel,
} from "./complexity_router_tiers";
import TierModelEffortRows from "./TierModelEffortRows";
-import EscalationKeywords from "./EscalationKeywords";
-import KeywordTierRules, { KeywordTierRule } from "./KeywordTierRules";
-import SemanticKeywordMatching from "./SemanticKeywordMatching";
+import { KeywordTierRule } from "./KeywordTierRules";
import { type DimensionWeights, type TierBoundaries, type TokenThresholds } from "./heuristic_scoring_knobs";
import { type CustomDimensionRow } from "./custom_dimensions";
-import CompressionControls from "./CompressionControls";
import { type AutoRouterCompressionState, DEFAULT_AUTO_ROUTER_COMPRESSION } from "./buildAutoRouterCompression";
-import HeuristicKeywordOverrides from "./HeuristicKeywordOverrides";
-import HousekeepingRoutingControls from "./HousekeepingRoutingControls";
-import ReminderMarkers from "./ReminderMarkers";
import { type ReminderMarkerPair } from "./build_complexity_router_config";
export type { DimensionWeights, TierBoundaries, TokenThresholds };
@@ -782,167 +768,33 @@ const ComplexityRouterConfig: React.FC = ({
>
)}
- {[
- ...(!forecast
- ? [
- {
- key: "classifier",
- label:
Advanced: Classification Method,
- children: (
-
- ),
- },
- ]
- : []),
- ...(!forecast
- ? [
- {
- key: "keyword-overrides",
- label: (
-
Advanced: Heuristic Keyword Overrides
- ),
- children:
,
- },
- ]
- : []),
- {
- key: "adaptive",
- label:
Advanced: Adaptive Routing,
- children: (
-
-
-
- ),
- },
- {
- key: "affinity",
- label:
Advanced: Affinity,
- children:
,
- },
- {
- key: "modality",
- label:
Advanced: Modality Routing,
- children:
,
- },
- {
- key: "plan-mode",
- label:
Advanced: Plan-Mode Override,
- children: (
-
- ),
- },
- {
- key: "housekeeping",
- label:
Advanced: Housekeeping Routing,
- children:
,
- },
- {
- key: "reminder-markers",
- label:
Advanced: Reminder Markers,
- children:
,
- },
- {
- key: "context-window",
- label:
Advanced: Context Window Escalation,
- children:
,
- },
- {
- key: "stall-escalation",
- label:
Advanced: Stalled Task Escalation,
- children: (
-
-
-
- ),
- },
- {
- key: "response",
- label:
Advanced: Response Format,
- children:
,
- },
- ...(onEscalationKeywordsChange
- ? [
- {
- key: "escalation",
- label:
Advanced: Escalation Keywords,
- children: (
-
-
-
- ),
- },
- ]
- : []),
- ...(onAutoRouterCompressionChange
- ? [
- {
- key: "compression",
- label:
Advanced: Compression,
- children: (
-
- ),
- },
- ]
- : []),
- ...(onKeywordTierRulesChange || onSemanticMatchingEnabledChange
- ? [
- {
- key: "keyword-semantic",
- label: (
-
Advanced: Keyword/Semantic Matching
- ),
- children: (
- <>
- {onKeywordTierRulesChange && (
-
- )}
- {onKeywordTierRulesChange && onSemanticMatchingEnabledChange &&
}
- {onSemanticMatchingEnabledChange && (
-
- )}
- >
- ),
- },
- ]
- : []),
- ]
- .filter(({ key }) => !forecast || !["adaptive", "context-window", "escalation"].includes(key))
- .map(({ key, label, children }) => (
-
-
-
- {label}
-
- {children}
-
- ))}
+
diff --git a/ui/litellm-dashboard/src/components/add_model/ReminderMarkers.tsx b/ui/litellm-dashboard/src/components/add_model/ReminderMarkers.tsx
index 452f4dc727f..970394291d4 100644
--- a/ui/litellm-dashboard/src/components/add_model/ReminderMarkers.tsx
+++ b/ui/litellm-dashboard/src/components/add_model/ReminderMarkers.tsx
@@ -30,14 +30,13 @@ const ReminderMarkers: React.FC<{
{markers.map((marker, index) => (
-
+
update(index, { open: event.target.value })}
@@ -49,7 +48,6 @@ const ReminderMarkers: React.FC<{
update(index, { close: event.target.value })}
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 2fa28f761e3..f805a5b5511 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
@@ -28,10 +28,6 @@ import ComplexityRouterConfig, {
effectiveClassifierType,
usesLlmClassifier,
heuristicScoringRole,
- DEFAULT_ADAPTIVE_WEIGHTS,
- DEFAULT_SESSION_AFFINITY,
- DEFAULT_DEPLOYMENT_AFFINITY,
- DEFAULT_TIER_DISTANCE_PENALTY,
} from "./ComplexityRouterConfig";
import { KeywordTierRule } from "./KeywordTierRules";
import { customDimensionsError } from "./custom_dimensions";
@@ -57,6 +53,7 @@ import {
getTierLabelsError,
dryRunRejection,
} from "./build_complexity_router_config";
+import { builderParamsFromValue } from "./complexity_router_builder_params";
import { activeTierName, activeTierRows, getCustomTierRowsError, resolveComplexityDefaultModel } from "./tier_rows";
import { tierRowLabel } from "./complexity_router_tiers";
import { buildAutoRouterTestTargets, AutoRouterTestTarget } from "./build_auto_router_test_targets";
@@ -403,65 +400,13 @@ const AddAutoRouterTab: React.FC
= ({
);
const complexityRouterConfigParams: BuildComplexityRouterConfigParams = {
- tiers: complexityRouterConfig.tiers,
- enableNonReasoningTier: complexityRouterConfig.enable_non_reasoning_tier,
- customTierSet: complexityRouterConfig.custom_tier_set,
- defaultModel: complexityRouterConfig.default_model,
- planModeMinTier: complexityRouterConfig.plan_mode_min_tier,
- classificationPrompt: complexityRouterConfig.classification_prompt,
- classificationExamples: complexityRouterConfig.classification_examples,
- heuristicFirstMaxTier: complexityRouterConfig.heuristic_first_max_tier,
- hybridBoundaryMargin: complexityRouterConfig.hybrid_boundary_margin,
- classificationMode: complexityRouterConfig.classification_mode,
- tierLabels: complexityRouterConfig.tier_labels,
- classifierType: complexityRouterConfig.classifier_type,
- jevClassifierConfig: complexityRouterConfig.jev_classifier_config,
- heuristicV2SuccessThreshold: complexityRouterConfig.heuristic_v2_success_threshold,
- capabilityClassifierConfig: complexityRouterConfig.capability_classifier_config,
- llmV2Config: complexityRouterConfig.llm_v2_config,
- classifierLlmConfig: complexityRouterConfig.classifier_llm_config,
- classifierContextWindowSize: complexityRouterConfig.classifier_context_window_size,
- classifierContextBudgetChars: complexityRouterConfig.classifier_context_budget_chars,
- classifierContextPerTurnChars: complexityRouterConfig.classifier_context_per_turn_chars,
- classifierContextIncludeAssistantTurns: complexityRouterConfig.classifier_context_include_assistant_turns,
- classifierFallback: complexityRouterConfig.classifier_fallback,
- sessionAffinity: complexityRouterConfig.session_affinity ?? DEFAULT_SESSION_AFFINITY,
- modalityRouting: complexityRouterConfig.modality_routing ?? false,
- modalityPinOverride: complexityRouterConfig.modality_pin_override ?? false,
- deploymentAffinity: complexityRouterConfig.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
+ ...builderParamsFromValue(complexityRouterConfig),
customTechnicalKeywords,
keywordTierRules,
semanticMatchingEnabled,
embeddingModel,
matchThreshold,
escalationKeywords,
- stallEscalationEnabled: complexityRouterConfig.stall_escalation_enabled,
- stallEscalationWindow: complexityRouterConfig.stall_escalation_window,
- stallEscalationRepeatThreshold: complexityRouterConfig.stall_escalation_repeat_threshold,
- adaptive: complexityRouterConfig.adaptive ?? false,
- adaptiveWeights: complexityRouterConfig.adaptive_weights ?? DEFAULT_ADAPTIVE_WEIGHTS,
- tierDistancePenalty: complexityRouterConfig.tier_distance_penalty ?? DEFAULT_TIER_DISTANCE_PENALTY,
- adaptiveEligible: complexityRouterConfig.adaptive_eligible ?? "all",
- returnRawModelName: complexityRouterConfig.return_raw_model_name ?? false,
- tierModelParams: complexityRouterConfig.tier_model_params,
- tierBoundaries: complexityRouterConfig.tier_boundaries,
- tokenThresholds: complexityRouterConfig.token_thresholds,
- dimensionWeights: complexityRouterConfig.dimension_weights,
- customDimensions: complexityRouterConfig.custom_dimensions,
- reasoningOverrideMinScore: complexityRouterConfig.reasoning_override_min_score,
- enableContextWindowEscalation: complexityRouterConfig.enable_context_window_escalation,
- contextWindowEscalationBuffer: complexityRouterConfig.context_window_escalation_buffer,
- sessionAffinityTtlSeconds: complexityRouterConfig.session_affinity_ttl_seconds,
- codeKeywords: complexityRouterConfig.code_keywords,
- reasoningKeywords: complexityRouterConfig.reasoning_keywords,
- technicalKeywords: complexityRouterConfig.technical_keywords,
- simpleKeywords: complexityRouterConfig.simple_keywords,
- planModePatterns: complexityRouterConfig.plan_mode_patterns,
- routeHousekeepingToCheapestTier: complexityRouterConfig.route_housekeeping_to_cheapest_tier,
- housekeepingPatterns: complexityRouterConfig.housekeeping_patterns,
- reminderMarkers: complexityRouterConfig.reminder_markers,
- maxTokensFromTierModel: complexityRouterConfig.max_tokens_from_tier_model,
- classifierPluginTimeoutMs: complexityRouterConfig.classifier_plugin_timeout_ms,
};
const submitRecommendedRouter = async (name: string) => {
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 cf5164d91c4..6db2b8213d1 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
@@ -1531,6 +1531,22 @@ describe("advanced complexity router fields", () => {
expect(payload).not.toHaveProperty("classifier_plugin_timeout_ms");
});
+ it.each([
+ "code_keywords",
+ "reasoning_keywords",
+ "technical_keywords",
+ "simple_keywords",
+ "plan_mode_patterns",
+ "route_housekeeping_to_cheapest_tier",
+ "housekeeping_patterns",
+ "reminder_markers",
+ "max_tokens_from_tier_model",
+ "classifier_plugin_timeout_ms",
+ ])("omits unset advanced field %s", (key) => {
+ const payload = buildComplexityRouterConfig(baseParams);
+ expect(payload).not.toHaveProperty(key);
+ });
+
it("validates marker pairs and custom classifier timeout", () => {
expect(getReminderMarkersError([{ open: " ", close: " " }])).toContain("different");
expect(getReminderMarkersError([{ open: "", close: "" }])).toContain("needs both");
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 96fbe7f2c2e..ab71b6b3cce 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
@@ -744,6 +744,16 @@ export const buildComplexityRouterConfig = ({
open: open.trim().toLowerCase(),
close: close.trim().toLowerCase(),
}));
+ const cleanedLists = Object.fromEntries(
+ Object.entries({
+ code_keywords: cleanList(codeKeywords),
+ reasoning_keywords: cleanList(reasoningKeywords),
+ technical_keywords: cleanList(technicalKeywords),
+ simple_keywords: cleanList(simpleKeywords),
+ plan_mode_patterns: cleanList(planModePatterns),
+ housekeeping_patterns: cleanList(housekeepingPatterns),
+ }).filter(([, list]) => list !== undefined),
+ );
const supportsOpeningPrompt = !customTierSet && !forecast && usesLlmClassifier(effectiveType);
const payload: ComplexityRouterConfigPayload = {
@@ -812,13 +822,8 @@ export const buildComplexityRouterConfig = ({
...(sessionAffinityTtlSeconds !== undefined && {
session_affinity_ttl_seconds: sessionAffinityTtlSeconds,
}),
- ...(cleanList(codeKeywords) && { code_keywords: cleanList(codeKeywords) }),
- ...(cleanList(reasoningKeywords) && { reasoning_keywords: cleanList(reasoningKeywords) }),
- ...(cleanList(technicalKeywords) && { technical_keywords: cleanList(technicalKeywords) }),
- ...(cleanList(simpleKeywords) && { simple_keywords: cleanList(simpleKeywords) }),
- ...(cleanList(planModePatterns) && { plan_mode_patterns: cleanList(planModePatterns) }),
+ ...cleanedLists,
...(routeHousekeepingToCheapestTier === false && { route_housekeeping_to_cheapest_tier: false }),
- ...(cleanList(housekeepingPatterns) && { housekeeping_patterns: cleanList(housekeepingPatterns) }),
...(cleanedReminderMarkers && cleanedReminderMarkers.length > 0 && { reminder_markers: cleanedReminderMarkers }),
...(maxTokensFromTierModel === false && { max_tokens_from_tier_model: false }),
...(classifierType === "custom" &&
diff --git a/ui/litellm-dashboard/src/components/add_model/complexity_router_builder_params.ts b/ui/litellm-dashboard/src/components/add_model/complexity_router_builder_params.ts
new file mode 100644
index 00000000000..124a85ce9a3
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/complexity_router_builder_params.ts
@@ -0,0 +1,74 @@
+import type { BuildComplexityRouterConfigParams } from "./build_complexity_router_config";
+import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
+import {
+ DEFAULT_ADAPTIVE_WEIGHTS,
+ DEFAULT_DEPLOYMENT_AFFINITY,
+ DEFAULT_SESSION_AFFINITY,
+ DEFAULT_TIER_DISTANCE_PENALTY,
+} from "./ComplexityRouterConfig";
+
+export const builderParamsFromValue = (
+ value: ComplexityRouterConfigValue,
+): Omit<
+ BuildComplexityRouterConfigParams,
+ | "customTechnicalKeywords"
+ | "keywordTierRules"
+ | "semanticMatchingEnabled"
+ | "embeddingModel"
+ | "matchThreshold"
+ | "escalationKeywords"
+> => ({
+ tiers: value.tiers,
+ enableNonReasoningTier: value.enable_non_reasoning_tier,
+ customTierSet: value.custom_tier_set,
+ defaultModel: value.default_model,
+ planModeMinTier: value.plan_mode_min_tier,
+ classificationPrompt: value.classification_prompt,
+ classificationExamples: value.classification_examples,
+ heuristicFirstMaxTier: value.heuristic_first_max_tier,
+ hybridBoundaryMargin: value.hybrid_boundary_margin,
+ classificationMode: value.classification_mode,
+ tierLabels: value.tier_labels,
+ classifierType: value.classifier_type,
+ jevClassifierConfig: value.jev_classifier_config,
+ heuristicV2SuccessThreshold: value.heuristic_v2_success_threshold,
+ capabilityClassifierConfig: value.capability_classifier_config,
+ llmV2Config: value.llm_v2_config,
+ classifierLlmConfig: value.classifier_llm_config,
+ classifierContextWindowSize: value.classifier_context_window_size,
+ classifierContextBudgetChars: value.classifier_context_budget_chars,
+ classifierContextPerTurnChars: value.classifier_context_per_turn_chars,
+ classifierContextIncludeAssistantTurns: value.classifier_context_include_assistant_turns,
+ classifierFallback: value.classifier_fallback,
+ sessionAffinity: value.session_affinity ?? DEFAULT_SESSION_AFFINITY,
+ sessionAffinityTtlSeconds: value.session_affinity_ttl_seconds,
+ modalityRouting: value.modality_routing ?? false,
+ modalityPinOverride: value.modality_pin_override ?? false,
+ deploymentAffinity: value.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
+ adaptive: value.adaptive ?? false,
+ adaptiveWeights: value.adaptive_weights ?? DEFAULT_ADAPTIVE_WEIGHTS,
+ tierDistancePenalty: value.tier_distance_penalty ?? DEFAULT_TIER_DISTANCE_PENALTY,
+ adaptiveEligible: value.adaptive_eligible ?? "all",
+ returnRawModelName: value.return_raw_model_name ?? false,
+ tierBoundaries: value.tier_boundaries,
+ tokenThresholds: value.token_thresholds,
+ dimensionWeights: value.dimension_weights,
+ customDimensions: value.custom_dimensions,
+ reasoningOverrideMinScore: value.reasoning_override_min_score,
+ tierModelParams: value.tier_model_params,
+ enableContextWindowEscalation: value.enable_context_window_escalation,
+ contextWindowEscalationBuffer: value.context_window_escalation_buffer,
+ stallEscalationEnabled: value.stall_escalation_enabled,
+ stallEscalationWindow: value.stall_escalation_window,
+ stallEscalationRepeatThreshold: value.stall_escalation_repeat_threshold,
+ codeKeywords: value.code_keywords,
+ reasoningKeywords: value.reasoning_keywords,
+ technicalKeywords: value.technical_keywords,
+ simpleKeywords: value.simple_keywords,
+ planModePatterns: value.plan_mode_patterns,
+ routeHousekeepingToCheapestTier: value.route_housekeeping_to_cheapest_tier,
+ housekeepingPatterns: value.housekeeping_patterns,
+ reminderMarkers: value.reminder_markers,
+ maxTokensFromTierModel: value.max_tokens_from_tier_model,
+ classifierPluginTimeoutMs: value.classifier_plugin_timeout_ms,
+});
diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.integration.test.tsx b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.integration.test.tsx
index 34db61483cf..aa6cf92ceb9 100644
--- a/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.integration.test.tsx
@@ -347,6 +347,77 @@ describe("EditAutoRouterModal keyword matching", () => {
});
});
+describe("EditAutoRouterModal advanced field round trips", () => {
+ const storedAdvancedConfig = {
+ ...STORED_CONFIG,
+ route_housekeeping_to_cheapest_tier: false,
+ housekeeping_patterns: ["conversation title"],
+ reminder_markers: [{ open: "", close: "" }],
+ max_tokens_from_tier_model: false,
+ };
+
+ const renderAdvancedModal = (props: Partial> = {}) =>
+ renderModal({
+ modelData: {
+ ...MODEL_DATA,
+ litellm_params: { ...MODEL_DATA.litellm_params, complexity_router_config: storedAdvancedConfig },
+ },
+ ...props,
+ });
+
+ beforeEach(() => {
+ modelPatchUpdateCall.mockClear();
+ });
+
+ it("hydrates housekeeping and reminder fields, then omits the default max-token value after editing", async () => {
+ const user = userEvent.setup();
+ renderAdvancedModal();
+
+ await user.click(await screen.findByText("Advanced: Housekeeping Routing"));
+ expect(screen.getByRole("switch", { name: "Route housekeeping calls to the cheapest tier" })).not.toBeChecked();
+ expect(screen.getByRole("combobox", { name: "e.g., conversation title" })).toHaveValue("");
+
+ await user.click(screen.getByText("Advanced: Reminder Markers"));
+ expect(screen.getByLabelText("Opening delimiter")).toHaveValue("");
+ expect(screen.getByLabelText("Closing delimiter")).toHaveValue("");
+
+ await user.click(screen.getByText("Advanced: Response Format"));
+ const maxTokensSwitch = screen.getByRole("switch", { name: "Cap max_tokens at the tier model's output ceiling" });
+ await user.click(maxTokensSwitch);
+ await user.click(screen.getByRole("button", { name: /save changes/i }));
+ await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalledOnce());
+
+ expect(savedConfig()).not.toHaveProperty("max_tokens_from_tier_model");
+ expect(savedConfig()).toMatchObject({
+ route_housekeeping_to_cheapest_tier: false,
+ housekeeping_patterns: ["conversation title"],
+ reminder_markers: [{ open: "", close: "" }],
+ });
+ });
+
+ it("does not PATCH when the edit is cancelled", async () => {
+ const user = userEvent.setup();
+ const onCancel = vi.fn();
+ renderAdvancedModal({ onCancel });
+ await user.click(screen.getByRole("button", { name: /cancel/i }));
+ expect(onCancel).toHaveBeenCalledOnce();
+ expect(modelPatchUpdateCall).not.toHaveBeenCalled();
+ });
+
+ it("preserves all stored advanced fields through an untouched save", async () => {
+ const user = userEvent.setup();
+ renderAdvancedModal();
+ await user.click(screen.getByRole("button", { name: /save changes/i }));
+ await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalledOnce());
+ expect(savedConfig()).toMatchObject({
+ route_housekeeping_to_cheapest_tier: false,
+ housekeeping_patterns: ["conversation title"],
+ reminder_markers: [{ open: "", close: "" }],
+ max_tokens_from_tier_model: false,
+ });
+ });
+});
+
describe("EditAutoRouterModal classifier context window", () => {
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 476207553f7..e3a2df39b88 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
@@ -1,13 +1,9 @@
import AutoRouterClassifierTabs from "../add_model/AutoRouterClassifierTabs";
import { usesClassifierContext } from "../add_model/classifier_types";
-import { defaultJevClassifierConfig, jevClassifierConfigSchema } from "../add_model/jev_classifier_config";
-import type { StoredComplexityRouterConfig } from "../add_model/build_complexity_router_config";
export type { StoredComplexityRouterConfig } from "../add_model/build_complexity_router_config";
import {
getForecastConfigError,
isForecastClassifier,
- capabilitySettingsSchema,
- fuseSettingsSchema,
} from "../add_model/forecast_classifier_config";
import React, { useEffect, useMemo, useState } from "react";
import {
@@ -30,13 +26,10 @@ import ModelChoiceCombobox, { type ModelChoice } from "../add_model/ModelChoiceC
import { modelAvailableCall, modelPatchUpdateCall, validateAutoRouterConfig } from "../networking";
import { fetchAutoRouterModels, fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models";
import RouterConfigBuilder, { type RouterConfig, serializeRouterConfig } from "../add_model/RouterConfigBuilder";
-import { hydrateTierModelParams } from "../add_model/complexity_router_tiers";
import {
- type ActiveTierSet,
CUSTOM_TIER_OMITTED_KEYS,
activeTierRows,
getCustomTierRowsError,
- tierParamsByRowId,
resolveComplexityDefaultModel,
} from "../add_model/tier_rows";
import { isComplexityRouter } from "../add_model/auto_router_strategies";
@@ -53,10 +46,6 @@ import {
getSemanticConfigError,
getPlanModeTierError,
getTierLabelsError,
- hydrateBuiltInTiers,
- hydrateCustomTierSet,
- hydratePlanModeMinTier,
- hydrateTierLabels,
dryRunRejection,
} from "../add_model/build_complexity_router_config";
import { KeywordTierRule } from "../add_model/KeywordTierRules";
@@ -68,22 +57,14 @@ import {
hydrateAutoRouterCompression,
} from "../add_model/buildAutoRouterCompression";
import { hydrateKeywordTierRules } from "../add_model/complexity_router_keywords";
-import { customDimensionsError, hydrateCustomDimensions } from "../add_model/custom_dimensions";
-import {
- hydrateDimensionWeights,
- hydrateReasoningOverrideMinScore,
- hydrateTierBoundaries,
- hydrateTokenThresholds,
-} from "../add_model/heuristic_scoring_knobs";
+import { customDimensionsError } from "../add_model/custom_dimensions";
import ComplexityRouterConfig, {
ComplexityRouterConfigValue,
effectiveClassifierType,
heuristicScoringRole,
- DEFAULT_ADAPTIVE_WEIGHTS,
- DEFAULT_SESSION_AFFINITY,
- DEFAULT_DEPLOYMENT_AFFINITY,
- DEFAULT_TIER_DISTANCE_PENALTY,
} from "../add_model/ComplexityRouterConfig";
+import { builderParamsFromValue } from "../add_model/complexity_router_builder_params";
+import { hydrateComplexityRouterConfig, hydratePinnedDefaultModel } from "./hydrate_complexity_router_config";
import {
Dialog,
DialogContent,
@@ -106,151 +87,7 @@ interface EditAutoRouterModalProps {
// Keys this modal rewrites from its own form state on save. Anything absent from this set is
// carried through untouched from the stored config, so a key only belongs here once the modal
// actually renders a control that can set it.
-
-/**
- * The stored complexity_router_config as form state. Every key in MANAGED_COMPLEXITY_ROUTER_KEYS is
- * rewritten from this state on save, so a key missing here is silently dropped from the saved config.
- */
-export const hydrateComplexityRouterConfig = (
- parsedConfig: StoredComplexityRouterConfig,
- complexityRouterDefaultModel: string | null | undefined,
-): ComplexityRouterConfigValue => {
- const stringList = (input: unknown): string[] | undefined =>
- Array.isArray(input) ? input.filter((item): item is string => typeof item === "string") : undefined;
- const builtIn = hydrateBuiltInTiers(parsedConfig.tiers, parsedConfig.enable_non_reasoning_tier);
- const { tiers: hydratedTiers, enable_non_reasoning_tier } = builtIn;
- const custom_tier_set = hydrateCustomTierSet(parsedConfig);
- const activeTiers = { ...builtIn, custom_tier_set };
-
- return {
- tiers: hydratedTiers,
- enable_non_reasoning_tier,
- custom_tier_set,
- tier_model_params: tierParamsByRowId(
- hydrateTierModelParams(parsedConfig.tiers, parsedConfig.tier_model_configs),
- activeTierRows(activeTiers),
- ),
- default_model: hydratePinnedDefaultModel(parsedConfig.default_model, complexityRouterDefaultModel, activeTiers),
- plan_mode_min_tier: hydratePlanModeMinTier(parsedConfig.plan_mode_min_tier, custom_tier_set),
- tier_labels: hydrateTierLabels(parsedConfig.tier_labels),
- classifier_type: parsedConfig.classifier_type || "heuristic",
- heuristic_v2_success_threshold:
- typeof parsedConfig.heuristic_v2_success_threshold === "number"
- ? parsedConfig.heuristic_v2_success_threshold
- : undefined,
- capability_classifier_config: capabilitySettingsSchema.safeParse(parsedConfig.capability_classifier_config).data,
- llm_v2_config: fuseSettingsSchema.safeParse(parsedConfig.llm_v2_config).data,
- classifier_llm_config: parsedConfig.classifier_type === "jev" ? undefined : parsedConfig.classifier_llm_config,
- jev_classifier_config:
- parsedConfig.classifier_type === "jev"
- ? jevClassifierConfigSchema.safeParse(parsedConfig.jev_classifier_config ?? {}).data ??
- defaultJevClassifierConfig()
- : undefined,
- classifier_context_window_size:
- typeof parsedConfig.classifier_context_window_size === "number"
- ? parsedConfig.classifier_context_window_size
- : undefined,
- classifier_context_budget_chars:
- typeof parsedConfig.classifier_context_budget_chars === "number"
- ? parsedConfig.classifier_context_budget_chars
- : undefined,
- classifier_context_per_turn_chars:
- typeof parsedConfig.classifier_context_per_turn_chars === "number"
- ? parsedConfig.classifier_context_per_turn_chars
- : undefined,
- classifier_context_include_assistant_turns:
- typeof parsedConfig.classifier_context_include_assistant_turns === "boolean"
- ? parsedConfig.classifier_context_include_assistant_turns
- : undefined,
- classifier_fallback:
- parsedConfig.classifier_fallback === "default_model" || parsedConfig.classifier_fallback === "heuristic"
- ? parsedConfig.classifier_fallback
- : undefined,
- classification_prompt:
- typeof parsedConfig.classification_prompt === "string" && parsedConfig.classification_prompt.trim() !== ""
- ? parsedConfig.classification_prompt
- : undefined,
- classification_examples:
- typeof parsedConfig.classification_examples === "string" && parsedConfig.classification_examples.trim() !== ""
- ? parsedConfig.classification_examples
- : undefined,
- heuristic_first_max_tier:
- typeof parsedConfig.heuristic_first_max_tier === "string" && parsedConfig.heuristic_first_max_tier.trim() !== ""
- ? parsedConfig.heuristic_first_max_tier
- : undefined,
- hybrid_boundary_margin:
- typeof parsedConfig.hybrid_boundary_margin === "number" ? parsedConfig.hybrid_boundary_margin : undefined,
- classification_mode:
- parsedConfig.classification_mode === "user_turn" || parsedConfig.classification_mode === "every_request"
- ? parsedConfig.classification_mode
- : undefined,
- tier_boundaries: hydrateTierBoundaries(parsedConfig.tier_boundaries),
- token_thresholds: hydrateTokenThresholds(parsedConfig.token_thresholds),
- dimension_weights: hydrateDimensionWeights(parsedConfig.dimension_weights),
- custom_dimensions: hydrateCustomDimensions(parsedConfig.custom_dimensions),
- reasoning_override_min_score: hydrateReasoningOverrideMinScore(parsedConfig.reasoning_override_min_score),
- session_affinity:
- typeof parsedConfig.session_affinity === "boolean" ? parsedConfig.session_affinity : DEFAULT_SESSION_AFFINITY,
- session_affinity_ttl_seconds:
- typeof parsedConfig.session_affinity_ttl_seconds === "number" &&
- Number.isFinite(parsedConfig.session_affinity_ttl_seconds)
- ? parsedConfig.session_affinity_ttl_seconds
- : undefined,
- modality_routing: typeof parsedConfig.modality_routing === "boolean" ? parsedConfig.modality_routing : false,
- modality_pin_override:
- typeof parsedConfig.modality_pin_override === "boolean" ? parsedConfig.modality_pin_override : false,
- 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,
- adaptive_eligible: parsedConfig.adaptive_eligible || "all",
- return_raw_model_name: parsedConfig.return_raw_model_name || false,
- enable_context_window_escalation:
- typeof parsedConfig.enable_context_window_escalation === "boolean"
- ? parsedConfig.enable_context_window_escalation
- : undefined,
- context_window_escalation_buffer:
- typeof parsedConfig.context_window_escalation_buffer === "number"
- ? parsedConfig.context_window_escalation_buffer
- : undefined,
- stall_escalation_enabled: parsedConfig.stall_escalation_enabled === true || undefined,
- stall_escalation_window:
- typeof parsedConfig.stall_escalation_window === "number" ? parsedConfig.stall_escalation_window : undefined,
- stall_escalation_repeat_threshold:
- typeof parsedConfig.stall_escalation_repeat_threshold === "number"
- ? parsedConfig.stall_escalation_repeat_threshold
- : undefined,
- code_keywords: stringList(parsedConfig.code_keywords),
- reasoning_keywords: stringList(parsedConfig.reasoning_keywords),
- technical_keywords: stringList(parsedConfig.technical_keywords),
- simple_keywords: stringList(parsedConfig.simple_keywords),
- plan_mode_patterns: stringList(parsedConfig.plan_mode_patterns),
- route_housekeeping_to_cheapest_tier:
- typeof parsedConfig.route_housekeeping_to_cheapest_tier === "boolean"
- ? parsedConfig.route_housekeeping_to_cheapest_tier
- : undefined,
- housekeeping_patterns: stringList(parsedConfig.housekeeping_patterns),
- reminder_markers: Array.isArray(parsedConfig.reminder_markers)
- ? parsedConfig.reminder_markers.filter(
- (pair): pair is { open: string; close: string } =>
- typeof pair === "object" &&
- pair !== null &&
- typeof (pair as { open?: unknown }).open === "string" &&
- typeof (pair as { close?: unknown }).close === "string",
- )
- : undefined,
- max_tokens_from_tier_model:
- typeof parsedConfig.max_tokens_from_tier_model === "boolean" ? parsedConfig.max_tokens_from_tier_model : undefined,
- classifier_plugin_timeout_ms:
- typeof parsedConfig.classifier_plugin_timeout_ms === "number" && Number.isFinite(parsedConfig.classifier_plugin_timeout_ms)
- ? parsedConfig.classifier_plugin_timeout_ms
- : undefined,
- };
-};
-
+export { hydrateComplexityRouterConfig, hydratePinnedDefaultModel };
export const MANAGED_COMPLEXITY_ROUTER_KEYS = new Set([
"tiers",
"enable_non_reasoning_tier",
@@ -324,24 +161,6 @@ const toRecord = (value: unknown): Record => {
: {};
};
-// A pin lives in two places: complexity_router_config.default_model (this UI's own marker, added
-// by PR #36615) and litellm_params.complexity_router_default_model (what the backend reads). Only
-// the marker proves an operator picked it, because before #36615 every save wrote a tier-derived
-// value into litellm_params. So with no marker, a litellm_params value counts as a pin only when
-// it diverges from what the tiers alone derive; a match stays unpinned and keeps tracking tiers.
-export const hydratePinnedDefaultModel = (
- storedConfigDefaultModel: unknown,
- litellmParamsDefaultModel: string | null | undefined,
- activeTiers: ActiveTierSet,
-): string | undefined => {
- if (typeof storedConfigDefaultModel === "string" && storedConfigDefaultModel.trim()) {
- return storedConfigDefaultModel;
- }
- const tierDerived = resolveComplexityDefaultModel(activeTiers);
- const externalOverride = litellmParamsDefaultModel?.trim();
- return externalOverride && externalOverride !== tierDerived ? externalOverride : undefined;
-};
-
export interface KeywordMatchingState {
keywordTierRules: KeywordTierRule[];
escalationKeywords: string[];
@@ -377,65 +196,13 @@ export const buildUpdatedComplexityRouterConfig = (
);
const builderParams: BuildComplexityRouterConfigParams = {
- tiers: value.tiers,
- enableNonReasoningTier: value.enable_non_reasoning_tier,
- customTierSet: value.custom_tier_set,
- defaultModel: value.default_model,
- planModeMinTier: value.plan_mode_min_tier,
- classificationPrompt: value.classification_prompt,
- classificationExamples: value.classification_examples,
- heuristicFirstMaxTier: value.heuristic_first_max_tier,
- hybridBoundaryMargin: value.hybrid_boundary_margin,
- classificationMode: value.classification_mode,
- tierLabels: value.tier_labels,
- classifierType: value.classifier_type,
- jevClassifierConfig: value.jev_classifier_config,
- heuristicV2SuccessThreshold: value.heuristic_v2_success_threshold,
- capabilityClassifierConfig: value.capability_classifier_config,
- llmV2Config: value.llm_v2_config,
- classifierLlmConfig: value.classifier_llm_config,
- classifierContextWindowSize: value.classifier_context_window_size,
- classifierContextBudgetChars: value.classifier_context_budget_chars,
- classifierContextPerTurnChars: value.classifier_context_per_turn_chars,
- classifierContextIncludeAssistantTurns: value.classifier_context_include_assistant_turns,
- classifierFallback: value.classifier_fallback,
- sessionAffinity: value.session_affinity ?? DEFAULT_SESSION_AFFINITY,
- sessionAffinityTtlSeconds: value.session_affinity_ttl_seconds,
- modalityRouting: value.modality_routing ?? false,
- modalityPinOverride: value.modality_pin_override ?? false,
- deploymentAffinity: value.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
+ ...builderParamsFromValue(value),
customTechnicalKeywords: customTechnicalKeywords ?? [],
keywordTierRules: keywordMatching?.keywordTierRules ?? [],
semanticMatchingEnabled: keywordMatching?.semanticMatchingEnabled ?? false,
embeddingModel: keywordMatching?.embeddingModel,
matchThreshold: keywordMatching?.matchThreshold ?? DEFAULT_MATCH_THRESHOLD,
escalationKeywords: keywordMatching?.escalationKeywords ?? [],
- adaptive: value.adaptive ?? false,
- adaptiveWeights: value.adaptive_weights ?? DEFAULT_ADAPTIVE_WEIGHTS,
- tierDistancePenalty: value.tier_distance_penalty ?? DEFAULT_TIER_DISTANCE_PENALTY,
- adaptiveEligible: value.adaptive_eligible ?? "all",
- returnRawModelName: value.return_raw_model_name ?? false,
- tierBoundaries: value.tier_boundaries,
- tokenThresholds: value.token_thresholds,
- dimensionWeights: value.dimension_weights,
- customDimensions: value.custom_dimensions,
- reasoningOverrideMinScore: value.reasoning_override_min_score,
- tierModelParams: value.tier_model_params,
- enableContextWindowEscalation: value.enable_context_window_escalation,
- contextWindowEscalationBuffer: value.context_window_escalation_buffer,
- stallEscalationEnabled: value.stall_escalation_enabled,
- stallEscalationWindow: value.stall_escalation_window,
- stallEscalationRepeatThreshold: value.stall_escalation_repeat_threshold,
- codeKeywords: value.code_keywords,
- reasoningKeywords: value.reasoning_keywords,
- technicalKeywords: value.technical_keywords,
- simpleKeywords: value.simple_keywords,
- planModePatterns: value.plan_mode_patterns,
- routeHousekeepingToCheapestTier: value.route_housekeeping_to_cheapest_tier,
- housekeepingPatterns: value.housekeeping_patterns,
- reminderMarkers: value.reminder_markers,
- maxTokensFromTierModel: value.max_tokens_from_tier_model,
- classifierPluginTimeoutMs: value.classifier_plugin_timeout_ms,
};
const built = buildComplexityRouterConfig(builderParams);
diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/hydrate_complexity_router_config.ts b/ui/litellm-dashboard/src/components/edit_auto_router/hydrate_complexity_router_config.ts
new file mode 100644
index 00000000000..c7c7bcb3382
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/edit_auto_router/hydrate_complexity_router_config.ts
@@ -0,0 +1,183 @@
+import { defaultJevClassifierConfig, jevClassifierConfigSchema } from "../add_model/jev_classifier_config";
+import { capabilitySettingsSchema, fuseSettingsSchema } from "../add_model/forecast_classifier_config";
+import type { StoredComplexityRouterConfig } from "../add_model/build_complexity_router_config";
+import {
+ hydrateBuiltInTiers,
+ hydrateCustomTierSet,
+ hydratePlanModeMinTier,
+ hydrateTierLabels,
+} from "../add_model/build_complexity_router_config";
+import { hydrateTierModelParams } from "../add_model/complexity_router_tiers";
+import { hydrateCustomDimensions } from "../add_model/custom_dimensions";
+import {
+ hydrateDimensionWeights,
+ hydrateReasoningOverrideMinScore,
+ hydrateTierBoundaries,
+ hydrateTokenThresholds,
+} from "../add_model/heuristic_scoring_knobs";
+import type { ComplexityRouterConfigValue } from "../add_model/ComplexityRouterConfig";
+import { DEFAULT_DEPLOYMENT_AFFINITY, DEFAULT_SESSION_AFFINITY } from "../add_model/ComplexityRouterConfig";
+import {
+ type ActiveTierSet,
+ activeTierRows,
+ tierParamsByRowId,
+ resolveComplexityDefaultModel,
+} from "../add_model/tier_rows";
+
+const isReminderMarkerPair = (
+ input: unknown,
+): input is { open: string; close: string } =>
+ typeof input === "object" &&
+ input !== null &&
+ "open" in input &&
+ "close" in input &&
+ typeof input.open === "string" &&
+ typeof input.close === "string";
+
+const stringList = (input: unknown): string[] | undefined =>
+ Array.isArray(input) ? input.filter((item): item is string => typeof item === "string") : undefined;
+
+export const hydratePinnedDefaultModel = (
+ storedConfigDefaultModel: unknown,
+ litellmParamsDefaultModel: string | null | undefined,
+ activeTiers: ActiveTierSet,
+): string | undefined => {
+ if (typeof storedConfigDefaultModel === "string" && storedConfigDefaultModel.trim()) {
+ return storedConfigDefaultModel;
+ }
+ const tierDerived = resolveComplexityDefaultModel(activeTiers);
+ const externalOverride = litellmParamsDefaultModel?.trim();
+ return externalOverride && externalOverride !== tierDerived ? externalOverride : undefined;
+};
+
+export const hydrateComplexityRouterConfig = (
+ parsedConfig: StoredComplexityRouterConfig,
+ complexityRouterDefaultModel: string | null | undefined,
+): ComplexityRouterConfigValue => {
+ const builtIn = hydrateBuiltInTiers(parsedConfig.tiers, parsedConfig.enable_non_reasoning_tier);
+ const { tiers: hydratedTiers, enable_non_reasoning_tier } = builtIn;
+ const custom_tier_set = hydrateCustomTierSet(parsedConfig);
+ const activeTiers = { ...builtIn, custom_tier_set };
+
+ return {
+ tiers: hydratedTiers,
+ enable_non_reasoning_tier,
+ custom_tier_set,
+ tier_model_params: tierParamsByRowId(
+ hydrateTierModelParams(parsedConfig.tiers, parsedConfig.tier_model_configs),
+ activeTierRows(activeTiers),
+ ),
+ default_model: hydratePinnedDefaultModel(parsedConfig.default_model, complexityRouterDefaultModel, activeTiers),
+ plan_mode_min_tier: hydratePlanModeMinTier(parsedConfig.plan_mode_min_tier, custom_tier_set),
+ tier_labels: hydrateTierLabels(parsedConfig.tier_labels),
+ classifier_type: parsedConfig.classifier_type || "heuristic",
+ heuristic_v2_success_threshold:
+ typeof parsedConfig.heuristic_v2_success_threshold === "number"
+ ? parsedConfig.heuristic_v2_success_threshold
+ : undefined,
+ capability_classifier_config: capabilitySettingsSchema.safeParse(parsedConfig.capability_classifier_config).data,
+ llm_v2_config: fuseSettingsSchema.safeParse(parsedConfig.llm_v2_config).data,
+ classifier_llm_config: parsedConfig.classifier_type === "jev" ? undefined : parsedConfig.classifier_llm_config,
+ jev_classifier_config:
+ parsedConfig.classifier_type === "jev"
+ ? jevClassifierConfigSchema.safeParse(parsedConfig.jev_classifier_config ?? {}).data ??
+ defaultJevClassifierConfig()
+ : undefined,
+ classifier_context_window_size:
+ typeof parsedConfig.classifier_context_window_size === "number"
+ ? parsedConfig.classifier_context_window_size
+ : undefined,
+ classifier_context_budget_chars:
+ typeof parsedConfig.classifier_context_budget_chars === "number"
+ ? parsedConfig.classifier_context_budget_chars
+ : undefined,
+ classifier_context_per_turn_chars:
+ typeof parsedConfig.classifier_context_per_turn_chars === "number"
+ ? parsedConfig.classifier_context_per_turn_chars
+ : undefined,
+ classifier_context_include_assistant_turns:
+ typeof parsedConfig.classifier_context_include_assistant_turns === "boolean"
+ ? parsedConfig.classifier_context_include_assistant_turns
+ : undefined,
+ classifier_fallback:
+ parsedConfig.classifier_fallback === "default_model" || parsedConfig.classifier_fallback === "heuristic"
+ ? parsedConfig.classifier_fallback
+ : undefined,
+ classification_prompt:
+ typeof parsedConfig.classification_prompt === "string" && parsedConfig.classification_prompt.trim() !== ""
+ ? parsedConfig.classification_prompt
+ : undefined,
+ classification_examples:
+ typeof parsedConfig.classification_examples === "string" && parsedConfig.classification_examples.trim() !== ""
+ ? parsedConfig.classification_examples
+ : undefined,
+ heuristic_first_max_tier:
+ typeof parsedConfig.heuristic_first_max_tier === "string" && parsedConfig.heuristic_first_max_tier.trim() !== ""
+ ? parsedConfig.heuristic_first_max_tier
+ : undefined,
+ hybrid_boundary_margin:
+ typeof parsedConfig.hybrid_boundary_margin === "number" ? parsedConfig.hybrid_boundary_margin : undefined,
+ classification_mode:
+ parsedConfig.classification_mode === "user_turn" || parsedConfig.classification_mode === "every_request"
+ ? parsedConfig.classification_mode
+ : undefined,
+ tier_boundaries: hydrateTierBoundaries(parsedConfig.tier_boundaries),
+ token_thresholds: hydrateTokenThresholds(parsedConfig.token_thresholds),
+ dimension_weights: hydrateDimensionWeights(parsedConfig.dimension_weights),
+ custom_dimensions: hydrateCustomDimensions(parsedConfig.custom_dimensions),
+ reasoning_override_min_score: hydrateReasoningOverrideMinScore(parsedConfig.reasoning_override_min_score),
+ session_affinity:
+ typeof parsedConfig.session_affinity === "boolean" ? parsedConfig.session_affinity : DEFAULT_SESSION_AFFINITY,
+ session_affinity_ttl_seconds:
+ typeof parsedConfig.session_affinity_ttl_seconds === "number" &&
+ Number.isFinite(parsedConfig.session_affinity_ttl_seconds)
+ ? parsedConfig.session_affinity_ttl_seconds
+ : undefined,
+ modality_routing: typeof parsedConfig.modality_routing === "boolean" ? parsedConfig.modality_routing : false,
+ modality_pin_override:
+ typeof parsedConfig.modality_pin_override === "boolean" ? parsedConfig.modality_pin_override : false,
+ 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,
+ adaptive_eligible: parsedConfig.adaptive_eligible || "all",
+ return_raw_model_name: parsedConfig.return_raw_model_name || false,
+ enable_context_window_escalation:
+ typeof parsedConfig.enable_context_window_escalation === "boolean"
+ ? parsedConfig.enable_context_window_escalation
+ : undefined,
+ context_window_escalation_buffer:
+ typeof parsedConfig.context_window_escalation_buffer === "number"
+ ? parsedConfig.context_window_escalation_buffer
+ : undefined,
+ stall_escalation_enabled: parsedConfig.stall_escalation_enabled === true || undefined,
+ stall_escalation_window:
+ typeof parsedConfig.stall_escalation_window === "number" ? parsedConfig.stall_escalation_window : undefined,
+ stall_escalation_repeat_threshold:
+ typeof parsedConfig.stall_escalation_repeat_threshold === "number"
+ ? parsedConfig.stall_escalation_repeat_threshold
+ : undefined,
+ code_keywords: stringList(parsedConfig.code_keywords),
+ reasoning_keywords: stringList(parsedConfig.reasoning_keywords),
+ technical_keywords: stringList(parsedConfig.technical_keywords),
+ simple_keywords: stringList(parsedConfig.simple_keywords),
+ plan_mode_patterns: stringList(parsedConfig.plan_mode_patterns),
+ route_housekeeping_to_cheapest_tier:
+ typeof parsedConfig.route_housekeeping_to_cheapest_tier === "boolean"
+ ? parsedConfig.route_housekeeping_to_cheapest_tier
+ : undefined,
+ housekeeping_patterns: stringList(parsedConfig.housekeeping_patterns),
+ reminder_markers: Array.isArray(parsedConfig.reminder_markers)
+ ? parsedConfig.reminder_markers.filter(isReminderMarkerPair)
+ : undefined,
+ max_tokens_from_tier_model:
+ typeof parsedConfig.max_tokens_from_tier_model === "boolean" ? parsedConfig.max_tokens_from_tier_model : undefined,
+ classifier_plugin_timeout_ms:
+ typeof parsedConfig.classifier_plugin_timeout_ms === "number" && Number.isFinite(parsedConfig.classifier_plugin_timeout_ms)
+ ? parsedConfig.classifier_plugin_timeout_ms
+ : undefined,
+ };
+};