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 @@ -1115,3 +1115,105 @@ describe("LLM V2 configuration preservation", () => {
expect(saved).not.toHaveProperty("classifier_llm_config");
});
});

describe("untouched save round trip", () => {
const STORED_PRE_MANAGED_BOOLEANS: Record<string, unknown> = {
tiers: { SIMPLE: ["gpt-4o-mini"], MEDIUM: ["gpt-4o"], COMPLEX: ["opus"], REASONING: ["o1"] },
tier_model_configs: { REASONING: [{ model_name: "o1", litellm_params: { reasoning_effort: "high" } }] },
default_model: "gpt-4o",
plan_mode_min_tier: "COMPLEX",
tier_labels: { SIMPLE: "Cheap" },
classifier_type: "heuristic_first",
heuristic_v2_success_threshold: 0.89,
heuristic_first_max_tier: "SIMPLE",
classifier_llm_config: { model: "gpt-4o-mini", timeout_ms: 3000, reasoning_effort: "low" },
classifier_context_window_size: 5,
classifier_context_budget_chars: 4000,
classifier_context_include_assistant_turns: true,
classifier_fallback: "default_model",
classification_prompt: "Route for a payments team.",
classification_examples: "- refund status -> SIMPLE",
classification_mode: "user_turn",
session_affinity: true,
session_affinity_ttl_seconds: 300,
modality_routing: true,
modality_pin_override: true,
deployment_affinity: false,
adaptive: true,
adaptive_weights: { quality: 0.4, cost: 0.6 },
tier_distance_penalty: 0.25,
adaptive_eligible: "all",
return_raw_model_name: true,
tier_boundaries: { simple_medium: 0.2, medium_complex: 0.4, complex_reasoning: 0.7 },
token_thresholds: { simple: 20, complex: 500 },
dimension_weights: { tokenCount: 0.1 },
custom_dimensions: [{ name: "domain", weight: 0.9, keywords: ["orbitmesh"] }],
reasoning_override_min_score: 0.3,
enable_context_window_escalation: false,
context_window_escalation_buffer: 0.9,
code_keywords: ["async", "await"],
reasoning_keywords: ["prove"],
technical_keywords: ["api"],
simple_keywords: ["hello"],
plan_mode_patterns: ["plan now"],
route_housekeeping_to_cheapest_tier: true,
housekeeping_patterns: ["conversation title"],
reminder_markers: [{ open: "<System-Reminder>", close: "</System-Reminder>" }],
max_tokens_from_tier_model: true,
};

it("returns the stored config unchanged when nothing was edited", () => {
const hydrated = hydrateComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, undefined);
expect(buildUpdatedComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, hydrated)).toEqual(
STORED_PRE_MANAGED_BOOLEANS,
);
});

it("keeps both housekeeping and max-token booleans stored as true", () => {
const hydrated = hydrateComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, undefined);
const saved = buildUpdatedComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, hydrated);
expect(saved.route_housekeeping_to_cheapest_tier).toBe(true);
expect(saved.max_tokens_from_tier_model).toBe(true);
});

it("keeps the stored reminder marker casing", () => {
const hydrated = hydrateComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, undefined);
const saved = buildUpdatedComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, hydrated);
expect(saved.reminder_markers).toEqual(STORED_PRE_MANAGED_BOOLEANS.reminder_markers);
});

it("lets an edited toggle win over the stored value", () => {
const hydrated = hydrateComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, undefined);
const edited = {
...hydrated,
route_housekeeping_to_cheapest_tier: false,
max_tokens_from_tier_model: false,
};
const saved = buildUpdatedComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, edited);
expect(saved.route_housekeeping_to_cheapest_tier).toBe(false);
expect(saved.max_tokens_from_tier_model).toBe(false);

const storedDisabled: Record<string, unknown> = {
...STORED_PRE_MANAGED_BOOLEANS,
route_housekeeping_to_cheapest_tier: false,
max_tokens_from_tier_model: false,
};
const enabled = {
...hydrateComplexityRouterConfig(storedDisabled, undefined),
route_housekeeping_to_cheapest_tier: true,
max_tokens_from_tier_model: true,
};
const resaved = buildUpdatedComplexityRouterConfig(storedDisabled, enabled);
expect(resaved).not.toHaveProperty("route_housekeeping_to_cheapest_tier");
expect(resaved).not.toHaveProperty("max_tokens_from_tier_model");
});

it("lowercases reminder markers the user edited", () => {
const hydrated = hydrateComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, undefined);
const saved = buildUpdatedComplexityRouterConfig(STORED_PRE_MANAGED_BOOLEANS, {
...hydrated,
reminder_markers: [{ open: "<Other>", close: "</Other>" }],
});
expect(saved.reminder_markers).toEqual([{ open: "<other>", close: "</other>" }]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import { isComplexityRouter } from "../add_model/auto_router_strategies";
import {
type BuildComplexityRouterConfigParams,
type StoredComplexityRouterConfig,
buildComplexityRouterConfig,
getClassifierModelError,
getHeuristicV2SuccessThresholdError,
Expand Down Expand Up @@ -75,8 +76,8 @@
interface EditAutoRouterModalProps {
isVisible: boolean;
onCancel: () => void;
onSuccess: (updatedModel: any) => void;

Check warning on line 79 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
modelData: any;

Check warning on line 80 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
accessToken: string;
userRole: string;
isMemberManaged?: boolean;
Expand Down Expand Up @@ -152,6 +153,12 @@
"match_threshold",
]);

const UNEDITED_STORED_VALUE_KEYS: readonly (keyof ComplexityRouterConfigValue)[] = [
"route_housekeeping_to_cheapest_tier",
"reminder_markers",
"max_tokens_from_tier_model",
];

const toRecord = (value: unknown): Record<string, unknown> => {
const parsed: unknown = typeof value === "string" ? JSON.parse(value) : value;
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
Expand Down Expand Up @@ -179,7 +186,15 @@
customTechnicalKeywords?: string[],
keywordMatching?: KeywordMatchingState,
): Record<string, unknown> => {
const stored = toRecord(storedConfig);
const hydratedFromStored = hydrateComplexityRouterConfig(stored as StoredComplexityRouterConfig, undefined);
const unedited = new Set<string>(
UNEDITED_STORED_VALUE_KEYS.filter(
(key) => key in stored && JSON.stringify(value[key]) === JSON.stringify(hydratedFromStored[key]),
),
);
const isManaged = (key: string): boolean => {
if (unedited.has(key)) return false;
if (key === "classifier_context_per_turn_chars") {
return !usesClassifierContext(effectiveClassifierType(value)) || Object.prototype.hasOwnProperty.call(value, key);
}
Expand All @@ -190,7 +205,7 @@
};
const dropped = customTierDroppedKeys(value);
const preservedConfig = Object.fromEntries(
Object.entries(toRecord(storedConfig)).filter(([key]) => !isManaged(key) && !dropped.includes(key)),
Object.entries(stored).filter(([key]) => !isManaged(key) && !dropped.includes(key)),
);

const builderParams: BuildComplexityRouterConfigParams = {
Expand All @@ -208,6 +223,7 @@
const unowned: readonly string[] = [
...(keywordMatching === undefined ? [...KEYWORD_MATCHING_KEYS].filter((key) => !isManaged(key)) : []),
...(customTechnicalKeywords === undefined ? ["custom_technical_keywords"] : []),
...unedited,
];
return {
...preservedConfig,
Expand All @@ -223,7 +239,7 @@
accessToken,
userRole,
isMemberManaged = false,
}) => {

Check warning on line 242 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Arrow function has a complexity of 31. Maximum allowed is 20
const [loading, setLoading] = useState(false);
const [modelAccessGroups, setModelAccessGroups] = useState<string[]>([]);
const [modelInfo, setModelInfo] = useState<ModelGroup[]>([]);
Expand Down Expand Up @@ -254,7 +270,7 @@
modelData?.litellm_params?.complexity_router_config,
complexityRouterConfig,
customTechnicalKeywords,
{ keywordTierRules, escalationKeywords, semanticMatchingEnabled, embeddingModel, matchThreshold },

Check warning on line 273 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Object literal with 5 properties passed inline as an argument; assign it to a named variable first
),
},
isVisible && isComplexityRouterModel,
Expand Down Expand Up @@ -291,7 +307,7 @@
if (isVisible && modelData) {
initializeForm();
}
}, [isVisible, modelData]);

Check warning on line 310 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

React Hook useEffect has a missing dependency: 'initializeForm'. Either include it or remove the dependency array

useEffect(() => {
let active = true;
Expand All @@ -299,7 +315,7 @@
if (!accessToken) return;
try {
const response = await modelAvailableCall(accessToken, "", "", false, null, true, true);
setModelAccessGroups(response["data"].map((model: any) => model["id"]));

Check warning on line 318 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
} catch (error) {
console.error("Error fetching model access groups:", error);
}
Expand Down Expand Up @@ -327,7 +343,7 @@
};
}, [isVisible, accessToken, isMemberManaged, modelData?.model_info?.team_id]);

const initializeForm = () => {

Check warning on line 346 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Arrow function has a complexity of 24. Maximum allowed is 20
setEditingTiers(false);
try {
if (isComplexityRouterModel) {
Expand Down Expand Up @@ -400,7 +416,7 @@
}
};

const saveValues = async (values: EditAutoRouterFormValues) => {

Check warning on line 419 in ui/litellm-dashboard/src/components/edit_auto_router/edit_auto_router_modal.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Async arrow function has a complexity of 28. Maximum allowed is 20
if (routerAvailability.saveBlockedReason) {
toast.fromError(routerAvailability.saveBlockedReason);
return;
Expand Down
Loading