Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,8 @@ export function AutoRoutersPanel({
<DialogHeader>
<DialogTitle>Add Auto Router</DialogTitle>
<DialogDescription>
Routes each request to a model by classifying its complexity. Called like any other model, so clients keep
using a single model name.
Choose a classifier to route each request to a model. Called like any other model, so clients keep using a
single model name.
</DialogDescription>
</DialogHeader>
<AddAutoRouterTab
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ const dedupe = (models: string[]): string[] => Array.from(new Set(models));

const COMPLEXITY_TYPE_LABELS: Record<string, string> = {
llm: "LLM Classifier",
capability: "Capability",
llm_v2: "Fuse v2",
heuristic_first: "Heuristic first",
hybrid: "Hybrid",
custom: "Custom classifier",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useState } from "react";
import { describe, expect, it, vi } from "vitest";
import { fireEvent, renderWithProviders, screen } from "../../../tests/test-utils";
import AutoRouterClassifierTabs from "./AutoRouterClassifierTabs";
import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";

const initial: ComplexityRouterConfigValue = {
classifier_type: "llm",
tiers: { SIMPLE: ["efficient"], MEDIUM: [], COMPLEX: [], REASONING: ["capable"] },
};

function Form({ initialValue = initial }: { initialValue?: ComplexityRouterConfigValue }) {
const [value, setValue] = useState(initialValue);
return (
<AutoRouterClassifierTabs value={value} onChange={setValue}>
<output aria-label="Classifier type">{value.classifier_type}</output>
</AutoRouterClassifierTabs>
);
}

describe("AutoRouterClassifierTabs", () => {
it.each(["heuristic", "heuristic_v2", "llm", "heuristic_first", "hybrid"] as const)(
"groups %s under Complexity without resetting its configuration",
(classifier_type) => {
const onChange = vi.fn();
renderWithProviders(
<AutoRouterClassifierTabs value={{ ...initial, classifier_type }} onChange={onChange}>
Existing classifier settings
</AutoRouterClassifierTabs>,
);
expect(screen.getByRole("tab", { name: "Complexity" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("tabpanel", { name: "Complexity" })).toHaveTextContent("Existing classifier settings");
fireEvent.click(screen.getByRole("tab", { name: "Complexity" }));
expect(onChange).not.toHaveBeenCalled();
},
);

it.each([
["capability", "Capability"],
["llm_v2", "Fuse v2"],
] as const)("opens saved %s settings and switches back to local Complexity", (classifier_type, label) => {
renderWithProviders(<Form initialValue={{ ...initial, classifier_type }} />);
expect(screen.getByRole("tab", { name: label })).toHaveAttribute("aria-selected", "true");
fireEvent.click(screen.getByRole("tab", { name: "Complexity" }));
expect(screen.getByRole("tab", { name: "Complexity" })).toHaveAttribute("aria-selected", "true");
expect(screen.getByRole("status", { name: "Classifier type" })).toHaveTextContent("heuristic");
});

it("keeps custom tiers editable under Complexity and explains why forecast tabs are disabled", () => {
const onChange = vi.fn();
renderWithProviders(
<AutoRouterClassifierTabs
value={{
...initial,
custom_tier_set: {
tiers: [{ id: "review", name: "REVIEW", definition: "Code reviews", models: ["capable"] }],
fallback_tier_id: "review",
},
}}
onChange={onChange}
>
Custom tiers
</AutoRouterClassifierTabs>,
);
expect(screen.getByRole("tabpanel", { name: "Complexity" })).toHaveTextContent("Custom tiers");
for (const name of ["Capability", "Fuse v2"]) {
const tab = screen.getByRole("tab", { name });
expect(tab).toHaveAttribute("aria-disabled", "true");
expect(tab).toHaveAccessibleDescription("Restore standard tiers to use Capability or Fuse v2.");
fireEvent.click(tab);
}
expect(onChange).not.toHaveBeenCalled();
expect(screen.getByText("Restore standard tiers to use Capability or Fuse v2.")).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React, { useId } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { effectiveClassifierType, type ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
import { transitionClassifierType } from "./classifier_type_transition";
import { isForecastClassifier } from "./forecast_classifier_config";

interface AutoRouterClassifierTabsProps {
value: ComplexityRouterConfigValue;
onChange: (value: ComplexityRouterConfigValue) => void;
children: React.ReactNode;
}

const AutoRouterClassifierTabs: React.FC<AutoRouterClassifierTabsProps> = ({ value, onChange, children }) => {
const restrictionId = useId();
const classifierType = effectiveClassifierType(value);
const selected = isForecastClassifier(classifierType) ? classifierType : "complexity";
const hasCustomTiers = Boolean(value.custom_tier_set);

const handleChange = (tab: unknown) => {
if (tab === selected) return;
if (tab === "complexity") {
onChange(transitionClassifierType(value, isForecastClassifier(classifierType) ? "heuristic" : classifierType));
} else if (!hasCustomTiers && (tab === "capability" || tab === "llm_v2")) {
onChange(transitionClassifierType(value, tab));
}
};

return (
<Tabs value={selected} onValueChange={handleChange}>
<p className="text-sm font-medium">Classifier type</p>
<TabsList aria-label="Classifier type" className="w-full">
<TabsTrigger value="complexity">Complexity</TabsTrigger>
<TabsTrigger
value="capability"
disabled={hasCustomTiers}
aria-describedby={hasCustomTiers ? restrictionId : undefined}
>
Capability
</TabsTrigger>
<TabsTrigger
value="llm_v2"
disabled={hasCustomTiers}
aria-describedby={hasCustomTiers ? restrictionId : undefined}
>
Fuse v2
</TabsTrigger>
</TabsList>
{hasCustomTiers && (
<p id={restrictionId} className="text-sm text-muted-foreground">
Restore standard tiers to use Capability or Fuse v2.
</p>
)}
<TabsContent value={selected}>{children}</TabsContent>
</Tabs>
);
};

export default AutoRouterClassifierTabs;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { transitionClassifierType } from "./classifier_type_transition";
import { Info } from "lucide-react";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { MultiSelect } from "@/components/shared/MultiSelect";
Expand All @@ -17,7 +18,6 @@
import ClassifierCircuitBreakerConfig from "./ClassifierCircuitBreakerConfig";
import ClassifierVisionConfig from "./ClassifierVisionConfig";
import type { ReasoningEffort } from "./complexity_router_tiers";
import { nonReasoningTierFields } from "./nonReasoningTierFields";
import { useComplexityScorerDefaults } from "@/app/(dashboard)/hooks/autoRouter/useComplexityScorerDefaults";
import {
ClassificationFrequency,
Expand All @@ -33,12 +33,10 @@
DEFAULT_CLASSIFIER_FALLBACK,
DEFAULT_CLASSIFIER_TIMEOUT_MS,
DEFAULT_CLASSIFICATION_RUBRIC,
NEW_CLASSIFIER_CLASSIFICATION_RUBRIC,
ClassificationRubric,
effectiveTierLabel,
heuristicScoringRole,
usesLlmClassifier,
DEFAULT_HEURISTIC_FIRST_MAX_TIER,
DEFAULT_HYBRID_BOUNDARY_MARGIN,
HEURISTIC_FIRST_MAX_TIER_KEYS,
effectiveClassifierType,
Expand Down Expand Up @@ -247,7 +245,7 @@
onCustomTechnicalKeywordsChange,
showValidationErrors = false,
defaultModel,
}) => {

Check warning on line 248 in ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Arrow function has a complexity of 53. Maximum allowed is 20
const [draft, setDraft] = React.useState<{ id: string; raw: string } | null>(null);
const hasDefaultModel = Boolean(defaultModel);
const classifierType = effectiveClassifierType(value);
Expand All @@ -263,35 +261,7 @@
const explicitlySupportedClassifierEfforts = effortOptionsByModel[classifierModel];

const handleClassifierTypeChange = (classifierType: ClassifierType) => {
const nextValue: ComplexityRouterConfigValue = {
...value,
classifier_type: classifierType,
classifier_llm_config: usesLlmClassifier(classifierType)
? value.classifier_llm_config ?? {
model: "",
timeout_ms: DEFAULT_CLASSIFIER_TIMEOUT_MS,
classification_rubric: NEW_CLASSIFIER_CLASSIFICATION_RUBRIC,
}
: undefined,
classifier_context_window_size: usesLlmClassifier(classifierType)
? value.classifier_context_window_size ?? DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE
: undefined,
classifier_context_budget_chars: usesLlmClassifier(classifierType)
? value.classifier_context_budget_chars ?? DEFAULT_CLASSIFIER_CONTEXT_BUDGET_CHARS
: undefined,
classifier_context_include_assistant_turns: usesLlmClassifier(classifierType)
? value.classifier_context_include_assistant_turns
: undefined,
classifier_fallback: usesLlmClassifier(classifierType) ? value.classifier_fallback : undefined,
heuristic_first_max_tier:
classifierType === "heuristic_first"
? value.heuristic_first_max_tier ?? DEFAULT_HEURISTIC_FIRST_MAX_TIER
: undefined,
hybrid_boundary_margin:
classifierType === "hybrid" ? value.hybrid_boundary_margin ?? DEFAULT_HYBRID_BOUNDARY_MARGIN : undefined,
...nonReasoningTierFields(classifierType, value),
};
onChange(nextValue);
onChange(transitionClassifierType(value, classifierType));
};

const handleHeuristicFirstMaxTierChange = (tier: string) => {
Expand Down Expand Up @@ -433,27 +403,6 @@
});
};

if (classifierType === "capability") {
return (
<p className="text-sm text-muted-foreground">
This router uses capability forecasting. Configure its classifier, threshold, and calibration through YAML or
the API. Saving preserves those settings
</p>
);
}

if (classifierType === "llm_v2") {
return (
<div className="rounded-md border p-4 text-sm">
<strong>LLM V2 classifier (experimental)</strong>
<p className="mt-2 text-muted-foreground">
Combines task demands and model capability in one forecast. Its solver profiles and quality allowance are
configured through the API. Saving this router preserves those settings
</p>
</div>
);
}

return (
<>
<ClassifierTypeRadios value={value} classifierType={classifierType} onTypeChange={handleClassifierTypeChange} />
Expand Down
Loading
Loading