fix(ui): adapt the jev dashboard pieces to stable/1.101.x

This commit is contained in:
mateo 2026-09-23 00:50:58 +00:00
parent 347ec67498
commit 662a89290f
6 changed files with 86 additions and 101 deletions

View file

@ -1,5 +1,6 @@
import type { JevClassifierConfig } from "./jev_classifier_config";
import { type ClassifierType } from "./classifier_types";
import { type ClassifierType, usesLlmClassifier } from "./classifier_types";
import { TIER_DESCRIPTIONS, TIER_KEYS } from "./tier_descriptions";
export { type ClassifierType, usesLlmClassifier, usesClassifierContext } from "./classifier_types";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { MultiSelect } from "@/components/shared/MultiSelect";
@ -142,16 +143,6 @@ export interface ClassifierLLMConfig {
system_prompt?: string;
}
export type ClassifierType = "heuristic" | "heuristic_v2" | "llm" | "heuristic_first" | "hybrid";
/**
* Whether this router can call classifier_llm_config.model. Mirrors the backend's
* ComplexityRouterConfig.uses_llm_classifier, and is the single gate for every classifier-only
* control and payload key, so a new chaining type cannot strip knobs the operator set.
*/
export const usesLlmClassifier = (classifierType: ClassifierType): boolean =>
classifierType === "llm" || classifierType === "heuristic_first" || classifierType === "hybrid";
export type ClassifierFallback = "heuristic" | "default_model";
export const DEFAULT_CLASSIFIER_FALLBACK: ClassifierFallback = "heuristic";
@ -498,36 +489,7 @@ interface ComplexityRouterConfigProps {
showValidationErrors?: boolean;
}
export const TIER_DESCRIPTIONS: Record<
keyof ComplexityTiers,
{ label: string; description: string; examples: string }
> = {
SIMPLE: {
label: "Simple",
description: "Basic questions, greetings, simple factual queries",
examples: '"Hello!", "What is Python?", "Thanks!"',
},
MEDIUM: {
label: "Medium",
description: "Standard queries requiring some reasoning or explanation",
examples: '"Explain how REST APIs work", "Debug this error"',
},
COMPLEX: {
label: "Complex",
description: "Technical, multi-part requests requiring deep knowledge",
examples: '"Design a microservices architecture", "Implement a rate limiter"',
},
REASONING: {
label: "Reasoning",
description: "Chain-of-thought, analysis, explicit reasoning requests",
examples: '"Think step by step...", "Analyze the pros and cons..."',
},
};
export const TIER_KEYS = Object.keys(TIER_DESCRIPTIONS) as Array<keyof ComplexityTiers>;
export const effectiveTierLabel = (tier: keyof ComplexityTiers, tierLabels: ComplexityTierLabels | undefined): string =>
tierLabels?.[tier]?.trim() || TIER_DESCRIPTIONS[tier].label;
export { TIER_DESCRIPTIONS, TIER_KEYS, effectiveTierLabel } from "./tier_descriptions";
export const DEFAULT_HEURISTIC_FIRST_MAX_TIER = "SIMPLE";

View file

@ -803,8 +803,6 @@ describe("buildComplexityRouterConfig scorer knobs", () => {
expect(buildComplexityRouterConfig(tuned).tier_boundaries).toEqual(BOUNDARIES);
});
const uncheckedParams: unknown = {
const payload = buildComplexityRouterConfig(uncheckedParams as BuildComplexityRouterConfigParams);
it("drops them when the classifier falls back to the default model and nothing is scored", () => {
expect(buildComplexityRouterConfig(llmWithDefaultFallback)).not.toHaveProperty("tier_boundaries");
});

View file

@ -124,6 +124,7 @@ const scorerKnobPayload = ({
};
export interface BuildComplexityRouterConfigParams {
tiers: ComplexityTiers;
enableNonReasoningTier?: boolean;
customTierSet?: CustomTierSet;
defaultModel: string | undefined;
planModeMinTier: string | undefined;
@ -187,6 +188,7 @@ export interface TierDefinitionPayload {
export interface ComplexityRouterConfigPayload {
tiers: ComplexityTiers | Record<string, string[]>;
enable_non_reasoning_tier?: boolean;
tier_definitions?: TierDefinitionPayload[];
fallback_tier?: string;
default_model?: string;
@ -511,6 +513,7 @@ export const hydrateBuiltInTiers = (
export const buildComplexityRouterConfig = ({
tiers,
enableNonReasoningTier,
customTierSet,
defaultModel,
planModeMinTier,
@ -585,11 +588,12 @@ export const buildComplexityRouterConfig = ({
};
// An edited tier set forces the LLM classifier, so llm-only inputs must survive a classifier_type
// the form never rewrote. The UI gates the same controls on this, not on the raw value.
const effectiveType: ClassifierType = customTierSet ? "llm" : classifierType;
const effectiveType = effectiveClassifierType({ custom_tier_set: customTierSet, classifier_type: classifierType });
const payload: ComplexityRouterConfigPayload = {
tiers,
// The backend rejects the flag beside a custom tier set.
...(!customTierSet && enableNonReasoningTier && { enable_non_reasoning_tier: true }),
...(serializedTierModelConfigs && { tier_model_configs: serializedTierModelConfigs }),
...(defaultModel?.trim() && { default_model: defaultModel }),
...(planModeMinTier?.trim() && { plan_mode_min_tier: planModeMinTier }),

View file

@ -0,0 +1,37 @@
import type { ComplexityTierLabels, ComplexityTiers } from "./ComplexityRouterConfig";
export const TIER_DESCRIPTIONS: Record<
keyof ComplexityTiers,
{ label: string; description: string; examples: string }
> = {
NON_REASONING: {
label: "Non-reasoning",
description: "Operational relay work: passing information along with no judgment about it",
examples: '"Reformat this tool output", "Acknowledge the write succeeded"',
},
SIMPLE: {
label: "Simple",
description: "Basic questions, greetings, simple factual queries",
examples: '"Hello!", "What is Python?", "Thanks!"',
},
MEDIUM: {
label: "Medium",
description: "Standard queries requiring some reasoning or explanation",
examples: '"Explain how REST APIs work", "Debug this error"',
},
COMPLEX: {
label: "Complex",
description: "Technical, multi-part requests requiring deep knowledge",
examples: '"Design a microservices architecture", "Implement a rate limiter"',
},
REASONING: {
label: "Reasoning",
description: "Chain-of-thought, analysis, explicit reasoning requests",
examples: '"Think step by step...", "Analyze the pros and cons..."',
},
};
export const TIER_KEYS = Object.keys(TIER_DESCRIPTIONS) as Array<keyof ComplexityTiers>;
export const effectiveTierLabel = (tier: keyof ComplexityTiers, tierLabels: ComplexityTierLabels | undefined): string =>
tierLabels?.[tier]?.trim() || TIER_DESCRIPTIONS[tier].label;

View file

@ -16,7 +16,7 @@ import ModelChoiceCombobox, { type ModelChoice } from "../add_model/ModelChoiceC
import { modelAvailableCall, modelPatchUpdateCall, validateAutoRouterConfig } from "../networking";
import { fetchAvailableModels, ModelGroup } from "@/components/llm_calls/fetch_models";
import RouterConfigBuilder from "../add_model/RouterConfigBuilder";
import { hydrateTierModelParams, normalizeTierModels } from "../add_model/complexity_router_tiers";
import { hydrateTierModelParams } from "../add_model/complexity_router_tiers";
import {
type ActiveTierSet,
CUSTOM_TIER_OMITTED_KEYS,
@ -63,9 +63,8 @@ import ComplexityRouterConfig, {
ClassifierLLMConfig,
ClassifierType,
ComplexityRouterConfigValue,
ComplexityTiers,
effectiveClassifierType,
heuristicScoringRole,
DEFAULT_ADAPTIVE_WEIGHTS,
DEFAULT_SESSION_AFFINITY,
DEFAULT_DEPLOYMENT_AFFINITY,
@ -139,6 +138,11 @@ export interface StoredComplexityRouterConfig {
* 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.
*/
const asNumber = (v: unknown): number | undefined => (typeof v === "number" ? v : undefined);
const asBoolean = (v: unknown): boolean | undefined => (typeof v === "boolean" ? v : undefined);
const asNonEmptyString = (v: unknown): string | undefined =>
typeof v === "string" && v.trim() !== "" ? v : undefined;
export const hydrateComplexityRouterConfig = (
parsedConfig: StoredComplexityRouterConfig,
complexityRouterDefaultModel: string | null | undefined,
@ -166,38 +170,19 @@ export const hydrateComplexityRouterConfig = (
? 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_context_window_size: asNumber(parsedConfig.classifier_context_window_size),
classifier_context_budget_chars: asNumber(parsedConfig.classifier_context_budget_chars),
classifier_context_per_turn_chars: asNumber(parsedConfig.classifier_context_per_turn_chars),
classifier_context_include_assistant_turns: asBoolean(
parsedConfig.classifier_context_include_assistant_turns,
),
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,
classification_prompt: asNonEmptyString(parsedConfig.classification_prompt),
classification_examples: asNonEmptyString(parsedConfig.classification_examples),
heuristic_first_max_tier: asNonEmptyString(parsedConfig.heuristic_first_max_tier),
hybrid_boundary_margin:
typeof parsedConfig.hybrid_boundary_margin === "number" ? parsedConfig.hybrid_boundary_margin : undefined,
classification_mode:
@ -218,23 +203,14 @@ export const hydrateComplexityRouterConfig = (
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,
deployment_affinity: asBoolean(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,
enable_context_window_escalation: asBoolean(parsedConfig.enable_context_window_escalation),
context_window_escalation_buffer: asNumber(parsedConfig.context_window_escalation_buffer),
stall_escalation_enabled: parsedConfig.stall_escalation_enabled === true || undefined,
stall_escalation_window:
typeof parsedConfig.stall_escalation_window === "number" ? parsedConfig.stall_escalation_window : undefined,

View file

@ -155,6 +155,27 @@ function Row({ label, children }: { label: string; children: React.ReactNode })
);
}
function ClassifierRows({ decision }: { decision: RoutingDecision }) {
return (
<>
{decision.classifier_model && <Row label="Classifier model">{decision.classifier_model}</Row>}
{decision.classifier_confidence != null && (
<Row label="Confidence">{(decision.classifier_confidence * 100).toFixed(1)}%</Row>
)}
{decision.classifier_probabilities && (
<Row label="Probabilities">
{Object.entries(decision.classifier_probabilities).map(([name, probability]) => (
<div key={name}>
{name}: {(probability * 100).toFixed(1)}%
</div>
))}
</Row>
)}
{decision.classifier_cost != null && <Row label="Classifier cost">${decision.classifier_cost.toFixed(8)}</Row>}
</>
);
}
export function RoutingDecisionCard({
decision,
className,
@ -213,20 +234,7 @@ export function RoutingDecisionCard({
{requestType && <Row label="Request type">{requestType}</Row>}
<Row label="Decided by">{describeCause(decision)}</Row>
{decision.classifier_model && <Row label="Classifier model">{decision.classifier_model}</Row>}
{decision.classifier_confidence != null && (
<Row label="Confidence">{(decision.classifier_confidence * 100).toFixed(1)}%</Row>
)}
{decision.classifier_probabilities && (
<Row label="Probabilities">
{Object.entries(decision.classifier_probabilities).map(([name, probability]) => (
<div key={name}>
{name}: {(probability * 100).toFixed(1)}%
</div>
))}
</Row>
)}
{decision.classifier_cost != null && <Row label="Classifier cost">${decision.classifier_cost.toFixed(8)}</Row>}
<ClassifierRows decision={decision} />
{score !== undefined && (
<Row label="Score">