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
2 changes: 1 addition & 1 deletion ui/litellm-dashboard/src/autorouter_presets.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"openai_family": {
"label": "OpenAI Family",
"description": "Routes across the GPT model family: gpt-5-nano for simple queries, gpt-5-mini for medium, gpt-5 for complex, o3 for reasoning-heavy requests.",
"description": "Routes across the GPT model family: gpt-5.4-nano for simple queries, gpt-5.4-mini for medium, gpt-5.4 for complex, o3 for reasoning-heavy requests.",
"complexity_router_config": {
"tiers": {
"SIMPLE": ["gpt-5.4-nano"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,20 @@ import { handleAddAutoRouterSubmit } from "./handle_add_auto_router_submit";
import { getMissingTiersError } from "./build_complexity_router_config";
import { testAutoRouterRouting } from "../networking";
import { ModelGroup } from "@/components/llm_calls/fetch_models";
import { getAllPresets, getPresetByKey, getRequiredModelsInPreset } from "@/lib/autorouter_presets";

// Every model referenced by both bundled family presets. A caller holding all of these can select
// either preset; dropping any one greys out the preset that names it.
const ANTHROPIC_PRESET = getPresetByKey("anthropic_family")!;
const ANTHROPIC_TIERS = ANTHROPIC_PRESET.complexity_router_config.tiers;

// Every model referenced by the bundled family presets, derived from the presets themselves so
// that renaming a preset's models in autorouter_presets.json does not red these tests. A caller
// holding all of these can select either preset; dropping any one greys out the preset that
// names it.
Comment thread
tin-berri marked this conversation as resolved.
const ALL_FAMILY_MODELS: ModelGroup[] = [
{ model_group: "claude-haiku-4-5", mode: "chat" },
{ model_group: "claude-sonnet-4-5", mode: "chat" },
{ model_group: "claude-opus-5", mode: "chat" },
{ model_group: "gpt-5-nano", mode: "chat" },
{ model_group: "gpt-5-mini", mode: "chat" },
{ model_group: "gpt-5", mode: "chat" },
{ model_group: "o3", mode: "chat" },
];
...new Set(getAllPresets().flatMap((preset) => [...getRequiredModelsInPreset(preset)])),
].map((model_group) => ({ model_group, mode: "chat" }));

const ANTHROPIC_ONLY_MODEL = ANTHROPIC_TIERS.COMPLEX[0];

const openTemplateDropdown = (): void => {
fireEvent.mouseDown(screen.getByTestId("template-selector").querySelector(".ant-select-selector")!);
Expand Down Expand Up @@ -428,13 +430,15 @@ describe("AddAutoRouterTab", () => {
});

it("disables a preset missing one of its models, naming the missing model", async () => {
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS.filter((m) => m.model_group !== "claude-opus-5"));
mockFetchAvailableModels.mockResolvedValue(
ALL_FAMILY_MODELS.filter((m) => m.model_group !== ANTHROPIC_ONLY_MODEL),
);

renderWithProviders(<Harness />);
openTemplateDropdown();

await waitFor(() => {
expect(optionByLabel("Anthropic Family")!.textContent).toContain("Missing: claude-opus-5");
expect(optionByLabel("Anthropic Family")!.textContent).toContain(`Missing: ${ANTHROPIC_ONLY_MODEL}`);
});
expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(true);
});
Expand All @@ -458,7 +462,8 @@ describe("AddAutoRouterTab", () => {
expect(screen.queryByText("Advanced: Keyword/Semantic Matching")).not.toBeInTheDocument();
expect(
screen.getByText(
"Simple: claude-haiku-4-5 · Medium: claude-sonnet-4-5 · Complex: claude-opus-5 · Reasoning: claude-opus-5",
`Simple: ${ANTHROPIC_TIERS.SIMPLE.join(", ")} · Medium: ${ANTHROPIC_TIERS.MEDIUM.join(", ")} · ` +
`Complex: ${ANTHROPIC_TIERS.COMPLEX.join(", ")} · Reasoning: ${ANTHROPIC_TIERS.REASONING.join(", ")}`,
),
).toBeInTheDocument();
});
Expand Down Expand Up @@ -500,15 +505,8 @@ describe("AddAutoRouterTab", () => {

await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0]).toMatchObject({
auto_router_default_model: "claude-sonnet-4-5",
complexity_router_config: {
tiers: {
SIMPLE: ["claude-haiku-4-5"],
MEDIUM: ["claude-sonnet-4-5"],
COMPLEX: ["claude-opus-5"],
REASONING: ["claude-opus-5"],
},
},
auto_router_default_model: ANTHROPIC_TIERS.MEDIUM[0],
complexity_router_config: { tiers: ANTHROPIC_TIERS },
});
});

Expand Down
18 changes: 11 additions & 7 deletions ui/litellm-dashboard/src/lib/autorouter_presets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,24 @@ describe("autorouter_presets", () => {
it("reports only the models the caller is missing, and none when the family is fully available", () => {
const preset = getPresetByKey("openai_family")!;
const required = [...getRequiredModelsInPreset(preset)];
const [held] = required;

expect(getMissingModelsInPreset(preset, new Set(["gpt-5-nano"]))).toEqual(
required.filter((m) => m !== "gpt-5-nano").sort(),
);
expect(getMissingModelsInPreset(preset, new Set([held]))).toEqual(required.filter((m) => m !== held).sort());
expect(getMissingModelsInPreset(preset, new Set(required))).toEqual([]);
});

// Admins spell version numbers with either "-" or "." (claude-sonnet-4-5 vs claude-sonnet-4.5);
// a caller who only registered one form still satisfies a preset that names the other.
// a caller who only registered one form still satisfies a preset that names the other. The
// caller's spellings are derived from the preset itself so that renaming a preset's models in
// autorouter_presets.json can't quietly turn this into a no-op (the inequality below fails
// instead, if no preset model carries a version number at all).
it("treats a preset's model as available under either version-separator spelling", () => {
const preset = getPresetByKey("anthropic_family")!;
expect(
getMissingModelsInPreset(preset, new Set(["claude-haiku-4.5", "claude-sonnet-4.5", "claude-opus-5"])),
).toEqual([]);
const required = [...getRequiredModelsInPreset(preset)];
const dottedSpellings = required.map((model) => model.replace(/(\d)-(\d)/g, "$1.$2"));

expect(dottedSpellings).not.toEqual(required);
expect(getMissingModelsInPreset(preset, new Set(dottedSpellings))).toEqual([]);
});

// The two-arm mirror: a differently-punctuated preset model must not be reported missing.
Expand Down
Loading