- Each configured tier routes to a saved model group. Test Connection sends a minimal request through the proxy to
- each one, exactly as the auto router would.
+ Test Connection sends a minimal request to every configured tier, classifier, default, and embedding model. The
+ classifier probe includes its reasoning effort override.
{targets.map((target, index) => {
const result = results[index] ?? { status: "pending" };
return (
diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts
index 7c29ea53060..6eb1825710d 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.test.ts
@@ -126,4 +126,32 @@ describe("buildAutoRouterTestTargets", () => {
});
expect(targets).toEqual([{ labels: ["SIMPLE"], modelGroup: "gpt-4o-mini", mode: "chat" }]);
});
+
+ it("adds a distinct classifier probe with its reasoning effort", () => {
+ const targets = buildAutoRouterTestTargets({
+ tiers: tierEntries(["gpt-5-mini"]),
+ semanticMatchingEnabled: false,
+ embeddingModel: undefined,
+ classifier: { model: "gpt-5-mini", reasoningEffort: "low" },
+ });
+ expect(targets).toEqual([
+ { labels: ["SIMPLE"], modelGroup: "gpt-5-mini", mode: "chat" },
+ {
+ labels: ["Classifier"],
+ modelGroup: "gpt-5-mini",
+ mode: "chat",
+ requestParams: { reasoning_effort: "low" },
+ },
+ ]);
+ });
+
+ it("omits an empty classifier and omits params when the classifier uses provider defaults", () => {
+ const base = { tiers: tierEntries([]), semanticMatchingEnabled: false, embeddingModel: undefined };
+ const emptyClassifier = { ...base, classifier: { model: " " } };
+ const providerDefaultClassifier = { ...base, classifier: { model: "gpt-5-mini" } };
+ expect(buildAutoRouterTestTargets(emptyClassifier)).toEqual([]);
+ expect(buildAutoRouterTestTargets(providerDefaultClassifier)).toEqual([
+ { labels: ["Classifier"], modelGroup: "gpt-5-mini", mode: "chat" },
+ ]);
+ });
});
diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts
index 70b92dbf8cc..295a3b7f400 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_test_targets.ts
@@ -4,6 +4,7 @@ export interface AutoRouterTestTarget {
labels: string[];
modelGroup: string;
mode: AutoRouterTestMode;
+ requestParams?: Record;
}
export interface BuildAutoRouterTestTargetsParams {
@@ -14,6 +15,7 @@ export interface BuildAutoRouterTestTargetsParams {
/** The resolved default model - see resolveComplexityDefaultModel. A live fallback destination,
* so it is probed even when no tier lists it. */
defaultModel?: string;
+ classifier?: { model: string; reasoningEffort?: string };
}
export const buildAutoRouterTestTargets = ({
@@ -21,6 +23,7 @@ export const buildAutoRouterTestTargets = ({
semanticMatchingEnabled,
embeddingModel,
defaultModel,
+ classifier,
}: BuildAutoRouterTestTargetsParams): AutoRouterTestTarget[] => {
const tieredByModel = tiers.reduce>((acc, [tier, models]) => {
return models.reduce((tierAcc, rawModel) => {
@@ -50,5 +53,17 @@ export const buildAutoRouterTestTargets = ({
? [{ labels: ["Embedding"], modelGroup: embeddingModel.trim(), mode: "embedding" as const }]
: [];
- return [...tierTargets, ...embeddingTarget];
+ const classifierModel = classifier?.model.trim();
+ const classifierTarget: AutoRouterTestTarget[] = classifierModel
+ ? [
+ {
+ labels: ["Classifier"],
+ modelGroup: classifierModel,
+ mode: "chat",
+ ...(classifier?.reasoningEffort && { requestParams: { reasoning_effort: classifier.reasoningEffort } }),
+ },
+ ]
+ : [];
+
+ return [...tierTargets, ...embeddingTarget, ...classifierTarget];
};
diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
index 3ef55b9295d..53ab859baa0 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.test.ts
@@ -4,6 +4,7 @@ import {
normalizeClassifierLlmConfig,
getKeywordTierRulesError,
getClassifierModelError,
+ getClassifierReasoningEffortError,
getMissingTiersError,
hydrateCustomTierSet,
getSemanticConfigError,
@@ -506,6 +507,19 @@ describe("classifier prompt and fallback", () => {
expect(buildComplexityRouterConfig(llmParams)).not.toHaveProperty("classifier_fallback");
});
+ it("keeps an explicit classifier reasoning effort", () => {
+ const config = buildComplexityRouterConfig({
+ ...llmParams,
+ classifierLlmConfig: { model: "haiku-classifier", timeout_ms: 400, reasoning_effort: "low" },
+ });
+ expect(config.classifier_llm_config?.reasoning_effort).toBe("low");
+ });
+
+ it("omits classifier reasoning effort when the provider default is selected", () => {
+ const config = buildComplexityRouterConfig(llmParams);
+ expect(config.classifier_llm_config).not.toHaveProperty("reasoning_effort");
+ });
+
it("sends the chat preset the operator picked", () => {
const config = buildComplexityRouterConfig({
...llmParams,
@@ -540,12 +554,16 @@ describe("classifier prompt and fallback", () => {
});
it("normalizeClassifierLlmConfig leaves a real prompt untouched and strips an empty one", () => {
- expect(normalizeClassifierLlmConfig({ model: "m", timeout_ms: 1, system_prompt: "x" })).toEqual({
+ const customPromptConfig = { model: "m", timeout_ms: 1, reasoning_effort: "none" as const, system_prompt: "x" };
+ const emptyPromptConfig = { model: "m", timeout_ms: 1, system_prompt: "" };
+ const expectedCustomPromptConfig = {
model: "m",
timeout_ms: 1,
+ reasoning_effort: "none",
system_prompt: "x",
- });
- expect(normalizeClassifierLlmConfig({ model: "m", timeout_ms: 1, system_prompt: "" })).toEqual({
+ };
+ expect(normalizeClassifierLlmConfig(customPromptConfig)).toEqual(expectedCustomPromptConfig);
+ expect(normalizeClassifierLlmConfig(emptyPromptConfig)).toEqual({
model: "m",
timeout_ms: 1,
});
@@ -767,6 +785,26 @@ describe("getClassifierModelError", () => {
});
});
+describe("getClassifierReasoningEffortError", () => {
+ const classifier = {
+ classifier_type: "llm" as const,
+ classifier_llm_config: { model: "classifier", timeout_ms: 3000, reasoning_effort: "low" },
+ };
+
+ it.each([
+ [["low", "medium"], null],
+ [["medium", "high"], "low reasoning effort is not supported"],
+ [null, null],
+ [undefined, null],
+ ])("validates capability levels %o", (supportedReasoningEfforts, expectedError) => {
+ const error = getClassifierReasoningEffortError(classifier, [
+ { model_group: "classifier", supported_reasoning_efforts: supportedReasoningEfforts },
+ ]);
+ if (expectedError) expect(error).toContain(expectedError);
+ else expect(error).toBeNull();
+ });
+});
+
describe("getKeywordTierRulesError orphaned tiers", () => {
const rows = activeTierRows({ tiers });
@@ -944,11 +982,16 @@ describe("buildComplexityRouterConfig with an edited tier set", () => {
classifierLlmConfig: {
model: "gpt-4o-mini",
timeout_ms: 3000,
+ reasoning_effort: "low",
system_prompt: "replace the whole rubric",
classification_rubric: "agentic",
},
});
- expect(payload.classifier_llm_config).toEqual({ model: "gpt-4o-mini", timeout_ms: 3000 });
+ expect(payload.classifier_llm_config).toEqual({
+ model: "gpt-4o-mini",
+ timeout_ms: 3000,
+ reasoning_effort: "low",
+ });
});
it.each(
diff --git a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
index 91e4fdb23b5..b8af55e8c8a 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_complexity_router_config.ts
@@ -1,4 +1,5 @@
import { KeywordTierRule } from "./KeywordTierRules";
+import type { ModelGroup } from "../llm_calls/fetch_models";
import {
type CustomTierSet,
type TierRow,
@@ -55,12 +56,18 @@ import {
export const normalizeClassifierLlmConfig = ({
model,
timeout_ms,
+ reasoning_effort,
classification_rubric,
system_prompt,
}: ClassifierLLMConfig): ClassifierLLMConfig =>
system_prompt?.trim()
- ? { model, timeout_ms, system_prompt }
- : { model, timeout_ms, ...(classification_rubric && { classification_rubric }) };
+ ? { model, timeout_ms, ...(reasoning_effort && { reasoning_effort }), system_prompt }
+ : {
+ model,
+ timeout_ms,
+ ...(reasoning_effort && { reasoning_effort }),
+ ...(classification_rubric && { classification_rubric }),
+ };
interface ScorerKnobInputs {
classifierType: ClassifierType;
@@ -268,6 +275,20 @@ export const getClassifierModelError = (
: "Please select a classifier model, or switch back to Heuristic";
};
+export const getClassifierReasoningEffortError = (
+ config: Pick,
+ modelInfo: readonly ModelGroup[],
+): string | null => {
+ if (!usesLlmClassifier(effectiveClassifierType(config))) return null;
+ const classifierConfig = config.classifier_llm_config;
+ if (!classifierConfig?.model || !classifierConfig.reasoning_effort) return null;
+ const supported = modelInfo.find(
+ (model) => model.model_group === classifierConfig.model,
+ )?.supported_reasoning_efforts;
+ if (!Array.isArray(supported) || supported.includes(classifierConfig.reasoning_effort)) return null;
+ return `${classifierConfig.reasoning_effort} reasoning effort is not supported by every deployment in ${classifierConfig.model}. Choose Default or a supported value.`;
+};
+
export const getSemanticConfigError = ({
semanticMatchingEnabled,
embeddingModel,
@@ -295,11 +316,15 @@ export const customTierWireFields = (
tier_definitions: tierDefinitionsFromRows(rows),
...(fallback && { fallback_tier: activeTierName(fallback) }),
classifier_type: "llm",
- // Rebuilt from the two fields an edited tier set allows. The backend rejects system_prompt and
+ // Rebuilt from the fields an edited tier set allows. The backend rejects system_prompt and
// classification_rubric beside tier_definitions, and both live inside this object rather than at
// the top level the omit list covers. The opening instructions ride classification_prompt below.
...(classifierLlmConfig && {
- classifier_llm_config: { model: classifierLlmConfig.model, timeout_ms: classifierLlmConfig.timeout_ms },
+ classifier_llm_config: {
+ model: classifierLlmConfig.model,
+ timeout_ms: classifierLlmConfig.timeout_ms,
+ ...(classifierLlmConfig.reasoning_effort && { reasoning_effort: classifierLlmConfig.reasoning_effort }),
+ },
}),
session_affinity: false,
...(classificationPrompt?.trim() && { classification_prompt: classificationPrompt.trim() }),
diff --git a/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts b/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts
index 8cdb730cfa6..eb58c94b789 100644
--- a/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts
+++ b/ui/litellm-dashboard/src/components/add_model/complexity_router_tiers.ts
@@ -1,4 +1,5 @@
import type { ComplexityTier } from "./KeywordTierRules";
+import type { ModelGroup } from "@/components/llm_calls/fetch_models";
import { TIER_ORDER } from "./tier_rows";
export type TierModelParams = Record;
@@ -18,6 +19,23 @@ export const REASONING_EFFORT_OPTIONS = ["none", "minimal", "low", "medium", "hi
*/
export type ReasoningEffort = (typeof REASONING_EFFORT_OPTIONS)[number] | (string & {});
+export const tierEffortOptionsForModels = (modelInfo: ModelGroup[]): Record =>
+ Object.fromEntries(
+ modelInfo.map((model) => [
+ model.model_group,
+ model.supported_reasoning_efforts ?? (model.supports_reasoning ? [...REASONING_EFFORT_OPTIONS] : []),
+ ]),
+ );
+
+/**
+ * Stricter than the tier variant on purpose: classifier overrides are new in this release, so an
+ * unknown capability list stays unknown instead of inventing provider levels.
+ */
+export const classifierEffortOptionsForModels = (
+ modelInfo: ModelGroup[],
+): Record =>
+ Object.fromEntries(modelInfo.map((model) => [model.model_group, model.supported_reasoning_efforts]));
+
const asRecord = (raw: unknown): Record | undefined =>
typeof raw === "object" && raw !== null && !Array.isArray(raw) ? (raw as Record) : undefined;
diff --git a/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.test.tsx b/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.test.tsx
index e0b2b897b36..524ec6a715e 100644
--- a/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.test.tsx
+++ b/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.test.tsx
@@ -58,6 +58,17 @@ describe("OrganizationDropdown", () => {
expect(onChange.mock.calls[0][0]).toBe("org-1");
});
+ it("emits null, never the empty string, when the selection is cleared", async () => {
+ const onChange = vi.fn();
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByRole("button", { name: "Clear" }));
+
+ expect(onChange).toHaveBeenCalledTimes(1);
+ expect(onChange).toHaveBeenCalledWith(null);
+ });
+
it("should filter options by organization id", async () => {
const user = userEvent.setup();
render();
diff --git a/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.tsx b/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.tsx
index 8da35ecd02e..663028b2d92 100644
--- a/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.tsx
+++ b/ui/litellm-dashboard/src/components/common_components/OrganizationDropdown.tsx
@@ -5,7 +5,7 @@ import { Organization } from "../networking";
interface OrganizationDropdownProps {
organizations?: Organization[] | null;
value?: string;
- onChange?: (value: string) => void;
+ onChange?: (value: string | null) => void;
disabled?: boolean;
loading?: boolean;
style?: React.CSSProperties;
@@ -32,7 +32,7 @@ const OrganizationDropdown: React.FC = ({
sublabel: org.organization_id,
}))}
value={value}
- onValueChange={(organizationId) => onChange?.(organizationId)}
+ onValueChange={(organizationId) => onChange?.(organizationId || null)}
placeholder={placeholder}
emptyText={loading ? "Loading organizations…" : "No organizations found"}
disabled={disabled}
diff --git a/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts b/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts
index 01198bcc118..f24f3033901 100644
--- a/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts
+++ b/ui/litellm-dashboard/src/components/edit_auto_router/build_updated_complexity_router_config.test.ts
@@ -16,7 +16,7 @@ const storedCustomConfig = (overrides: Record