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:
yuneng 2026-09-21 19:37:45 +00:00
parent bf4fccc937
commit 9644032cb8
14 changed files with 839 additions and 597 deletions

View file

@ -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 &lt;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" && (

View file

@ -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;

View file

@ -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 &lt;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;

View file

@ -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;

View file

@ -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();

View file

@ -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>

View file

@ -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 })}

View file

@ -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) => {

View file

@ -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");

View file

@ -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" &&

View file

@ -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,
});

View file

@ -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();

View file

@ -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);

View file

@ -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,
};
};