+ Uses TypeSafe System One Choice evaluation with your configured tiers +
++ Built-in JEV is available without a license and uses the shipped tier criteria + {!premiumUser && ( + <> + . Custom instructions require LiteLLM Enterprise. Get a trial key{" "} + + here + + > + )} +
+No complexity tiers are configured yet, so there is nothing to test.
+ {jevRequest && ( ++ {jevResult.status === "pending" && "Testing JEV classification"} + {jevResult.status === "success" && "JEV classification succeeded"} + {jevResult.status === "error" && jevResult.error} +
+
diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.test.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.test.ts
index 6678a3585c0..de0fb6fe6e1 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.test.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.test.ts
@@ -1,5 +1,11 @@
-import { buildAutoRouterRoutingTestRequest } from "./build_auto_router_routing_test_request";
+import { describe, expect, it } from "vitest";
+import {
+ buildAutoRouterRoutingTestRequest,
+ buildSavedJevConnectionTestRequest,
+ JEV_CONNECTION_TEST_PROMPT,
+} from "./build_auto_router_routing_test_request";
import { ComplexityRouterConfigPayload } from "./build_complexity_router_config";
+import { defaultJevClassifierConfig } from "./jev_classifier_config";
const CONFIG = {
tiers: { SIMPLE: ["cheap"], MEDIUM: ["mid"], COMPLEX: ["strong"], REASONING: ["o3"] },
@@ -15,6 +21,53 @@ const params = {
};
describe("buildAutoRouterRoutingTestRequest", () => {
+ it("references the saved deployment without copying masked credentials or client overrides", () => {
+ const request = buildSavedJevConnectionTestRequest(
+ {
+ classifier_type: "jev",
+ tiers: CONFIG.tiers,
+ jev_classifier_config: { api_key: "sk-masked****", api_base: "https://custom-jev.test" },
+ },
+ "saved-id",
+ );
+ const expectedRequest = {
+ prompt: JEV_CONNECTION_TEST_PROMPT,
+ complexity_router_config: {
+ classifier_type: "jev",
+ tiers: CONFIG.tiers,
+ jev_classifier_config: defaultJevClassifierConfig(),
+ },
+ saved_model_id: "saved-id",
+ };
+ expect(request).toEqual(expectedRequest);
+ expect(request?.complexity_router_config.jev_classifier_config).not.toHaveProperty("api_key");
+ expect(request?.complexity_router_config.jev_classifier_config).not.toHaveProperty("api_base");
+ });
+ it.each(["object", "json"])("probes saved JEV %s configuration with custom tiers and team context", (format) => {
+ const config = {
+ classifier_type: "jev",
+ jev_classifier_config: { model: "jev-test", timeout_ms: 900 },
+ tiers: { QUICK: ["fast"], DEEP: ["strong"] },
+ tier_definitions: { QUICK: "Simple questions", DEEP: "Complex questions" },
+ fallback_tier: "DEEP",
+ classifier_context_window_size: 4,
+ };
+ const expectedRequest = {
+ prompt: JEV_CONNECTION_TEST_PROMPT,
+ complexity_router_config: config,
+ saved_model_id: "saved-id",
+ team_id: "team-1",
+ };
+ expect(
+ buildSavedJevConnectionTestRequest(format === "json" ? JSON.stringify(config) : config, "saved-id", "team-1"),
+ ).toEqual(expectedRequest);
+ });
+ it.each([undefined, null, "not json", "[]", {}, { classifier_type: "llm", tiers: {} }, { classifier_type: "jev" }])(
+ "does not build a JEV probe for invalid or other classifier configurations: %j",
+ (config) => {
+ expect(buildSavedJevConnectionTestRequest(config, "saved-id")).toBeUndefined();
+ },
+ );
it("sends the prompt with the config being edited", () => {
const request = buildAutoRouterRoutingTestRequest(params);
diff --git a/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.ts b/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.ts
index 219dcbf6070..6a9d1ce7d92 100644
--- a/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.ts
+++ b/ui/litellm-dashboard/src/components/add_model/build_auto_router_routing_test_request.ts
@@ -1,5 +1,42 @@
import { AutoRouterRoutingTestRequest } from "../networking";
import { ComplexityRouterConfigPayload } from "./build_complexity_router_config";
+import { z } from "zod";
+import { jevClassifierConfigSchema } from "./jev_classifier_config";
+
+export const JEV_CONNECTION_TEST_PROMPT = "What is 2 plus 2?";
+
+export const buildSavedJevConnectionTestRequest = (
+ rawConfig: unknown,
+ savedModelId?: string,
+ teamId?: string,
+): AutoRouterRoutingTestRequest | undefined => {
+ if (!savedModelId) return undefined;
+ const parsed: unknown =
+ typeof rawConfig === "string"
+ ? (() => {
+ try {
+ return JSON.parse(rawConfig) as unknown;
+ } catch {
+ return undefined;
+ }
+ })()
+ : rawConfig;
+ const result = z
+ .object({
+ classifier_type: z.literal("jev"),
+ tiers: z.record(z.unknown()),
+ jev_classifier_config: jevClassifierConfigSchema.default({}),
+ })
+ .passthrough()
+ .safeParse(parsed);
+ if (!result.success) return undefined;
+ return {
+ prompt: JEV_CONNECTION_TEST_PROMPT,
+ complexity_router_config: result.data,
+ saved_model_id: savedModelId,
+ ...(teamId && { team_id: teamId }),
+ };
+};
export interface BuildAutoRouterRoutingTestRequestParams {
prompt: string;
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 38c3168023e..65c8573f876 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
@@ -1,3 +1,4 @@
+import { describe, expect, it } from "vitest";
import {
buildComplexityRouterConfig,
getPlanModeTierError,
@@ -23,6 +24,11 @@ const tiers = {
const baseParams: BuildComplexityRouterConfigParams = {
tiers,
+ defaultModel: undefined,
+ planModeMinTier: undefined,
+ classificationExamples: undefined,
+ heuristicFirstMaxTier: undefined,
+ classificationMode: undefined,
tierLabels: undefined,
classifierType: "heuristic",
classifierLlmConfig: undefined,
@@ -47,6 +53,99 @@ const baseParams: BuildComplexityRouterConfigParams = {
};
describe("buildComplexityRouterConfig", () => {
+ it("accepts built-in JEV defaults without an LLM classifier model", () => {
+ expect(getClassifierModelError({ classifier_type: "jev" })).toBeNull();
+ });
+
+ it.each([
+ { model: "" },
+ { model: " " },
+ { timeout_ms: 0 },
+ { timeout_ms: 1.5 },
+ { timeout_ms: Number.NaN },
+ { circuit_breaker_cooldown_seconds: -1 },
+ { circuit_breaker_cooldown_seconds: Number.POSITIVE_INFINITY },
+ ])("rejects invalid JEV settings before saving or testing: %j", (patch) => {
+ expect(
+ getClassifierModelError({
+ classifier_type: "jev",
+ jev_classifier_config: { model: "jev-latest", timeout_ms: 3000, ...patch },
+ }),
+ ).toBe("Enter a JEV model, a positive whole-number timeout and a positive cooldown");
+ });
+
+ it.each([false, true])("serializes JEV with shared context and no LLM config, custom tiers: %s", (custom) => {
+ const params: BuildComplexityRouterConfigParams = {
+ ...baseParams,
+ classifierType: "jev",
+ jevClassifierConfig: {
+ model: "jev-test",
+ timeout_ms: 4500,
+ instructions: " Choose the configured tier ",
+ circuit_breaker_enabled: false,
+ circuit_breaker_cooldown_seconds: 12.5,
+ },
+ classifierLlmConfig: { model: "stale", timeout_ms: 30 },
+ classificationPrompt: "stale prompt",
+ classificationExamples: "stale examples",
+ classifierContextWindowSize: 4,
+ classifierContextBudgetChars: 2000,
+ classifierContextPerTurnChars: 450,
+ classifierContextIncludeAssistantTurns: true,
+ classifierFallback: "default_model",
+ ...(custom && {
+ customTierSet: {
+ tiers: [
+ { id: "quick", name: "QUICK", definition: "Short answers", models: ["fast"] },
+ { id: "review", name: "REVIEW", definition: "Deep review", models: ["strong"] },
+ ],
+ fallback_tier_id: "quick",
+ },
+ }),
+ };
+ const config = buildComplexityRouterConfig(params);
+ expect(config.classifier_type).toBe("jev");
+ const expectedJevConfig = {
+ model: "jev-test",
+ timeout_ms: 4500,
+ instructions: "Choose the configured tier",
+ circuit_breaker_enabled: false,
+ circuit_breaker_cooldown_seconds: 12.5,
+ };
+ expect(config.jev_classifier_config).toEqual(expectedJevConfig);
+ expect(config.classifier_context_window_size).toBe(4);
+ expect(config.classifier_context_budget_chars).toBe(2000);
+ expect(config.classifier_context_per_turn_chars).toBe(450);
+ expect(config.classifier_context_include_assistant_turns).toBe(true);
+ expect(config).not.toHaveProperty("classifier_llm_config");
+ expect(config).not.toHaveProperty("classification_prompt");
+ expect(config).not.toHaveProperty("classification_examples");
+ if (custom) {
+ expect(config.tiers).toEqual({ QUICK: ["fast"], REVIEW: ["strong"] });
+ expect(config.fallback_tier).toBe("QUICK");
+ } else {
+ expect(config.classifier_fallback).toBe("default_model");
+ expect(config.tiers).toEqual(tiers);
+ }
+ });
+
+ it("omits blank JEV instructions and ignores stale JEV settings when saving LLM", () => {
+ const jev = buildComplexityRouterConfig({
+ ...baseParams,
+ classifierType: "jev",
+ jevClassifierConfig: { model: "jev-latest", timeout_ms: 3000, instructions: " " },
+ });
+ expect(jev.jev_classifier_config).toEqual({ model: "jev-latest", timeout_ms: 3000 });
+ const llmParams: BuildComplexityRouterConfigParams = {
+ ...baseParams,
+ classifierType: "llm",
+ classifierLlmConfig: { model: "judge", timeout_ms: 1000 },
+ jevClassifierConfig: jev.jev_classifier_config,
+ };
+ const llm = buildComplexityRouterConfig(llmParams);
+ expect(llm).not.toHaveProperty("jev_classifier_config");
+ });
+
it("emits tiers, classifier_type, and escalation_keywords when nothing else is configured", () => {
const config = buildComplexityRouterConfig(baseParams);
const expected = {
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 4c0ac6850c0..2f29b61a68e 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,9 @@
import { KeywordTierRule } from "./KeywordTierRules";
+import {
+ type JevClassifierConfig,
+ jevClassifierConfigSchema,
+ normalizeJevClassifierConfig,
+} from "./jev_classifier_config";
import {
type CustomTierSet,
type TierRow,
@@ -34,6 +39,7 @@ import {
effectiveTierLabel,
heuristicScoringRoleFor,
usesLlmClassifier,
+ usesClassifierContext,
} from "./ComplexityRouterConfig";
/**
@@ -164,6 +170,7 @@ export interface ComplexityRouterConfigPayload {
tier_labels?: ComplexityTierLabels;
classifier_type: ClassifierType;
classifier_llm_config?: ClassifierLLMConfig;
+ jev_classifier_config?: unknown;
classifier_context_window_size?: number;
classifier_context_budget_chars?: number;
classifier_context_per_turn_chars?: number;
@@ -497,3 +504,20 @@ export const buildComplexityRouterConfig = ({
...customTierWireFields(customTierSet, classifierLlmConfig, planModeMinTier, classificationPrompt),
};
};
+
+ classifier_context_per_turn_chars?: unknown;
+ jevClassifierConfig?: JevClassifierConfig;
+ classifierContextPerTurnChars?: number;
+ jev_classifier_config?: JevClassifierConfig;
+ if (effectiveClassifierType(config) === "jev") {
+ const parsed = jevClassifierConfigSchema.safeParse(config.jev_classifier_config ?? {});
+ return parsed.success ? null : "Enter a JEV model, a positive whole-number timeout and a positive cooldown";
+ }
+ classifierType?: ClassifierType;
+ classifierContextPerTurnChars,
+ | "classifierContextPerTurnChars"
+ jevClassifierConfig,
+ classifierContextPerTurnChars,
+ classifierContextPerTurnChars,
+ ...(effectiveType === "jev" && { jev_classifier_config: normalizeJevClassifierConfig(jevClassifierConfig) }),
+ classifierType: effectiveType,
\ No newline at end of file
diff --git a/ui/litellm-dashboard/src/components/add_model/classifier_type_transition.test.ts b/ui/litellm-dashboard/src/components/add_model/classifier_type_transition.test.ts
new file mode 100644
index 00000000000..a26b39c2980
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/classifier_type_transition.test.ts
@@ -0,0 +1,120 @@
+import { describe, expect, it } from "vitest";
+import { effectiveClassifierType, type ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
+import { transitionClassifierType } from "./classifier_type_transition";
+import { applyTierSetAction } from "./tier_set_actions";
+
+const standard: ComplexityRouterConfigValue = {
+ classifier_type: "llm",
+ classifier_llm_config: { model: "judge", timeout_ms: 20000, classification_rubric: "business" },
+ classifier_context_window_size: 8,
+ classifier_context_budget_chars: 16000,
+ classifier_context_include_assistant_turns: true,
+ classifier_fallback: "default_model",
+ tiers: { SIMPLE: ["efficient"], MEDIUM: ["middle"], COMPLEX: [], REASONING: ["capable"] },
+};
+
+describe("transitionClassifierType", () => {
+ it("switches between LLM and JEV without losing shared routing settings or leaking opposite config", () => {
+ const initial = {
+ ...standard,
+ classification_prompt: "LLM only",
+ classification_examples: "LLM examples",
+ enable_non_reasoning_tier: true,
+ tiers: { ...standard.tiers, NON_REASONING: ["fast"] },
+ plan_mode_min_tier: "NON_REASONING",
+ adaptive: true,
+ };
+ const jev = transitionClassifierType(initial, "jev");
+ const expectedJevConfig = {
+ classifier_type: "jev",
+ jev_classifier_config: { model: "jev-latest", timeout_ms: 3000 },
+ classifier_context_window_size: 8,
+ classifier_context_budget_chars: 16000,
+ classifier_context_include_assistant_turns: true,
+ classifier_fallback: "default_model",
+ adaptive: true,
+ enable_non_reasoning_tier: true,
+ plan_mode_min_tier: "NON_REASONING",
+ tiers: initial.tiers,
+ };
+ expect(jev).toMatchObject(expectedJevConfig);
+ expect(jev.classifier_llm_config).toBeUndefined();
+ expect(jev.classification_prompt).toBeUndefined();
+ expect(jev.classification_examples).toBeUndefined();
+ const custom = applyTierSetAction(jev, [], { kind: "patch", id: "SIMPLE", patch: { name: "QUICK" } }).value;
+ expect(effectiveClassifierType(custom)).toBe("jev");
+ const restored = applyTierSetAction(custom, [], { kind: "restore" }).value;
+ expect(effectiveClassifierType(restored)).toBe("jev");
+ expect(restored.jev_classifier_config).toEqual(jev.jev_classifier_config);
+ const llm = transitionClassifierType(custom, "llm");
+ expect(llm.jev_classifier_config).toBeUndefined();
+ expect(llm.classifier_llm_config).toMatchObject({ model: "" });
+ expect(llm.custom_tier_set).toEqual(custom.custom_tier_set);
+ expect(llm.classifier_context_window_size).toBe(8);
+ });
+
+ it.each(["heuristic_first", "hybrid"] as const)("keeps existing LLM settings when switching to %s", (target) => {
+ const result = transitionClassifierType(standard, target);
+ const expectedSettings = {
+ classifier_type: target,
+ classifier_llm_config: standard.classifier_llm_config,
+ classifier_context_window_size: 8,
+ classifier_context_budget_chars: 16000,
+ classifier_context_include_assistant_turns: true,
+ classifier_fallback: "default_model",
+ };
+ expect(result).toMatchObject(expectedSettings);
+ });
+
+ it.each(["capability", "llm_v2"] as const)("requires explicit policy input for a new %s classifier", (target) => {
+ const result = transitionClassifierType(standard, target);
+ expect(result.classifier_llm_config).toEqual({ model: "judge", timeout_ms: 20000 });
+ expect(result.classifier_fallback).toBeUndefined();
+ if (target === "capability") {
+ expect(result.capability_classifier_config?.base_threshold).toBeNaN();
+ } else {
+ expect(result.llm_v2_config).toMatchObject({ efficient_profile: "", capable_profile: "", harness: "" });
+ expect(result.llm_v2_config?.max_quality_gap).toBeNaN();
+ }
+ expect(standard.tiers.MEDIUM).toEqual(["middle"]);
+ expect(standard.classifier_llm_config?.classification_rubric).toBe("business");
+ });
+
+ it.each([
+ ["capability", "llm"],
+ ["capability", "heuristic_first"],
+ ["capability", "hybrid"],
+ ["llm_v2", "llm"],
+ ["llm_v2", "heuristic_first"],
+ ["llm_v2", "hybrid"],
+ ] as const)("restores the complexity rubric from %s to %s while preserving the judge", (source, target) => {
+ const forecast = transitionClassifierType(standard, source);
+ const result = transitionClassifierType(forecast, target);
+ expect(result.classifier_llm_config).toEqual({
+ model: "judge",
+ timeout_ms: 20000,
+ classification_rubric: "agentic",
+ });
+ expect(result.capability_classifier_config).toBeUndefined();
+ expect(result.llm_v2_config).toBeUndefined();
+ });
+
+ it("clears the inactive non-reasoning pool and plan floor when switching to local classification", () => {
+ const initial: ComplexityRouterConfigValue = {
+ ...standard,
+ tiers: { ...standard.tiers, NON_REASONING: ["chat"] },
+ enable_non_reasoning_tier: true,
+ plan_mode_min_tier: "NON_REASONING",
+ };
+ const result = transitionClassifierType(initial, "heuristic");
+ expect(result.classifier_llm_config).toBeUndefined();
+ expect(result.classifier_context_window_size).toBeUndefined();
+ expect(result.classifier_context_budget_chars).toBeUndefined();
+ expect(result.classifier_context_include_assistant_turns).toBeUndefined();
+ expect(result.classifier_fallback).toBeUndefined();
+ expect(result.tiers.NON_REASONING).toBeUndefined();
+ expect(result.enable_non_reasoning_tier).toBeUndefined();
+ expect(result.plan_mode_min_tier).toBeUndefined();
+ expect(result.tiers.SIMPLE).toEqual(["efficient"]);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/add_model/classifier_type_transition.ts b/ui/litellm-dashboard/src/components/add_model/classifier_type_transition.ts
new file mode 100644
index 00000000000..ba758eac471
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/classifier_type_transition.ts
@@ -0,0 +1,59 @@
+import {
+ type ClassifierType,
+ type ComplexityRouterConfigValue,
+ DEFAULT_CLASSIFIER_CONTEXT_BUDGET_CHARS,
+ DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE,
+ DEFAULT_CLASSIFIER_TIMEOUT_MS,
+ DEFAULT_HEURISTIC_FIRST_MAX_TIER,
+ DEFAULT_HYBRID_BOUNDARY_MARGIN,
+ NEW_CLASSIFIER_CLASSIFICATION_RUBRIC,
+ usesLlmClassifier,
+ usesClassifierContext,
+} from "./ComplexityRouterConfig";
+import { defaultJevClassifierConfig } from "./jev_classifier_config";
+import { isForecastClassifier, prepareForecastClassifier } from "./forecast_classifier_config";
+import { nonReasoningTierFields } from "./nonReasoningTierFields";
+
+export const transitionClassifierType = (
+ value: ComplexityRouterConfigValue,
+ classifierType: ClassifierType,
+): ComplexityRouterConfigValue => {
+ const startsLlmRubric =
+ !value.classifier_llm_config ||
+ (isForecastClassifier(value.classifier_type) && !isForecastClassifier(classifierType));
+ const judgeConfig = value.classifier_llm_config ?? { model: "", timeout_ms: DEFAULT_CLASSIFIER_TIMEOUT_MS };
+ const nextValue: ComplexityRouterConfigValue = {
+ ...value,
+ jev_classifier_config:
+ classifierType === "jev" ? value.jev_classifier_config ?? defaultJevClassifierConfig() : undefined,
+ classification_prompt: classifierType === "jev" ? undefined : value.classification_prompt,
+ classification_examples: classifierType === "jev" ? undefined : value.classification_examples,
+ classifier_llm_config: usesLlmClassifier(classifierType)
+ ? {
+ ...judgeConfig,
+ ...(startsLlmRubric && { classification_rubric: NEW_CLASSIFIER_CLASSIFICATION_RUBRIC }),
+ }
+ : undefined,
+ classifier_context_window_size: usesClassifierContext(classifierType)
+ ? value.classifier_context_window_size ?? DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE
+ : undefined,
+ classifier_context_budget_chars: usesClassifierContext(classifierType)
+ ? value.classifier_context_budget_chars ?? DEFAULT_CLASSIFIER_CONTEXT_BUDGET_CHARS
+ : undefined,
+ classifier_context_per_turn_chars: usesClassifierContext(classifierType)
+ ? value.classifier_context_per_turn_chars
+ : undefined,
+ classifier_context_include_assistant_turns: usesClassifierContext(classifierType)
+ ? value.classifier_context_include_assistant_turns
+ : undefined,
+ classifier_fallback: usesClassifierContext(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),
+ };
+ return prepareForecastClassifier(nextValue, classifierType);
+};
diff --git a/ui/litellm-dashboard/src/components/add_model/classifier_types.ts b/ui/litellm-dashboard/src/components/add_model/classifier_types.ts
new file mode 100644
index 00000000000..ec88166ed2e
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/classifier_types.ts
@@ -0,0 +1,15 @@
+export type ClassifierType =
+ | "heuristic"
+ | "heuristic_v2"
+ | "llm"
+ | "jev"
+ | "heuristic_first"
+ | "hybrid"
+ | "capability"
+ | "llm_v2";
+
+export const usesLlmClassifier = (classifierType: ClassifierType): boolean =>
+ (["llm", "heuristic_first", "hybrid", "capability", "llm_v2"] as const).some((type) => type === classifierType);
+
+export const usesClassifierContext = (classifierType: ClassifierType): boolean =>
+ classifierType === "jev" || usesLlmClassifier(classifierType);
diff --git a/ui/litellm-dashboard/src/components/add_model/jev_classifier_config.ts b/ui/litellm-dashboard/src/components/add_model/jev_classifier_config.ts
new file mode 100644
index 00000000000..478c763351c
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/add_model/jev_classifier_config.ts
@@ -0,0 +1,30 @@
+import { z } from "zod";
+
+const jevClassifierConfigFields = {
+ model: z.string().trim().min(1).default("jev-latest"),
+ timeout_ms: z.number().int().positive().default(3000),
+ instructions: z
+ .string()
+ .nullish()
+ .transform((value) => value ?? undefined),
+ circuit_breaker_enabled: z.boolean().optional(),
+ circuit_breaker_cooldown_seconds: z.number().finite().positive().optional(),
+};
+
+export const jevClassifierConfigSchema = z.object(jevClassifierConfigFields);
+
+export type JevClassifierConfig = z.infer