refactor(dashboard): simplify tier config serialization

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Krrish Dholakia 2026-08-16 03:03:26 +00:00
parent 5f12fdfca2
commit 003148ceef
7 changed files with 88 additions and 15 deletions

View file

@ -1,5 +1,6 @@
import { fireEvent, renderWithProviders, screen, within } from "../../../tests/test-utils";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { vi } from "vitest";
import ComplexityRouterConfig, { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
vi.mock(
@ -102,6 +103,42 @@ describe("ComplexityRouterConfig", () => {
expect(screen.queryByRole("option", { name: "xhigh" })).not.toBeInTheDocument();
});
it("prunes tier params when a model is removed", async () => {
const user = userEvent.setup();
const onChange = vi.fn();
const Wrapper = () => {
const [value, setValue] = useState<ComplexityRouterConfigValue>({
...defaultValue,
tiers: { ...defaultValue.tiers, REASONING: ["reasoning-model"] },
tier_model_params: {},
});
return (
<ComplexityRouterConfig
{...baseProps}
modelInfo={[{ model_group: "reasoning-model", mode: "chat", supports_reasoning: true }]}
value={value}
onChange={(nextValue) => {
setValue(nextValue);
onChange(nextValue);
}}
/>
);
};
renderWithProviders(<Wrapper />);
await user.click(screen.getByRole("combobox", { name: "Reasoning effort for reasoning-model" }));
await user.click(screen.getByText("high"));
await user.click(screen.getByRole("combobox", { name: "Select model(s) for reasoning queries" }));
await user.keyboard("{Backspace}");
expect(onChange).toHaveBeenLastCalledWith(
expect.objectContaining({
tiers: expect.objectContaining({ REASONING: [] }),
tier_model_params: {},
}),
);
});
it("should display the how classification works section", () => {
renderWithProviders(<ComplexityRouterConfig {...baseProps} />);
fireEvent.click(screen.getByText("Advanced: Classification Method"));

View file

@ -268,7 +268,7 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
? { ...existingParams, reasoning_effort: reasoningEffort }
: Object.fromEntries(Object.entries(existingParams).filter(([key]) => key !== "reasoning_effort"));
const updatedTierModelParams =
updatedParams && Object.keys(updatedParams).length > 0
Object.keys(updatedParams).length > 0
? { ...tierModelParams, [model]: updatedParams }
: Object.fromEntries(Object.entries(tierModelParams).filter(([key]) => key !== model));
onChange({

View file

@ -15,7 +15,7 @@ import {
effectiveTierLabel,
heuristicScoringRoleFor,
} from "./ComplexityRouterConfig";
import { serializeTierConfig, type TierModelParamsByTier } from "./complexity_router_tiers";
import { serializeTierConfig, type SerializedTierConfig, type TierModelParamsByTier } from "./complexity_router_tiers";
/**
* Drop an empty system_prompt so the payload carries an override only when there is one. The
@ -104,7 +104,7 @@ export interface BuildComplexityRouterConfigParams {
}
export interface ComplexityRouterConfigPayload {
tiers: Record<string, unknown>;
tiers: SerializedTierConfig;
default_model?: string;
plan_mode_min_tier?: string;
tier_labels?: ComplexityTierLabels;

View file

@ -74,7 +74,7 @@ describe("normalizeTierModels", () => {
const tiers = { REASONING: normalizeTierModels(stored) };
const params = { REASONING: extractTierModelParams(stored) };
expect(serializeTierConfig(tiers, params)).toEqual({ REASONING: stored });
expect(serializeTierConfig(tiers, params)).toEqual({ REASONING: [stored] });
});
});

View file

@ -51,7 +51,7 @@ export const tierOptions = (
export type TierModelParams = Record<string, unknown>;
export type SerializedTierModel = string | { model_name: string; litellm_params: TierModelParams };
export type TierModelParamsByTier = Partial<Record<string, Record<string, TierModelParams>>>;
export type TierModelParamsByTier = Partial<Record<keyof ComplexityTiers, Record<string, TierModelParams>>>;
export const extractTierModelParams = (value: unknown): Record<string, TierModelParams> => {
const entries = Array.isArray(value) ? value : [value];
@ -75,19 +75,23 @@ export const extractTierModelParams = (value: unknown): Record<string, TierModel
export const serializeTierModels = (
models: string[],
paramsByModel: Record<string, TierModelParams> | undefined,
): SerializedTierModel | SerializedTierModel[] => {
): SerializedTierModel[] => {
const entries = models.map((model) => {
const params = paramsByModel?.[model];
return params && Object.keys(params).length > 0 ? { model_name: model, litellm_params: params } : model;
});
if (entries.length === 1 && typeof entries[0] !== "string") return entries[0];
return entries;
};
export type SerializedTierConfig = Partial<Record<keyof ComplexityTiers, SerializedTierModel[]>>;
export const serializeTierConfig = (
tiers: Partial<ComplexityTiers>,
paramsByTier: TierModelParamsByTier | undefined,
): Record<string, SerializedTierModel | SerializedTierModel[]> =>
): SerializedTierConfig =>
Object.fromEntries(
Object.entries(tiers).map(([tier, models]) => [tier, serializeTierModels(models ?? [], paramsByTier?.[tier])]),
);
Object.entries(tiers).map(([tier, models]) => [
tier,
serializeTierModels(models ?? [], paramsByTier?.[tier as keyof ComplexityTiers]),
]),
) as SerializedTierConfig;

View file

@ -68,7 +68,39 @@ describe("buildUpdatedComplexityRouterConfig keyword matching", () => {
},
};
expect(buildUpdatedComplexityRouterConfig(stored, value, undefined, hydratedState).tiers).toEqual(stored.tiers);
expect(buildUpdatedComplexityRouterConfig(stored, value, undefined, hydratedState).tiers).toEqual({
...stored.tiers,
SIMPLE: [stored.tiers.SIMPLE],
});
});
it("keeps plain tier lists unchanged and widens a stored bare string tier", () => {
const stored = {
...STORED,
tiers: {
SIMPLE: "gpt-4o-mini",
MEDIUM: ["gpt-4o"],
COMPLEX: ["claude-sonnet-4"],
REASONING: ["o1-preview"],
},
};
const formValue = {
...FORM_VALUE,
tiers: {
SIMPLE: ["gpt-4o-mini"],
MEDIUM: ["gpt-4o"],
COMPLEX: ["claude-sonnet-4"],
REASONING: ["o1-preview"],
},
};
expect(buildUpdatedComplexityRouterConfig(stored, formValue, undefined, hydratedState).tiers).toEqual({
SIMPLE: ["gpt-4o-mini"],
MEDIUM: ["gpt-4o"],
COMPLEX: ["claude-sonnet-4"],
REASONING: ["o1-preview"],
});
});
it("persists an edited keyword rule", () => {

View file

@ -348,10 +348,10 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
const hydratedComplexityRouterConfig: ComplexityRouterConfigValue = {
tiers: hydratedTiers,
tier_model_params: Object.fromEntries(
["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"].map((tier) => [
tier,
extractTierModelParams(parsedConfig.tiers?.[tier]),
]),
["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"].flatMap((tier) => {
const params = extractTierModelParams(parsedConfig.tiers?.[tier]);
return Object.keys(params).length > 0 ? [[tier, params]] : [];
}),
),
default_model: hydratePinnedDefaultModel(
parsedConfig.default_model,