From 8921bfb0fc37f3431c2df9afa23a7abb3f599209 Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Wed, 26 Aug 2026 23:23:33 -0700 Subject: [PATCH] feat(ui): let an edited tier set write its own classifier opening instructions The classifier prompt control was replaced by a sentence in custom mode, so an edited tier set could not steer its classifier at all. That control edits classifier_llm_config.system_prompt, a whole-prompt replacement the backend rejects beside tier_definitions, so refusing it there is right. classification_prompt is the field that fits: the router uses it as the opening of the prompt and appends the tier bullets and the injection guard after it, so an operator can state their own instructions and calibration examples without being able to restate or remove either. It has always been accepted beside tier_definitions and the dashboard just never exposed it. Custom mode now gets its own editor for that field, showing the tier bullets it will append underneath. Built-in routers keep the whole-prompt editor. The classifierPrompt restriction goes away with the control it described, and the Restricted helpers move out of ComplexityRouterConfig, which was at its max-lines ceiling. --- .../add_model/ClassificationMethodConfig.tsx | 19 +++- .../add_model/ClassificationPromptEditor.tsx | 101 ++++++++++++++++++ .../add_model/ComplexityRouterConfig.test.tsx | 20 ++++ .../add_model/ComplexityRouterConfig.tsx | 15 +-- .../components/add_model/TierRestrictions.tsx | 12 +++ .../add_model/add_auto_router_tab.tsx | 1 + .../build_complexity_router_config.test.ts | 20 ++++ .../build_complexity_router_config.ts | 16 ++- .../src/components/add_model/tier_rows.ts | 4 - ...d_updated_complexity_router_config.test.ts | 45 +++++++- .../edit_auto_router_modal.tsx | 7 ++ 11 files changed, 232 insertions(+), 28 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/add_model/ClassificationPromptEditor.tsx create mode 100644 ui/litellm-dashboard/src/components/add_model/TierRestrictions.tsx diff --git a/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx index 8a982556a51..4eaab3f3af9 100644 --- a/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx @@ -10,6 +10,8 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Switch } from "@/components/ui/switch"; import React from "react"; import ClassifierPromptEditor from "./ClassifierPromptEditor"; +import ClassificationPromptEditor from "./ClassificationPromptEditor"; +import { Restricted, restrictedBy } from "./TierRestrictions"; import HeuristicScoringConfig from "./HeuristicScoringConfig"; import { useComplexityScorerDefaults } from "@/app/(dashboard)/hooks/autoRouter/useComplexityScorerDefaults"; import { @@ -32,9 +34,6 @@ import { DEFAULT_HEURISTIC_FIRST_MAX_TIER, HEURISTIC_FIRST_MAX_TIER_KEYS, effectiveClassifierType, - heuristicScoringRole, - restrictedBy, - Restricted, } from "./ComplexityRouterConfig"; const DEFAULT_SCORING_EXPLANATION = @@ -202,6 +201,10 @@ const ClassificationMethodConfig: React.FC = ({ onChange({ ...value, heuristic_first_max_tier: tier }); }; + const handleClassificationPromptChange = (classificationPrompt: string | undefined) => { + onChange({ ...value, classification_prompt: classificationPrompt }); + }; + const handleClassifierModelChange = (model: string) => { onChange({ ...value, @@ -414,7 +417,13 @@ const ClassificationMethodConfig: React.FC = ({
Classifier Prompt - + {value.custom_tier_set ? ( + + ) : ( = ({ tierLabels={value.tier_labels} classificationRubric={classificationRubric} /> - + )}
If the classifier fails diff --git a/ui/litellm-dashboard/src/components/add_model/ClassificationPromptEditor.tsx b/ui/litellm-dashboard/src/components/add_model/ClassificationPromptEditor.tsx new file mode 100644 index 00000000000..1840eaea13c --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/ClassificationPromptEditor.tsx @@ -0,0 +1,101 @@ +import React, { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { TierRow, activeTierName } from "./tier_rows"; + +interface ClassificationPromptEditorProps { + classificationPrompt: string | undefined; + onChange: (classificationPrompt: string | undefined) => void; + tierRows: readonly TierRow[]; +} + +const PLACEHOLDER = `Classify the request into exactly one tier for a payments engineering team. + +Examples: +- "bump the copy on the checkout button" -> TRIAGE +- "why is our webhook signature check failing" -> SECURITY_REVIEW`; + +const appendedTierBullets = (tierRows: readonly TierRow[]): string => + tierRows.map((row) => `- ${activeTierName(row)}: ${row.definition}`).join("\n"); + +const ClassificationPromptEditor: React.FC = ({ + classificationPrompt, + onChange, + tierRows, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [draft, setDraft] = useState(""); + const isOverridden = Boolean(classificationPrompt?.trim()); + + const openEditor = () => { + setDraft(classificationPrompt ?? ""); + setIsOpen(true); + }; + + const handleSave = () => { + onChange(draft.trim() || undefined); + setIsOpen(false); + }; + + return ( +
+
+ + {isOverridden && ( + + )} +
+

+ {isOverridden + ? "This router opens its classifier prompt with your own instructions." + : "Replace the opening instructions, and write calibration examples of your own. Your tier definitions and the injection guard are always appended below them."} +

+ + + + + Classifier prompt + + +

+ Your text is the opening of the classifier prompt, so it is where calibration examples of your own belong. + The router appends your tier definitions and its injection guard underneath, and neither can be edited or + removed from here. Edit the definitions themselves with Edit tiers above. +

+ +