diff --git a/ui/litellm-dashboard/src/autorouter_presets.json b/ui/litellm-dashboard/src/autorouter_presets.json index aff6f09da04..41107e88b8c 100644 --- a/ui/litellm-dashboard/src/autorouter_presets.json +++ b/ui/litellm-dashboard/src/autorouter_presets.json @@ -1,13 +1,16 @@ { "anthropic_family": { "label": "Anthropic Family", - "description": "Routes across the Claude model family: Haiku for simple queries, Sonnet for medium, Opus for complex and reasoning-heavy requests.", + "description": "Routes across the Claude model family: Haiku for simple queries, Sonnet for medium, Opus for complex, Opus at high thinking for reasoning.", "complexity_router_config": { "tiers": { "SIMPLE": ["claude-haiku-4-5"], "MEDIUM": ["claude-sonnet-5"], "COMPLEX": ["claude-opus-5"], - "REASONING": ["claude-fable-5"] + "REASONING": ["claude-opus-5"] + }, + "tier_model_configs": { + "REASONING": [{ "model_name": "claude-opus-5", "litellm_params": { "reasoning_effort": "high" } }] }, "classifier_type": "heuristic", "escalation_keywords": ["LITELLM ESCALATE"], diff --git a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx index c5924bdc959..6afdd1dbcfb 100644 --- a/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx +++ b/ui/litellm-dashboard/src/components/add_model/add_auto_router_tab.test.tsx @@ -592,6 +592,28 @@ describe("AddAutoRouterTab", () => { }); }); + // Every step between the bundled JSON and the payload drops these params silently. + it("carries a preset's per-tier reasoning effort through to the create payload", async () => { + const user = userEvent.setup(); + mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS); + + renderWithProviders(); + await waitForPresetEnabled("Anthropic Family"); + await selectTemplate("Anthropic Family"); + + await user.type(screen.getByPlaceholderText(/smart_router/i), "anthropic-router"); + await user.click(screen.getByRole("button", { name: /add auto router/i })); + + await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled()); + expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0]).toMatchObject({ + complexity_router_config: { + tier_model_configs: { + REASONING: [{ model_name: "claude-opus-5", litellm_params: { reasoning_effort: "high" } }], + }, + }, + }); + }); + // Bugbot-found bug: submitBlockedReason disables the button for this, but Form's onFinish // (wired to the same handler as the button) fires whenever the form itself is submitted, // independent of the button's own disabled state. Without submitRecommendedRouter re-checking diff --git a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts index a2d23473fb9..02f39c5344c 100644 --- a/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts +++ b/ui/litellm-dashboard/src/lib/autorouter_presets.test.ts @@ -99,6 +99,36 @@ describe("autorouter_presets", () => { ); }); + // Opus serves both tiers, so the effort is all that separates them and losing it fails silently. + it("pins the anthropic preset's reasoning tier to Opus at high thinking", () => { + const config = getPresetByKey("anthropic_family")!.complexity_router_config; + expect(config.tiers.COMPLEX).toEqual(["claude-opus-5"]); + expect(config.tiers.REASONING).toEqual(["claude-opus-5"]); + expect(config.tier_model_configs).toEqual({ + REASONING: [{ model_name: "claude-opus-5", litellm_params: { reasoning_effort: "high" } }], + }); + }); + + // serializeTierModelConfigs filters on the tier's models, so a stray name drops silently. + it("never names a model in tier_model_configs that its own tier does not hold", () => { + for (const preset of getAllPresets()) { + const { tiers, tier_model_configs: configs } = preset.complexity_router_config; + for (const [tier, entries] of Object.entries(configs ?? {})) { + for (const entry of entries) { + expect(tiers[tier as keyof typeof tiers] ?? [], `${preset.key}.${tier}`).toContain(entry.model_name); + } + } + } + }); + + it("prefills the anthropic preset's effort through to tier_model_params", () => { + const preset = getPresetByKey("anthropic_family")!; + const prefill = buildPresetPrefill(preset.complexity_router_config, groupsOnly(getRequiredModelsInPreset(preset))); + expect(prefill.complexityRouterConfig.tier_model_params).toEqual({ + REASONING: { "claude-opus-5": { reasoning_effort: "high" } }, + }); + }); + it("pins the gemini preset to concrete model ids, never Google's hot-swapping -latest aliases", () => { const gemini = getPresetByKey("gemini_family")!; const config = gemini.complexity_router_config;