mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
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>
This commit is contained in:
parent
bf4fccc937
commit
9644032cb8
14 changed files with 839 additions and 597 deletions
|
|
@ -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<Pick<ClassificationMethodCon
|
|||
);
|
||||
};
|
||||
|
||||
const ClassifierTypeRadios: React.FC<{
|
||||
value: ComplexityRouterConfigValue;
|
||||
classifierType: ClassifierType;
|
||||
onTypeChange: (classifierType: ClassifierType) => void;
|
||||
}> = ({ value, classifierType, onTypeChange }) => {
|
||||
const scorerLocked = Boolean(value.custom_tier_set);
|
||||
const scorerLockedReason = restrictedBy(value, "heuristicClassifier")?.reason;
|
||||
return (
|
||||
<RadioGroup
|
||||
value={classifierType}
|
||||
onValueChange={(classifierType: unknown) => onTypeChange(classifierType as ClassifierType)}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="heuristic" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Heuristic</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
(default), rule-based scoring with no API calls and <1ms latency
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="heuristic_v2" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Heuristic v2</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
uses bundled calibrated four-tier probabilities with no API call
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
<Label className="items-start font-normal leading-normal">
|
||||
<RadioGroupItem value="llm" className="mt-0.5" />
|
||||
<span>
|
||||
<strong className="font-semibold">LLM Classifier</strong>{" "}
|
||||
<span className="text-muted-foreground">calls a model to decide the tier (e.g. a small/fast model)</span>
|
||||
</span>
|
||||
</Label>
|
||||
<Label className="items-start font-normal leading-normal">
|
||||
<RadioGroupItem value="jev" className="mt-0.5" />
|
||||
<span>
|
||||
<strong className="font-semibold">JEV Classifier</strong>{" "}
|
||||
<span className="text-muted-foreground">uses TypeSafe System One Choice to decide the tier</span>
|
||||
</span>
|
||||
</Label>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="heuristic_first" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Heuristic first</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
scores locally, and only pays for the classifier when the score does not confidently land a cheap tier
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="hybrid" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Hybrid</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
keeps the local score at any tier, and only pays for the classifier when that score lands near a tier
|
||||
boundary
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const ClassificationMethodConfig: React.FC<ClassificationMethodConfigProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
|
|
@ -472,37 +392,7 @@ const ClassificationMethodConfig: React.FC<ClassificationMethodConfigProps> = ({
|
|||
<ClassifierTypeRadios value={value} classifierType={classifierType} onTypeChange={handleClassifierTypeChange} />
|
||||
|
||||
{classifierType === "custom" && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This router uses a custom classifier plugin set in config.yaml. Pick a classifier below to replace it.
|
||||
</p>
|
||||
<Label htmlFor={CLASSIFIER_PLUGIN_TIMEOUT_ID} className="block font-semibold">
|
||||
Classifier plugin timeout (ms)
|
||||
</Label>
|
||||
<Input
|
||||
id={CLASSIFIER_PLUGIN_TIMEOUT_ID}
|
||||
inputMode="numeric"
|
||||
placeholder="3000"
|
||||
value={value.classifier_plugin_timeout_ms ?? ""}
|
||||
onChange={(event) =>
|
||||
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),
|
||||
)}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Time budget for the plugin call. On expiry the fallback path decides the tier.
|
||||
</p>
|
||||
{showValidationErrors && getClassifierPluginTimeoutError(classifierType, value.classifier_plugin_timeout_ms) && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{getClassifierPluginTimeoutError(classifierType, value.classifier_plugin_timeout_ms)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<ClassifierPluginTimeoutField value={value} onChange={onChange} showValidationErrors={showValidationErrors} />
|
||||
)}
|
||||
|
||||
{classifierType === "heuristic_v2" && (
|
||||
|
|
|
|||
|
|
@ -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<ClassifierPluginTimeoutFieldProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
showValidationErrors = false,
|
||||
}) => {
|
||||
const error = getClassifierPluginTimeoutError("custom", value.classifier_plugin_timeout_ms);
|
||||
return (
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This router uses a custom classifier plugin set in config.yaml. Pick a classifier below to replace it.
|
||||
</p>
|
||||
<Label htmlFor={CLASSIFIER_PLUGIN_TIMEOUT_ID} className="block font-semibold">
|
||||
Classifier plugin timeout (ms)
|
||||
</Label>
|
||||
<Input
|
||||
id={CLASSIFIER_PLUGIN_TIMEOUT_ID}
|
||||
inputMode="numeric"
|
||||
placeholder="3000"
|
||||
value={value.classifier_plugin_timeout_ms ?? ""}
|
||||
onChange={(event) =>
|
||||
onChange({
|
||||
...value,
|
||||
classifier_plugin_timeout_ms: event.target.value.trim() === "" ? undefined : Number(event.target.value),
|
||||
})
|
||||
}
|
||||
aria-invalid={Boolean(showValidationErrors && error)}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Time budget for the plugin call. On expiry the fallback path decides the tier.
|
||||
</p>
|
||||
{showValidationErrors && error && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassifierPluginTimeoutField;
|
||||
|
|
@ -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<ClassifierTypeRadiosProps> = ({ value, classifierType, onTypeChange }) => {
|
||||
const scorerLocked = Boolean(value.custom_tier_set);
|
||||
const scorerLockedReason = restrictedBy(value, "heuristicClassifier")?.reason;
|
||||
return (
|
||||
<RadioGroup
|
||||
value={classifierType}
|
||||
onValueChange={(nextType: unknown) => onTypeChange(nextType as ClassifierType)}
|
||||
className="w-full"
|
||||
>
|
||||
<div className="flex w-full flex-col items-start gap-2">
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="heuristic" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Heuristic</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
(default), rule-based scoring with no API calls and <1ms latency
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="heuristic_v2" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Heuristic v2</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
uses bundled calibrated four-tier probabilities with no API call
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
<Label className="items-start font-normal leading-normal">
|
||||
<RadioGroupItem value="llm" className="mt-0.5" />
|
||||
<span>
|
||||
<strong className="font-semibold">LLM Classifier</strong>{" "}
|
||||
<span className="text-muted-foreground">calls a model to decide the tier (e.g. a small/fast model)</span>
|
||||
</span>
|
||||
</Label>
|
||||
<Label className="items-start font-normal leading-normal">
|
||||
<RadioGroupItem value="jev" className="mt-0.5" />
|
||||
<span>
|
||||
<strong className="font-semibold">JEV Classifier</strong>{" "}
|
||||
<span className="text-muted-foreground">uses TypeSafe System One Choice to decide the tier</span>
|
||||
</span>
|
||||
</Label>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="heuristic_first" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Heuristic first</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
scores locally, and only pays for the classifier when the score does not confidently land a cheap tier
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
<SimpleTooltip content={scorerLockedReason}>
|
||||
<Label className="items-start font-normal leading-normal has-data-disabled:cursor-not-allowed has-data-disabled:opacity-50">
|
||||
<RadioGroupItem value="hybrid" className="mt-0.5" disabled={scorerLocked} />
|
||||
<span>
|
||||
<strong className="font-semibold">Hybrid</strong>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
keeps the local score at any tier, and only pays for the classifier when that score lands near a tier
|
||||
boundary
|
||||
</span>
|
||||
</span>
|
||||
</Label>
|
||||
</SimpleTooltip>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClassifierTypeRadios;
|
||||
|
|
@ -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<string, string[]>;
|
||||
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<ComplexityRouterAdvancedSectionsProps> = ({
|
||||
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: <strong className="text-foreground font-semibold">Advanced: Classification Method</strong>,
|
||||
children: (
|
||||
<ClassificationMethodConfig
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
modelOptions={modelOptions}
|
||||
effortOptionsByModel={classifierEffortOptionsByModel}
|
||||
customTechnicalKeywords={customTechnicalKeywords}
|
||||
onCustomTechnicalKeywordsChange={onCustomTechnicalKeywordsChange}
|
||||
showValidationErrors={showValidationErrors}
|
||||
defaultModel={defaultModel}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!forecast
|
||||
? [
|
||||
{
|
||||
key: "keyword-overrides",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Heuristic Keyword Overrides</strong>,
|
||||
children: <HeuristicKeywordOverrides value={value} onChange={onChange} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "adaptive",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Adaptive Routing</strong>,
|
||||
children: (
|
||||
<Restricted by={restrictedBy(value, "adaptive")}>
|
||||
<AdaptiveRoutingConfig value={value} onChange={onChange} />
|
||||
</Restricted>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "affinity",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Affinity</strong>,
|
||||
children: <AffinityControls value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "modality",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Modality Routing</strong>,
|
||||
children: <ModalityRoutingControls value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "plan-mode",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Plan-Mode Override</strong>,
|
||||
children: <PlanModeOverrideControls value={value} onChange={onChange} planModeTierOptions={planModeTierOptions} />,
|
||||
},
|
||||
{
|
||||
key: "housekeeping",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Housekeeping Routing</strong>,
|
||||
children: <HousekeepingRoutingControls value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "reminder-markers",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Reminder Markers</strong>,
|
||||
children: <ReminderMarkers value={value} onChange={onChange} showValidationErrors={showValidationErrors} />,
|
||||
},
|
||||
{
|
||||
key: "context-window",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Context Window Escalation</strong>,
|
||||
children: <ContextWindowEscalationConfig value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "stall-escalation",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Stalled Task Escalation</strong>,
|
||||
children: (
|
||||
<Restricted by={restrictedBy(value, "stallEscalation")}>
|
||||
<StallEscalationConfig value={value} onChange={onChange} />
|
||||
</Restricted>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "response",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Response Format</strong>,
|
||||
children: <ResponseFormatControls value={value} onChange={onChange} />,
|
||||
},
|
||||
...(onEscalationKeywordsChange
|
||||
? [
|
||||
{
|
||||
key: "escalation",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Escalation Keywords</strong>,
|
||||
children: (
|
||||
<Restricted by={restrictedBy(value, "escalation")}>
|
||||
<EscalationKeywords keywords={escalationKeywords} onChange={onEscalationKeywordsChange} />
|
||||
</Restricted>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onAutoRouterCompressionChange
|
||||
? [
|
||||
{
|
||||
key: "compression",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Compression</strong>,
|
||||
children: <CompressionControls value={autoRouterCompression} onChange={onAutoRouterCompressionChange} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onKeywordTierRulesChange || onSemanticMatchingEnabledChange
|
||||
? [
|
||||
{
|
||||
key: "keyword-semantic",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Keyword/Semantic Matching</strong>,
|
||||
children: (
|
||||
<>
|
||||
{onKeywordTierRulesChange && (
|
||||
<KeywordTierRules
|
||||
rules={keywordTierRules}
|
||||
onChange={onKeywordTierRulesChange}
|
||||
tierLabels={value.tier_labels}
|
||||
tierNames={
|
||||
customTierSet || forecast ? tierRows.map(activeTierName).filter(Boolean) : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{onKeywordTierRulesChange && onSemanticMatchingEnabledChange && <Separator className="my-4" />}
|
||||
{onSemanticMatchingEnabledChange && (
|
||||
<SemanticKeywordMatching
|
||||
enabled={semanticMatchingEnabled}
|
||||
onEnabledChange={onSemanticMatchingEnabledChange}
|
||||
embeddingModel={embeddingModel}
|
||||
onEmbeddingModelChange={onEmbeddingModelChange}
|
||||
matchThreshold={matchThreshold}
|
||||
onMatchThresholdChange={onMatchThresholdChange}
|
||||
modelInfo={modelInfo}
|
||||
showValidationErrors={showValidationErrors}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{sections
|
||||
.filter(({ key }) => !forecast || !["adaptive", "context-window", "escalation"].includes(key))
|
||||
.map(({ key, label, children }) => (
|
||||
<Collapsible key={key} className="border-b border-border last:border-b-0">
|
||||
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left">
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-data-panel-open:rotate-90" />
|
||||
{label}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="px-4 pb-4">{children}</CollapsibleContent>
|
||||
</Collapsible>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ComplexityRouterAdvancedSections;
|
||||
|
|
@ -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(<ComplexityRouterConfig {...baseProps} />);
|
||||
|
||||
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(<ComplexityRouterConfig {...baseProps} value={capabilityValue} />);
|
||||
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(
|
||||
<ComplexityRouterConfig {...baseProps} value={{ ...defaultValue, classifier_type: classifierType }} />,
|
||||
);
|
||||
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(
|
||||
<ComplexityRouterConfig
|
||||
{...baseProps}
|
||||
value={value}
|
||||
showValidationErrors={showValidationErrors}
|
||||
/>,
|
||||
);
|
||||
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(
|
||||
<ComplexityRouterConfig
|
||||
{...baseProps}
|
||||
value={{ ...defaultValue, route_housekeeping_to_cheapest_tier: false }}
|
||||
/>,
|
||||
);
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<ComplexityRouterConfigProps> = ({
|
|||
</>
|
||||
)}
|
||||
<div className="rounded-lg border border-border bg-muted">
|
||||
{[
|
||||
...(!forecast
|
||||
? [
|
||||
{
|
||||
key: "classifier",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Classification Method</strong>,
|
||||
children: (
|
||||
<ClassificationMethodConfig
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
modelOptions={modelOptions}
|
||||
effortOptionsByModel={classifierEffortOptionsByModel}
|
||||
customTechnicalKeywords={customTechnicalKeywords}
|
||||
onCustomTechnicalKeywordsChange={onCustomTechnicalKeywordsChange}
|
||||
showValidationErrors={showValidationErrors}
|
||||
defaultModel={defaultModel}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(!forecast
|
||||
? [
|
||||
{
|
||||
key: "keyword-overrides",
|
||||
label: (
|
||||
<strong className="text-foreground font-semibold">Advanced: Heuristic Keyword Overrides</strong>
|
||||
),
|
||||
children: <HeuristicKeywordOverrides value={value} onChange={onChange} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "adaptive",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Adaptive Routing</strong>,
|
||||
children: (
|
||||
<Restricted by={restrictedBy(value, "adaptive")}>
|
||||
<AdaptiveRoutingConfig value={value} onChange={onChange} />
|
||||
</Restricted>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "affinity",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Affinity</strong>,
|
||||
children: <AffinityControls value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "modality",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Modality Routing</strong>,
|
||||
children: <ModalityRoutingControls value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "plan-mode",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Plan-Mode Override</strong>,
|
||||
children: (
|
||||
<PlanModeOverrideControls value={value} onChange={onChange} planModeTierOptions={planModeTierOptions} />
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "housekeeping",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Housekeeping Routing</strong>,
|
||||
children: <HousekeepingRoutingControls value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "reminder-markers",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Reminder Markers</strong>,
|
||||
children: <ReminderMarkers value={value} onChange={onChange} showValidationErrors={showValidationErrors} />,
|
||||
},
|
||||
{
|
||||
key: "context-window",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Context Window Escalation</strong>,
|
||||
children: <ContextWindowEscalationConfig value={value} onChange={onChange} />,
|
||||
},
|
||||
{
|
||||
key: "stall-escalation",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Stalled Task Escalation</strong>,
|
||||
children: (
|
||||
<Restricted by={restrictedBy(value, "stallEscalation")}>
|
||||
<StallEscalationConfig value={value} onChange={onChange} />
|
||||
</Restricted>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "response",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Response Format</strong>,
|
||||
children: <ResponseFormatControls value={value} onChange={onChange} />,
|
||||
},
|
||||
...(onEscalationKeywordsChange
|
||||
? [
|
||||
{
|
||||
key: "escalation",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Escalation Keywords</strong>,
|
||||
children: (
|
||||
<Restricted by={restrictedBy(value, "escalation")}>
|
||||
<EscalationKeywords keywords={escalationKeywords} onChange={onEscalationKeywordsChange} />
|
||||
</Restricted>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onAutoRouterCompressionChange
|
||||
? [
|
||||
{
|
||||
key: "compression",
|
||||
label: <strong className="text-foreground font-semibold">Advanced: Compression</strong>,
|
||||
children: (
|
||||
<CompressionControls value={autoRouterCompression} onChange={onAutoRouterCompressionChange} />
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(onKeywordTierRulesChange || onSemanticMatchingEnabledChange
|
||||
? [
|
||||
{
|
||||
key: "keyword-semantic",
|
||||
label: (
|
||||
<strong className="text-foreground font-semibold">Advanced: Keyword/Semantic Matching</strong>
|
||||
),
|
||||
children: (
|
||||
<>
|
||||
{onKeywordTierRulesChange && (
|
||||
<KeywordTierRules
|
||||
rules={keywordTierRules}
|
||||
onChange={onKeywordTierRulesChange}
|
||||
tierLabels={value.tier_labels}
|
||||
tierNames={
|
||||
customTierSet || isForecastClassifier(value.classifier_type)
|
||||
? tierRows.map(activeTierName).filter(Boolean)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{onKeywordTierRulesChange && onSemanticMatchingEnabledChange && <Separator className="my-4" />}
|
||||
{onSemanticMatchingEnabledChange && (
|
||||
<SemanticKeywordMatching
|
||||
enabled={semanticMatchingEnabled}
|
||||
onEnabledChange={onSemanticMatchingEnabledChange}
|
||||
embeddingModel={embeddingModel}
|
||||
onEmbeddingModelChange={onEmbeddingModelChange}
|
||||
matchThreshold={matchThreshold}
|
||||
onMatchThresholdChange={onMatchThresholdChange}
|
||||
modelInfo={modelInfo}
|
||||
showValidationErrors={showValidationErrors}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
.filter(({ key }) => !forecast || !["adaptive", "context-window", "escalation"].includes(key))
|
||||
.map(({ key, label, children }) => (
|
||||
<Collapsible key={key} className="border-b border-border last:border-b-0">
|
||||
<CollapsibleTrigger className="group flex w-full items-center gap-2 px-4 py-3 text-left">
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-data-panel-open:rotate-90" />
|
||||
{label}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="px-4 pb-4">{children}</CollapsibleContent>
|
||||
</Collapsible>
|
||||
))}
|
||||
<ComplexityRouterAdvancedSections
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
forecast={forecast}
|
||||
modelOptions={modelOptions}
|
||||
classifierEffortOptionsByModel={classifierEffortOptionsByModel}
|
||||
customTechnicalKeywords={customTechnicalKeywords}
|
||||
onCustomTechnicalKeywordsChange={onCustomTechnicalKeywordsChange}
|
||||
showValidationErrors={showValidationErrors}
|
||||
defaultModel={defaultModel}
|
||||
planModeTierOptions={planModeTierOptions}
|
||||
keywordTierRules={keywordTierRules}
|
||||
onKeywordTierRulesChange={onKeywordTierRulesChange}
|
||||
semanticMatchingEnabled={semanticMatchingEnabled}
|
||||
onSemanticMatchingEnabledChange={onSemanticMatchingEnabledChange}
|
||||
embeddingModel={embeddingModel}
|
||||
onEmbeddingModelChange={onEmbeddingModelChange}
|
||||
matchThreshold={matchThreshold}
|
||||
onMatchThresholdChange={onMatchThresholdChange}
|
||||
escalationKeywords={escalationKeywords}
|
||||
onEscalationKeywordsChange={onEscalationKeywordsChange}
|
||||
autoRouterCompression={autoRouterCompression}
|
||||
onAutoRouterCompressionChange={onAutoRouterCompressionChange}
|
||||
modelInfo={modelInfo}
|
||||
tierRows={tierRows}
|
||||
customTierSet={customTierSet}
|
||||
/>
|
||||
</div>
|
||||
</RoutingOptions>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,14 +30,13 @@ const ReminderMarkers: React.FC<{
|
|||
</p>
|
||||
<div className="space-y-3">
|
||||
{markers.map((marker, index) => (
|
||||
<div className="flex items-end gap-2" key={`${index}-${marker.open}-${marker.close}`}>
|
||||
<div className="flex items-end gap-2" key={index}>
|
||||
<div className="flex-1">
|
||||
<label className="mb-1 block text-sm font-medium" htmlFor={`reminder-marker-${index}-open`}>
|
||||
Opening delimiter
|
||||
</label>
|
||||
<Input
|
||||
id={`reminder-marker-${index}-open`}
|
||||
aria-label={`Reminder marker ${index + 1} opening delimiter`}
|
||||
placeholder="<system-reminder>"
|
||||
value={marker.open}
|
||||
onChange={(event) => update(index, { open: event.target.value })}
|
||||
|
|
@ -49,7 +48,6 @@ const ReminderMarkers: React.FC<{
|
|||
</label>
|
||||
<Input
|
||||
id={`reminder-marker-${index}-close`}
|
||||
aria-label={`Reminder marker ${index + 1} closing delimiter`}
|
||||
placeholder="</system-reminder>"
|
||||
value={marker.close}
|
||||
onChange={(event) => update(index, { close: event.target.value })}
|
||||
|
|
|
|||
|
|
@ -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<AddAutoRouterTabProps> = ({
|
|||
);
|
||||
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -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: " <X> ", close: " <x> " }])).toContain("different");
|
||||
expect(getReminderMarkersError([{ open: "", close: "</x>" }])).toContain("needs both");
|
||||
|
|
|
|||
|
|
@ -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" &&
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
@ -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: "<a>", close: "</a>" }],
|
||||
max_tokens_from_tier_model: false,
|
||||
};
|
||||
|
||||
const renderAdvancedModal = (props: Partial<React.ComponentProps<typeof EditAutoRouterModal>> = {}) =>
|
||||
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("<a>");
|
||||
expect(screen.getByLabelText("Closing delimiter")).toHaveValue("</a>");
|
||||
|
||||
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: "<a>", close: "</a>" }],
|
||||
});
|
||||
});
|
||||
|
||||
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: "<a>", close: "</a>" }],
|
||||
max_tokens_from_tier_model: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EditAutoRouterModal classifier context window", () => {
|
||||
beforeEach(() => {
|
||||
modelPatchUpdateCall.mockClear();
|
||||
|
|
|
|||
|
|
@ -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<string, unknown> => {
|
|||
: {};
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue