feat(ui): run the Anthropic Family preset's reasoning tier on Opus 5 at high thinking (#38490)

The preset put Fable 5 in REASONING, sitting above Opus in a Haiku to Sonnet to
Opus ladder even though Fable is the lighter model. Run Opus 5 there instead, at
high thinking, so the tier above COMPLEX is the same model thinking harder rather
than a different and lighter one.

This is the first bundled preset to carry tier_model_configs. The round trip was
already built and unit tested, but nothing between the bundled JSON and the
create payload asserted on it, so add that coverage here.
This commit is contained in:
tin-berri 2026-08-27 11:31:14 -07:00 • committed by GitHub
parent 490face7de
commit 0fba05800d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 57 additions and 2 deletions

View file

@ -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"],

View file

@ -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(<Harness />);
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

View file

@ -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;