From 662a89290f3b0089a774eaf13094512a1d46ca0f Mon Sep 17 00:00:00 2001 From: mateo Date: Wed, 23 Sep 2026 00:50:58 +0000 Subject: [PATCH] fix(ui): adapt the jev dashboard pieces to stable/1.101.x --- .../add_model/ComplexityRouterConfig.tsx | 44 +------------ .../build_complexity_router_config.test.ts | 2 - .../build_complexity_router_config.ts | 6 +- .../components/add_model/tier_descriptions.ts | 37 +++++++++++ .../edit_auto_router_modal.tsx | 62 ++++++------------- .../LogDetailsDrawer/RoutingDecisionCard.tsx | 36 ++++++----- 6 files changed, 86 insertions(+), 101 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/add_model/tier_descriptions.ts diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx index fcd6d3bca79..27077b0086b 100644 --- a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.tsx @@ -1,5 +1,6 @@ import type { JevClassifierConfig } from "./jev_classifier_config"; -import { type ClassifierType } from "./classifier_types"; +import { type ClassifierType, usesLlmClassifier } from "./classifier_types"; +import { TIER_DESCRIPTIONS, TIER_KEYS } from "./tier_descriptions"; export { type ClassifierType, usesLlmClassifier, usesClassifierContext } from "./classifier_types"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { MultiSelect } from "@/components/shared/MultiSelect"; @@ -142,16 +143,6 @@ export interface ClassifierLLMConfig { system_prompt?: string; } -export type ClassifierType = "heuristic" | "heuristic_v2" | "llm" | "heuristic_first" | "hybrid"; - -/** - * Whether this router can call classifier_llm_config.model. Mirrors the backend's - * ComplexityRouterConfig.uses_llm_classifier, and is the single gate for every classifier-only - * control and payload key, so a new chaining type cannot strip knobs the operator set. - */ -export const usesLlmClassifier = (classifierType: ClassifierType): boolean => - classifierType === "llm" || classifierType === "heuristic_first" || classifierType === "hybrid"; - export type ClassifierFallback = "heuristic" | "default_model"; export const DEFAULT_CLASSIFIER_FALLBACK: ClassifierFallback = "heuristic"; @@ -498,36 +489,7 @@ interface ComplexityRouterConfigProps { showValidationErrors?: boolean; } -export const TIER_DESCRIPTIONS: Record< - keyof ComplexityTiers, - { label: string; description: string; examples: string } -> = { - SIMPLE: { - label: "Simple", - description: "Basic questions, greetings, simple factual queries", - examples: '"Hello!", "What is Python?", "Thanks!"', - }, - MEDIUM: { - label: "Medium", - description: "Standard queries requiring some reasoning or explanation", - examples: '"Explain how REST APIs work", "Debug this error"', - }, - COMPLEX: { - label: "Complex", - description: "Technical, multi-part requests requiring deep knowledge", - examples: '"Design a microservices architecture", "Implement a rate limiter"', - }, - REASONING: { - label: "Reasoning", - description: "Chain-of-thought, analysis, explicit reasoning requests", - examples: '"Think step by step...", "Analyze the pros and cons..."', - }, -}; - -export const TIER_KEYS = Object.keys(TIER_DESCRIPTIONS) as Array; - -export const effectiveTierLabel = (tier: keyof ComplexityTiers, tierLabels: ComplexityTierLabels | undefined): string => - tierLabels?.[tier]?.trim() || TIER_DESCRIPTIONS[tier].label; +export { TIER_DESCRIPTIONS, TIER_KEYS, effectiveTierLabel } from "./tier_descriptions"; export const DEFAULT_HEURISTIC_FIRST_MAX_TIER = "SIMPLE"; 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 e8181568f1e..2341f3c8b25 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 @@ -803,8 +803,6 @@ describe("buildComplexityRouterConfig scorer knobs", () => { expect(buildComplexityRouterConfig(tuned).tier_boundaries).toEqual(BOUNDARIES); }); - const uncheckedParams: unknown = { - const payload = buildComplexityRouterConfig(uncheckedParams as BuildComplexityRouterConfigParams); it("drops them when the classifier falls back to the default model and nothing is scored", () => { expect(buildComplexityRouterConfig(llmWithDefaultFallback)).not.toHaveProperty("tier_boundaries"); }); 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 c04050fb6d6..ed14d9e138f 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 @@ -124,6 +124,7 @@ const scorerKnobPayload = ({ }; export interface BuildComplexityRouterConfigParams { tiers: ComplexityTiers; + enableNonReasoningTier?: boolean; customTierSet?: CustomTierSet; defaultModel: string | undefined; planModeMinTier: string | undefined; @@ -187,6 +188,7 @@ export interface TierDefinitionPayload { export interface ComplexityRouterConfigPayload { tiers: ComplexityTiers | Record; + enable_non_reasoning_tier?: boolean; tier_definitions?: TierDefinitionPayload[]; fallback_tier?: string; default_model?: string; @@ -511,6 +513,7 @@ export const hydrateBuiltInTiers = ( export const buildComplexityRouterConfig = ({ tiers, + enableNonReasoningTier, customTierSet, defaultModel, planModeMinTier, @@ -585,11 +588,12 @@ export const buildComplexityRouterConfig = ({ }; // An edited tier set forces the LLM classifier, so llm-only inputs must survive a classifier_type // the form never rewrote. The UI gates the same controls on this, not on the raw value. - const effectiveType: ClassifierType = customTierSet ? "llm" : classifierType; const effectiveType = effectiveClassifierType({ custom_tier_set: customTierSet, classifier_type: classifierType }); const payload: ComplexityRouterConfigPayload = { tiers, + // The backend rejects the flag beside a custom tier set. + ...(!customTierSet && enableNonReasoningTier && { enable_non_reasoning_tier: true }), ...(serializedTierModelConfigs && { tier_model_configs: serializedTierModelConfigs }), ...(defaultModel?.trim() && { default_model: defaultModel }), ...(planModeMinTier?.trim() && { plan_mode_min_tier: planModeMinTier }), diff --git a/ui/litellm-dashboard/src/components/add_model/tier_descriptions.ts b/ui/litellm-dashboard/src/components/add_model/tier_descriptions.ts new file mode 100644 index 00000000000..11bab473aa7 --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/tier_descriptions.ts @@ -0,0 +1,37 @@ +import type { ComplexityTierLabels, ComplexityTiers } from "./ComplexityRouterConfig"; + +export const TIER_DESCRIPTIONS: Record< + keyof ComplexityTiers, + { label: string; description: string; examples: string } +> = { + NON_REASONING: { + label: "Non-reasoning", + description: "Operational relay work: passing information along with no judgment about it", + examples: '"Reformat this tool output", "Acknowledge the write succeeded"', + }, + SIMPLE: { + label: "Simple", + description: "Basic questions, greetings, simple factual queries", + examples: '"Hello!", "What is Python?", "Thanks!"', + }, + MEDIUM: { + label: "Medium", + description: "Standard queries requiring some reasoning or explanation", + examples: '"Explain how REST APIs work", "Debug this error"', + }, + COMPLEX: { + label: "Complex", + description: "Technical, multi-part requests requiring deep knowledge", + examples: '"Design a microservices architecture", "Implement a rate limiter"', + }, + REASONING: { + label: "Reasoning", + description: "Chain-of-thought, analysis, explicit reasoning requests", + examples: '"Think step by step...", "Analyze the pros and cons..."', + }, +}; + +export const TIER_KEYS = Object.keys(TIER_DESCRIPTIONS) as Array; + +export const effectiveTierLabel = (tier: keyof ComplexityTiers, tierLabels: ComplexityTierLabels | undefined): string => + tierLabels?.[tier]?.trim() || TIER_DESCRIPTIONS[tier].label; 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 86556100615..d31a9f4c51c 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 @@ -16,7 +16,7 @@ import ModelChoiceCombobox, { type ModelChoice } from "../add_model/ModelChoiceC import { modelAvailableCall, modelPatchUpdateCall, validateAutoRouterConfig } from "../networking"; import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models"; import RouterConfigBuilder from "../add_model/RouterConfigBuilder"; -import { hydrateTierModelParams, normalizeTierModels } from "../add_model/complexity_router_tiers"; +import { hydrateTierModelParams } from "../add_model/complexity_router_tiers"; import { type ActiveTierSet, CUSTOM_TIER_OMITTED_KEYS, @@ -63,9 +63,8 @@ import ComplexityRouterConfig, { ClassifierLLMConfig, ClassifierType, ComplexityRouterConfigValue, - + ComplexityTiers, effectiveClassifierType, - heuristicScoringRole, DEFAULT_ADAPTIVE_WEIGHTS, DEFAULT_SESSION_AFFINITY, DEFAULT_DEPLOYMENT_AFFINITY, @@ -139,6 +138,11 @@ export interface StoredComplexityRouterConfig { * 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. */ +const asNumber = (v: unknown): number | undefined => (typeof v === "number" ? v : undefined); +const asBoolean = (v: unknown): boolean | undefined => (typeof v === "boolean" ? v : undefined); +const asNonEmptyString = (v: unknown): string | undefined => + typeof v === "string" && v.trim() !== "" ? v : undefined; + export const hydrateComplexityRouterConfig = ( parsedConfig: StoredComplexityRouterConfig, complexityRouterDefaultModel: string | null | undefined, @@ -166,38 +170,19 @@ export const hydrateComplexityRouterConfig = ( ? 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_context_window_size: asNumber(parsedConfig.classifier_context_window_size), + classifier_context_budget_chars: asNumber(parsedConfig.classifier_context_budget_chars), + classifier_context_per_turn_chars: asNumber(parsedConfig.classifier_context_per_turn_chars), + classifier_context_include_assistant_turns: asBoolean( + parsedConfig.classifier_context_include_assistant_turns, + ), 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, + classification_prompt: asNonEmptyString(parsedConfig.classification_prompt), + classification_examples: asNonEmptyString(parsedConfig.classification_examples), + heuristic_first_max_tier: asNonEmptyString(parsedConfig.heuristic_first_max_tier), hybrid_boundary_margin: typeof parsedConfig.hybrid_boundary_margin === "number" ? parsedConfig.hybrid_boundary_margin : undefined, classification_mode: @@ -218,23 +203,14 @@ export const hydrateComplexityRouterConfig = ( 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, + deployment_affinity: asBoolean(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, + enable_context_window_escalation: asBoolean(parsedConfig.enable_context_window_escalation), + context_window_escalation_buffer: asNumber(parsedConfig.context_window_escalation_buffer), stall_escalation_enabled: parsedConfig.stall_escalation_enabled === true || undefined, stall_escalation_window: typeof parsedConfig.stall_escalation_window === "number" ? parsedConfig.stall_escalation_window : undefined, diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RoutingDecisionCard.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RoutingDecisionCard.tsx index 7bbf18e16ed..30748b60180 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RoutingDecisionCard.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/RoutingDecisionCard.tsx @@ -155,6 +155,27 @@ function Row({ label, children }: { label: string; children: React.ReactNode }) ); } +function ClassifierRows({ decision }: { decision: RoutingDecision }) { + return ( + <> + {decision.classifier_model && {decision.classifier_model}} + {decision.classifier_confidence != null && ( + {(decision.classifier_confidence * 100).toFixed(1)}% + )} + {decision.classifier_probabilities && ( + + {Object.entries(decision.classifier_probabilities).map(([name, probability]) => ( +
+ {name}: {(probability * 100).toFixed(1)}% +
+ ))} +
+ )} + {decision.classifier_cost != null && ${decision.classifier_cost.toFixed(8)}} + + ); +} + export function RoutingDecisionCard({ decision, className, @@ -213,20 +234,7 @@ export function RoutingDecisionCard({ {requestType && {requestType}} {describeCause(decision)} - {decision.classifier_model && {decision.classifier_model}} - {decision.classifier_confidence != null && ( - {(decision.classifier_confidence * 100).toFixed(1)}% - )} - {decision.classifier_probabilities && ( - - {Object.entries(decision.classifier_probabilities).map(([name, probability]) => ( -
- {name}: {(probability * 100).toFixed(1)}% -
- ))} -
- )} - {decision.classifier_cost != null && ${decision.classifier_cost.toFixed(8)}} + {score !== undefined && (