fix(ui): carry a preset's per-tier litellm_params through the prefill

buildPresetPrefill rebuilt the complexity router config field by field and
never emitted tier_model_params, so a bundled preset that declares per-model
litellm_params (reasoning_effort, for instance) lost them before the create
form ever saw them. Both halves of the round trip already existed:
hydrateTierModelParams reads either storage shape, and serializeTierModelConfigs
writes them back on submit.

Hydrating alone is not enough. Tier entries get rewritten to the caller's
registered model spelling, which can differ from the preset's literal string by
version-separator punctuation, while the params stay keyed on what the preset
spelled. serializeTierModelConfigs then drops any param whose key is not in the
tier, silently. The param keys go through the same resolver as the tier entries.
This commit is contained in:
Tin Chi Lo 2026-08-26 20:28:07 -07:00
parent 02035120e4
commit dc25608ea1
2 changed files with 59 additions and 0 deletions

View file

@ -544,5 +544,51 @@ describe("autorouter_presets", () => {
const prefill = buildPresetPrefill(config, groupsOnly(["claude-sonnet-4.5"]));
expect(prefill.complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-sonnet-4.5"]);
});
it("prefills the per-model litellm_params a preset carries in tier_model_configs", () => {
const config = {
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: ["o3"] },
tier_model_configs: {
REASONING: [{ model_name: "o3", litellm_params: { reasoning_effort: "high" } }],
},
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
const prefill = buildPresetPrefill(config, groupsOnly(["gpt-5-nano", "o3"]));
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
REASONING: { o3: { reasoning_effort: "high" } },
});
});
// The params key on the preset's own spelling while the tier entry gets rewritten to the
// caller's. Leaving the key alone names a model the tier no longer holds, and
// serializeTierModelConfigs then drops the params on submit without saying so.
it("rewrites a param key to the same registered spelling its tier entry was rewritten to", () => {
const config = {
tiers: { SIMPLE: [], MEDIUM: [], COMPLEX: [], REASONING: ["claude-sonnet-4-5"] },
tier_model_configs: {
REASONING: [{ model_name: "claude-sonnet-4-5", litellm_params: { reasoning_effort: "high" } }],
},
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
const prefill = buildPresetPrefill(config, groupsOnly(["claude-sonnet-4.5"]));
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
REASONING: { "claude-sonnet-4.5": { reasoning_effort: "high" } },
});
});
it("leaves tier_model_params undefined for a preset that carries no per-model params", () => {
const config = {
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
const prefill = buildPresetPrefill(config, groupsOnly(["gpt-5-nano"]));
expect(prefill.complexityRouterConfig.tier_model_params).toBeUndefined();
});
});
});

View file

@ -12,6 +12,7 @@ import {
} from "@/components/add_model/ComplexityRouterConfig";
import { KeywordTierRule } from "@/components/add_model/KeywordTierRules";
import { hydrateKeywordTierRules } from "@/components/add_model/complexity_router_keywords";
import { TierModelParamsByTier, hydrateTierModelParams } from "@/components/add_model/complexity_router_tiers";
import { DEFAULT_ESCALATION_KEYWORDS } from "@/components/add_model/EscalationKeywords";
import { DEFAULT_MATCH_THRESHOLD } from "@/components/add_model/SemanticKeywordMatching";
import presetsRaw from "@/autorouter_presets.json";
@ -244,6 +245,17 @@ export const buildPresetPrefill = (
): PresetPrefill => {
const resolve = (model: string): string => resolveAvailableModel(model, availability) ?? model;
const resolveTier = (models: string[]): string[] => models.map(resolve);
// Params key on the model name the preset spells while every tier entry is rewritten to the
// caller's registered spelling, so the keys have to be rewritten the same way. Otherwise
// serializeTierModelConfigs drops them for naming a model the tier no longer holds.
const resolveParamKeys = (params: TierModelParamsByTier | undefined): TierModelParamsByTier | undefined =>
params &&
Object.fromEntries(
Object.entries(params).map(([tier, byModel]) => [
tier,
Object.fromEntries(Object.entries(byModel).map(([model, litellmParams]) => [resolve(model), litellmParams])),
]),
);
return {
complexityRouterConfig: {
@ -253,6 +265,7 @@ export const buildPresetPrefill = (
COMPLEX: resolveTier(config.tiers.COMPLEX),
REASONING: resolveTier(config.tiers.REASONING),
},
tier_model_params: resolveParamKeys(hydrateTierModelParams(config.tiers, config.tier_model_configs)),
tier_labels: hydrateTierLabels(config.tier_labels),
classifier_type: config.classifier_type,
classifier_llm_config: config.classifier_llm_config && {