feat(routing): cost-tiered complexity routing + saved presets (#656)

The 08-31 lever: model_assignments gains one compound rung —
AGENT_SLUG > ROLE('{role}:{complexity}') > ROLE > GLOBAL — so a
low-complexity task can route to a cheaper tier while coordinators stay
pinned. Structurally opt-in: zero rows means byte-identical routing
(pinned by a named test across every precedence case), the cost_tiered
apply-mode (seeds developer:low→haiku) is reachable only from the
explicit PM-gated endpoint — verified no startup path can apply it.
Overrides are downgrade-only (input-price comparator), allowlisted to
{developer, qa, documenter} — cell_pm excluded per the org's own
coordinator definition and its documented weak-model incidents — and
validated at write time (disabled/unconfigured provider rejected with
remediation; cross-provider-family overrides warn explicitly).

Per adversarial review: the four mode-switch applies now spare compound
rows exactly like agent pins (the 2026-07-17 unscoped-wipe class, new
victim, same fix extended via one shared wipe helper) with panel cache
invalidation + truthful confirm dialogs; preset apply validates the
entire payload BEFORE the wipe (validate-all-first), with a savepoint
crash test proving rollback.

Presets (CEO request): routing_presets table (migration 082) snapshots
the full mix — mode, per-agent overrides, complexity rows — with
save/apply/delete endpoints and a panel preset bar; applying skips
since-removed models with per-entry notes, never silently.

Task complexity threads task_id through _resolve_agent_route at both
call sites; taskless spawns unchanged. 235 backend + 23 panel tests.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-23 03:04:18 +02:00
committed by GitHub
co-authored by Renn F
parent 10f039c36f
commit 165892dc62
17 changed files with 2812 additions and 70 deletions
@@ -1,7 +1,18 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import {
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import React from "react";
import type {
ComplexityOverride,
RoutingPreset,
} from "@/lib/api/providers";
const {
catalog,
@@ -15,6 +26,13 @@ const {
saveSelfHostedConfig,
testSelfHosted,
getSelfHostedModels,
getComplexityOverrides,
setComplexityOverride,
deleteComplexityOverride,
listPresets,
savePreset,
applyPreset,
deletePreset,
} = vi.hoisted(() => ({
catalog: vi.fn(async () => [
{
@@ -53,6 +71,21 @@ const {
error: null,
})),
getSelfHostedModels: vi.fn(async () => []),
getComplexityOverrides: vi.fn(async (): Promise<ComplexityOverride[]> => []),
setComplexityOverride: vi.fn(async (payload: ComplexityOverride) => payload),
deleteComplexityOverride: vi.fn(async () => undefined),
listPresets: vi.fn(async (): Promise<RoutingPreset[]> => []),
savePreset: vi.fn(async (name: string) => ({
id: "preset-1",
name,
created_at: "2026-07-23T00:00:00Z",
})),
applyPreset: vi.fn(async () => ({
mode: "mix",
assignments: [],
skipped: [] as string[],
})),
deletePreset: vi.fn(async () => undefined),
}));
vi.mock("@/lib/api/providers", () => ({
@@ -68,10 +101,95 @@ vi.mock("@/lib/api/providers", () => ({
saveSelfHostedConfig,
testSelfHosted,
getSelfHostedModels,
getComplexityOverrides,
setComplexityOverride,
deleteComplexityOverride,
listPresets,
savePreset,
applyPreset,
deletePreset,
},
COMPLEXITY_OVERRIDE_ROLES: ["developer", "qa", "documenter"],
}));
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
vi.mock("sonner", () => ({
toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn() },
}));
// Functional Select mock (mirrors select-repo-picker.test.tsx /
// a2a-reply-composer.test.tsx): SelectItem renders as a clickable button
// wired to onValueChange via context, so a real "pick a value" interaction
// can be simulated without Radix's portal/pointer machinery. SelectTrigger
// keeps `role="combobox"` so the pre-existing per-agent-table combobox-count
// assertion is unaffected, and stamps `data-value` from context so a test can
// `waitFor` an async-loaded value (e.g. a complexity override) landing before
// interacting further.
vi.mock("@/components/ui/select", () => {
const Ctx = React.createContext<{
value?: string;
onValueChange: (v: string) => void;
}>({ onValueChange: () => {} });
return {
Select: ({
value,
onValueChange,
children,
}: {
value?: string;
onValueChange?: (v: string) => void;
children: React.ReactNode;
}) => (
<Ctx.Provider
value={{ value, onValueChange: onValueChange ?? (() => {}) }}
>
{children}
</Ctx.Provider>
),
SelectTrigger: ({ children }: { children: React.ReactNode }) => {
const { value } = React.useContext(Ctx);
return (
<button
type="button"
role="combobox"
aria-expanded={false}
aria-controls="mock-select-content"
data-value={value ?? ""}
>
{children}
</button>
);
},
SelectValue: () => null,
SelectGroup: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectLabel: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectContent: ({ children }: { children: React.ReactNode }) => (
<div>{children}</div>
),
SelectItem: ({
value,
children,
}: {
value: string;
children: React.ReactNode;
}) => {
const { onValueChange } = React.useContext(Ctx);
return (
<button
type="button"
role="option"
aria-selected={false}
onClick={() => onValueChange(value)}
>
{children}
</button>
);
},
};
});
// Full live roster (minus CEO/system) — mirrors foundation/identity.py so the
// per-agent override grid is exercised against the real 25-agent org chart,
@@ -277,6 +395,13 @@ describe("AIRoutingCard", () => {
saveSelfHostedConfig.mockClear();
testSelfHosted.mockClear();
getSelfHostedModels.mockClear();
getComplexityOverrides.mockClear();
setComplexityOverride.mockClear();
deleteComplexityOverride.mockClear();
listPresets.mockClear();
savePreset.mockClear();
applyPreset.mockClear();
deletePreset.mockClear();
useAgentDefinitions.mockReturnValue({
data: FULL_ROSTER,
isLoading: false,
@@ -468,4 +593,298 @@ describe("AIRoutingCard", () => {
);
expect(applyMode).not.toHaveBeenCalled();
});
// -------------------------------------------------------------------------
// Complexity overrides (cost-tiered routing)
// -------------------------------------------------------------------------
describe("Complexity overrides section", () => {
it("renders one row per allowlisted role with Low/High selects, and no coordinator role", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Complexity overrides");
const section = screen.getByText("Complexity overrides").closest("section")!;
for (const label of ["Developer", "QA", "Documenter"]) {
expect(within(section).getByText(label)).toBeInTheDocument();
}
// Coordinator (cell_pm, main_pm)/pr_reviewer/board/CEO-facing roles are
// never offered a row here — scoped to this section, since "Cell PM"/
// "Main PM"/"PR Reviewer" legitimately appear elsewhere (the per-agent
// mix table's roster).
expect(within(section).queryByText("Cell PM")).not.toBeInTheDocument();
expect(within(section).queryByText("Main PM")).not.toBeInTheDocument();
expect(within(section).queryByText("PR Reviewer")).not.toBeInTheDocument();
// 3 roles x 2 (low/high) selects.
expect(section.querySelectorAll('[role="combobox"]')).toHaveLength(6);
});
it("picking a model for a role+complexity calls setComplexityOverride with the right payload", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Complexity overrides");
const devLow = screen.getByTestId("complexity-select-developer-low");
fireEvent.click(
await within(devLow).findByRole("option", { name: "Claude Opus 4.6" }),
);
await waitFor(() =>
expect(setComplexityOverride).toHaveBeenCalledWith({
role: "developer",
complexity: "low",
model_name: "claude-opus-4-6",
}),
);
});
it("surfaces a returned cross-family warning as its own warning toast", async () => {
setComplexityOverride.mockResolvedValueOnce({
role: "developer",
complexity: "low",
model_name: "grok-build-0.1",
warning:
"'grok-build-0.1' routes through grok, a different provider family than developer's Anthropic baseline.",
});
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Complexity overrides");
const devLow = screen.getByTestId("complexity-select-developer-low");
fireEvent.click(
await within(devLow).findByRole("option", { name: "Grok Build 0.1" }),
);
await waitFor(() =>
expect(toast.warning).toHaveBeenCalledWith(
expect.stringContaining("different provider family"),
),
);
// Still allowed — the success toast still fires alongside the warning.
expect(toast.success).toHaveBeenCalledWith(
"developer:low → grok-build-0.1",
);
});
it("never shows a warning toast when the response carries none", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Complexity overrides");
const devLow = screen.getByTestId("complexity-select-developer-low");
fireEvent.click(
await within(devLow).findByRole("option", { name: "Claude Opus 4.6" }),
);
await waitFor(() => expect(setComplexityOverride).toHaveBeenCalled());
expect(toast.warning).not.toHaveBeenCalled();
});
it("picking '(none)' on a row with no existing override never calls deleteComplexityOverride", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Complexity overrides");
const qaHigh = screen.getByTestId("complexity-select-qa-high");
fireEvent.click(
await within(qaHigh).findByRole("option", { name: "(none)" }),
);
// No pre-existing row for qa:high (getComplexityOverrides returns []
// by default) — clearing an already-empty selection is a no-op.
await waitFor(() => expect(setComplexityOverride).not.toHaveBeenCalled());
expect(deleteComplexityOverride).not.toHaveBeenCalled();
});
it("picking '(none)' on a row WITH an existing override calls deleteComplexityOverride", async () => {
getComplexityOverrides.mockResolvedValueOnce([
{ role: "documenter", complexity: "low", model_name: "grok-build-0.1" },
]);
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Complexity overrides");
const documenterLow = await screen.findByTestId(
"complexity-select-documenter-low",
);
// Wait for the async-loaded override to actually land on the Select
// (its combobox reflects the current value via data-value) before
// clearing it — otherwise the click races the query resolving.
await waitFor(() =>
expect(
within(documenterLow).getByRole("combobox"),
).toHaveAttribute("data-value", "grok-build-0.1"),
);
fireEvent.click(
within(documenterLow).getByRole("option", { name: "(none)" }),
);
// deleteComplexityOverride(role, complexity) — positional, per the
// providersApi signature (mirrors the real hook's mutationFn).
await waitFor(() =>
expect(deleteComplexityOverride).toHaveBeenCalledWith(
"documenter",
"low",
),
);
});
});
// -------------------------------------------------------------------------
// Cost-Tiered mode button (additive seed, never wipes routing)
// -------------------------------------------------------------------------
describe("Cost-Tiered mode button", () => {
it("renders beside the other mode buttons and applies mode='cost_tiered' on confirm", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Grok (xAI) API key");
fireEvent.click(screen.getByText("Cost-Tiered"));
await waitFor(() =>
expect(applyMode).toHaveBeenCalledWith({ mode: "cost_tiered" }),
);
confirmSpy.mockRestore();
});
it("does nothing when the confirm dialog is declined", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Grok (xAI) API key");
fireEvent.click(screen.getByText("Cost-Tiered"));
await waitFor(() => expect(confirmSpy).toHaveBeenCalled());
expect(applyMode).not.toHaveBeenCalled();
confirmSpy.mockRestore();
});
});
// -------------------------------------------------------------------------
// Mode switches preserve complexity overrides (2026-07-17-style incident:
// these same buttons once wiped AGENT_SLUG pins) — the confirm text says so
// and the query cache is refreshed for both, not just the mode snapshot.
// -------------------------------------------------------------------------
describe("Mode switches preserve complexity overrides", () => {
it("applying Anthropic mode also refetches complexity overrides, not just the mode snapshot", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Grok (xAI) API key");
getComplexityOverrides.mockClear();
fireEvent.click(screen.getByText("Anthropic"));
await waitFor(() =>
expect(applyMode).toHaveBeenCalledWith({ mode: "anthropic" }),
);
await waitFor(() =>
expect(getComplexityOverrides.mock.calls.length).toBeGreaterThan(0),
);
confirmSpy.mockRestore();
});
it("the Anthropic confirm dialog states complexity overrides are kept", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Grok (xAI) API key");
fireEvent.click(screen.getByText("Anthropic"));
await waitFor(() => expect(confirmSpy).toHaveBeenCalled());
expect(confirmSpy.mock.calls[0][0]).toContain("complexity");
confirmSpy.mockRestore();
});
});
// -------------------------------------------------------------------------
// Routing presets (named, full snapshots)
// -------------------------------------------------------------------------
describe("Routing presets bar", () => {
it("lists saved presets and disables Apply/Delete until one is picked", async () => {
listPresets.mockResolvedValueOnce([
{ id: "p1", name: "Cheap Fleet", created_at: "2026-07-01T00:00:00Z" },
]);
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Cheap Fleet");
expect(screen.getByRole("button", { name: "Apply" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Delete" })).toBeDisabled();
});
it("save-as-preset flow: reveals a name input and Confirm calls savePreset", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Routing presets");
fireEvent.click(screen.getByText("Save as preset…"));
const nameInput = screen.getByPlaceholderText("Preset name");
fireEvent.change(nameInput, { target: { value: "My Setup" } });
fireEvent.click(screen.getByText("Confirm"));
await waitFor(() => expect(savePreset).toHaveBeenCalledWith("My Setup"));
await waitFor(() =>
expect(toast.success).toHaveBeenCalledWith('Saved preset "My Setup"'),
);
});
it("save-as-preset with an empty name shows an error and never calls savePreset", async () => {
render(withQueryClient(<AIRoutingCard />));
await screen.findByText("Routing presets");
fireEvent.click(screen.getByText("Save as preset…"));
fireEvent.click(screen.getByText("Confirm"));
await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith("Enter a preset name first"),
);
expect(savePreset).not.toHaveBeenCalled();
});
it("applying a preset with skipped rows surfaces them in a toast error", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
listPresets.mockResolvedValueOnce([
{ id: "p1", name: "Cheap Fleet", created_at: "2026-07-01T00:00:00Z" },
]);
applyPreset.mockResolvedValueOnce({
mode: "mix",
assignments: [],
skipped: ["Skipped role:developer (ghost-model) — Unknown model"],
});
render(withQueryClient(<AIRoutingCard />));
const presetSection = (await screen.findByText("Routing presets")).closest(
"section",
)!;
fireEvent.click(
await within(presetSection).findByRole("option", {
name: "Cheap Fleet",
}),
);
fireEvent.click(screen.getByRole("button", { name: "Apply" }));
await waitFor(() => expect(applyPreset).toHaveBeenCalledWith("p1"));
await waitFor(() =>
expect(toast.error).toHaveBeenCalledWith(
expect.stringContaining("1 row(s) skipped"),
),
);
confirmSpy.mockRestore();
});
it("deleting a preset calls deletePreset and clears the selection", async () => {
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
listPresets.mockResolvedValueOnce([
{ id: "p1", name: "Cheap Fleet", created_at: "2026-07-01T00:00:00Z" },
]);
render(withQueryClient(<AIRoutingCard />));
const presetSection = (await screen.findByText("Routing presets")).closest(
"section",
)!;
fireEvent.click(
await within(presetSection).findByRole("option", {
name: "Cheap Fleet",
}),
);
fireEvent.click(screen.getByRole("button", { name: "Delete" }));
await waitFor(() => expect(deletePreset).toHaveBeenCalledWith("p1"));
confirmSpy.mockRestore();
});
});
});
+345 -10
View File
@@ -3,10 +3,17 @@
import { useEffect, useMemo, useState, useCallback } from "react";
import {
useApplyMode,
useApplyPreset,
useCatalog,
useComplexityOverrides,
useDeleteComplexityOverride,
useDeletePreset,
useGrokKey,
useOllamaKey,
useRoutingMode,
useRoutingPresets,
useSavePreset,
useSetComplexityOverride,
useSetGrokKey,
useSetOllamaKey,
useSelfHostedModels,
@@ -34,6 +41,7 @@ import { Separator } from "@/components/ui/separator";
import {
AlertTriangle,
Cpu,
Gauge,
Key,
KeyRound,
Server,
@@ -43,7 +51,11 @@ import {
} from "lucide-react";
import { toast } from "sonner";
import { AssignmentScope, AgentRole, ModelProvider } from "@/types";
import type { SelfHostedModel } from "@/lib/api/providers";
import {
COMPLEXITY_OVERRIDE_ROLES,
type ComplexityLevel,
type SelfHostedModel,
} from "@/lib/api/providers";
import type { RoutingMode, SelfHostedTestResult } from "@/lib/api/providers";
import { SelfHostedSection } from "@/components/settings/self-hosted-section";
import { Badge } from "@/components/ui/badge";
@@ -126,11 +138,29 @@ function byOrgOrder(a: AgentDefinition, b: AgentDefinition): number {
return ra !== rb ? ra - rb : a.id.localeCompare(b.id);
}
// Display labels for the complexity-override allowlist — mirrors the
// backend's fixed `_COMPLEXITY_OVERRIDE_ROLES` (developer/qa/documenter).
// cell_pm is deliberately excluded (a coordinator role — see
// COMPLEXITY_OVERRIDE_ROLES), along with every other coordinator/board/
// CEO-facing role.
const COMPLEXITY_ROLE_LABELS: Record<string, string> = {
developer: "Developer",
qa: "QA",
documenter: "Documenter",
};
export function AIRoutingCard() {
const { data: catalog = [] } = useCatalog();
const { data: keyStatus } = useOllamaKey();
const { data: snapshot } = useRoutingMode();
const { data: selfHostedModels = [] } = useSelfHostedModels();
const { data: complexityOverrides = [] } = useComplexityOverrides();
const setComplexityOverride = useSetComplexityOverride();
const deleteComplexityOverride = useDeleteComplexityOverride();
const { data: presets = [] } = useRoutingPresets();
const savePreset = useSavePreset();
const applyPreset = useApplyPreset();
const deletePreset = useDeletePreset();
const {
data: agentDefs,
isLoading: agentsLoading,
@@ -255,14 +285,15 @@ export function AIRoutingCard() {
const flipToAnthropic = async () => {
if (
!confirm(
"Switch every agent to Anthropic? Per-agent pins are kept; role/global assignments are replaced.",
"Switch every agent to Anthropic? Per-agent pins and complexity " +
"overrides are kept; other role/global assignments are replaced.",
)
)
return;
try {
await applyMode.mutateAsync({ mode: "anthropic" });
toast.success(
"Role/global routing now on Anthropic — per-agent pins kept",
"Role/global routing now on Anthropic — per-agent pins and complexity overrides kept",
);
} catch (e) {
toast.error("Switch failed: " + errMsg(e));
@@ -276,13 +307,16 @@ export function AIRoutingCard() {
}
if (
!confirm(
"Switch every agent to Grok? Per-agent pins are kept; role/global assignments are replaced.",
"Switch every agent to Grok? Per-agent pins and complexity " +
"overrides are kept; other role/global assignments are replaced.",
)
)
return;
try {
await applyMode.mutateAsync({ mode: "grok" });
toast.success("Role/global routing now on Grok — per-agent pins kept");
toast.success(
"Role/global routing now on Grok — per-agent pins and complexity overrides kept",
);
} catch (e) {
toast.error("Switch failed: " + errMsg(e));
}
@@ -295,13 +329,16 @@ export function AIRoutingCard() {
}
if (
!confirm(
"Switch every agent to Ollama? Per-agent pins are kept; role/global assignments are replaced.",
"Switch every agent to Ollama? Per-agent pins and complexity " +
"overrides are kept; other role/global assignments are replaced.",
)
)
return;
try {
await applyMode.mutateAsync({ mode: "ollama" });
toast.success("Role/global routing now on Ollama — per-agent pins kept");
toast.success(
"Role/global routing now on Ollama — per-agent pins and complexity overrides kept",
);
} catch (e) {
toast.error("Switch failed: " + errMsg(e));
}
@@ -314,7 +351,9 @@ export function AIRoutingCard() {
}
if (
!confirm(
"Switch every agent to the self-hosted LLM? Per-agent pins are kept; role/global assignments are replaced.",
"Switch every agent to the self-hosted LLM? Per-agent pins and " +
"complexity overrides are kept; other role/global assignments " +
"are replaced.",
)
)
return;
@@ -324,7 +363,7 @@ export function AIRoutingCard() {
...(selfHostedModel ? { default_model: selfHostedModel } : {}),
});
toast.success(
"Role/global routing now on Self-Hosted LLM — per-agent pins kept",
"Role/global routing now on Self-Hosted LLM — per-agent pins and complexity overrides kept",
);
} catch (e) {
toast.error("Switch failed: " + errMsg(e));
@@ -382,6 +421,132 @@ export function AIRoutingCard() {
}
};
// --- Cost-tiered defaults (additive seed — never wipes existing routing) ---
const flipToCostTiered = async () => {
if (
!confirm(
"Seed the day-1 cost-tiered default (developer:low → Haiku)? " +
"Unlike the other buttons this is additive — it does not clear " +
"any existing routing.",
)
)
return;
try {
await applyMode.mutateAsync({ mode: "cost_tiered" });
toast.success(
"Cost-tiered default seeded (developer:low → Haiku)",
);
} catch (e) {
toast.error("Apply failed: " + errMsg(e));
}
};
// --- Complexity overrides (compound ROLE(":"complexity) rows) ---
const complexityMap = useMemo(() => {
const map: Record<string, Partial<Record<ComplexityLevel, string>>> = {};
for (const o of complexityOverrides) {
map[o.role] = { ...map[o.role], [o.complexity]: o.model_name };
}
return map;
}, [complexityOverrides]);
const handleComplexityChange = async (
role: string,
complexity: ComplexityLevel,
modelName: string,
) => {
try {
if (!modelName) {
if (complexityMap[role]?.[complexity]) {
await deleteComplexityOverride.mutateAsync({ role, complexity });
toast.success(`Cleared ${role}:${complexity} override`);
}
return;
}
const result = await setComplexityOverride.mutateAsync({
role,
complexity,
model_name: modelName,
});
toast.success(`${role}:${complexity}${modelName}`);
// Allowed but never silent: a cross-provider-family override (e.g. an
// Anthropic role pinned to a Grok/Ollama/self-hosted model) also gets
// its own warning toast.
if (result.warning) {
toast.warning(result.warning);
}
} catch (e) {
toast.error("Save failed: " + errMsg(e));
}
};
// --- Routing presets (named, full snapshots) ---
const [selectedPresetId, setSelectedPresetId] = useState("");
const [showPresetNameInput, setShowPresetNameInput] = useState(false);
const [presetNameDraft, setPresetNameDraft] = useState("");
const selectedPreset = presets.find((p) => p.id === selectedPresetId);
const handleSavePreset = async () => {
const name = presetNameDraft.trim();
if (!name) {
toast.error("Enter a preset name first");
return;
}
try {
const saved = await savePreset.mutateAsync(name);
toast.success(`Saved preset "${saved.name}"`);
setPresetNameDraft("");
setShowPresetNameInput(false);
setSelectedPresetId(saved.id);
} catch (e) {
toast.error("Save failed: " + errMsg(e));
}
};
const handleApplyPreset = async () => {
if (!selectedPresetId) {
toast.error("Pick a preset first");
return;
}
if (
!confirm(
`Apply preset "${selectedPreset?.name ?? selectedPresetId}"? This ` +
"replaces the ENTIRE current routing state (every per-agent pin, " +
"role row, and global default) with the saved snapshot.",
)
)
return;
try {
const result = await applyPreset.mutateAsync(selectedPresetId);
if (result.skipped.length > 0) {
toast.error(
`Applied with ${result.skipped.length} row(s) skipped: ` +
result.skipped.join("; "),
);
} else {
toast.success("Preset applied");
}
} catch (e) {
toast.error("Apply failed: " + errMsg(e));
}
};
const handleDeletePreset = async () => {
if (!selectedPresetId) {
toast.error("Pick a preset first");
return;
}
if (!confirm(`Delete preset "${selectedPreset?.name ?? selectedPresetId}"?`))
return;
try {
await deletePreset.mutateAsync(selectedPresetId);
toast.success("Preset deleted");
setSelectedPresetId("");
} catch (e) {
toast.error("Delete failed: " + errMsg(e));
}
};
return (
<Card>
<CardHeader>
@@ -534,7 +699,7 @@ export function AIRoutingCard() {
<HelpTip label="Anthropic / Grok / Ollama / Self-Hosted replace role/global routing with that provider; per-agent pins in the table below survive the switch. Mix keeps whatever's picked in the table.">
<Label className="text-sm font-medium">Routing mode</Label>
</HelpTip>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2">
<ModeButton
icon={<ShieldCheck className="h-4 w-4" />}
label="Anthropic"
@@ -591,6 +756,15 @@ export function AIRoutingCard() {
disabled={false}
highlight={currentMode === "mix"}
/>
<ModeButton
icon={<Gauge className="h-4 w-4" />}
label="Cost-Tiered"
description="Seed developer:low → Haiku (see below)."
active={false}
onClick={flipToCostTiered}
disabled={applyMode.isPending}
labelHint="Unlike every button to the left this never wipes existing routing — it's a one-time additive seed you can re-run anytime. Edit or remove individual rows in the Complexity overrides section below."
/>
</div>
{currentMode === "mix" && !hasOllamaKey ? (
<p className="text-xs text-amber-600 flex items-center gap-1">
@@ -649,6 +823,95 @@ export function AIRoutingCard() {
</>
)}
{/* -------- Preset bar (compact, sits right above the per-agent table) -------- */}
<Separator />
<section className="space-y-2">
<HelpTip label="A preset snapshots the ENTIRE current routing state — mode, every per-agent pin, every role row, and every complexity override — so you can switch between whole setups in one click instead of re-picking every Select.">
<Label className="text-sm font-medium">Routing presets</Label>
</HelpTip>
<div className="flex flex-wrap items-center gap-2 rounded-md border border-dashed p-2">
<Select value={selectedPresetId} onValueChange={setSelectedPresetId}>
<SelectTrigger size="sm" className="w-48 text-xs">
<SelectValue
placeholder={
presets.length ? "Choose a preset…" : "No presets saved"
}
/>
</SelectTrigger>
<SelectContent>
{presets.map((p) => (
<SelectItem key={p.id} value={p.id}>
{p.name}
</SelectItem>
))}
</SelectContent>
</Select>
<HelpTip label="Replaces the ENTIRE current routing state with this preset's snapshot — every per-agent pin, role row, and global default, not merged with what's here now.">
<Button
type="button"
size="sm"
variant="outline"
onClick={handleApplyPreset}
disabled={!selectedPresetId || applyPreset.isPending}
>
{applyPreset.isPending ? "Applying…" : "Apply"}
</Button>
</HelpTip>
<HelpTip label="Deletes the saved snapshot only — has no effect on the routing currently applied.">
<Button
type="button"
size="sm"
variant="outline"
onClick={handleDeletePreset}
disabled={!selectedPresetId || deletePreset.isPending}
>
Delete
</Button>
</HelpTip>
<Separator orientation="vertical" className="h-6" />
{showPresetNameInput ? (
<>
<Input
value={presetNameDraft}
onChange={(e) => setPresetNameDraft(e.target.value)}
placeholder="Preset name"
className="h-8 w-40 text-xs"
/>
<Button
type="button"
size="sm"
onClick={handleSavePreset}
disabled={savePreset.isPending}
>
{savePreset.isPending ? "Saving…" : "Confirm"}
</Button>
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => {
setShowPresetNameInput(false);
setPresetNameDraft("");
}}
>
Cancel
</Button>
</>
) : (
<HelpTip label="Snapshots exactly what this card currently shows — the applied mode, every per-agent pin, and every complexity override.">
<Button
type="button"
size="sm"
variant="outline"
onClick={() => setShowPresetNameInput(true)}
>
Save as preset
</Button>
</HelpTip>
)}
</div>
</section>
{/* -------- Mix-mode per-agent picker -------- */}
<Separator />
<section className="space-y-3">
@@ -843,6 +1106,78 @@ export function AIRoutingCard() {
</p>
) : null}
</section>
{/* -------- Complexity overrides (cost-tiered routing) -------- */}
<Separator />
<section className="space-y-3">
<HelpTip label="Downgrade-only by policy: a role+complexity override can never point to a costlier tier than that role's baseline model — this lever only saves cost, it never spends more. Coordinator roles (cell_pm, main_pm), pr_reviewer, and board/CEO-facing roles aren't offered a row here at all; tier pinning for those is deliberate — cell_pm especially, since a coordinator is the last place to gamble a downgrade.">
<Label className="text-sm font-medium">
Complexity overrides
</Label>
</HelpTip>
<p className="text-xs text-muted-foreground">
Pin a role to a cheaper model for LOW- or HIGH-complexity tasks
specifically wins over that role&apos;s plain default and the
global mode, still loses to a per-agent pin above. Leave a Select
blank to remove the override. Coordinator roles (cell_pm, main_pm)
aren&apos;t offered a row.
</p>
<div className="divide-y rounded-md border">
{COMPLEXITY_OVERRIDE_ROLES.map((role) => (
<div
key={role}
className="grid grid-cols-[1fr_140px_140px] items-center gap-4 p-3"
>
<HelpTip
label={`Applies only to ${COMPLEXITY_ROLE_LABELS[role]} agents; a per-agent pin above still wins over this.`}
>
<div className="text-xs font-medium">
{COMPLEXITY_ROLE_LABELS[role]}
</div>
</HelpTip>
{(["low", "high"] as const).map((complexity) => (
<div
key={complexity}
className="space-y-1"
data-testid={`complexity-select-${role}-${complexity}`}
>
<HelpTip
label={`Model used for ${COMPLEXITY_ROLE_LABELS[role]} tasks a PM estimates as ${complexity.toUpperCase()} complexity. Must be no costlier than ${COMPLEXITY_ROLE_LABELS[role]}'s baseline model — the server rejects an upgrade attempt here.`}
>
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">
{complexity}
</div>
</HelpTip>
<Select
value={complexityMap[role]?.[complexity] ?? "__clear__"}
onValueChange={(v: string) =>
handleComplexityChange(
role,
complexity,
v === "__clear__" ? "" : v,
)
}
>
<SelectTrigger size="sm" className="w-full text-xs">
<SelectValue placeholder="(none)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__clear__">(none)</SelectItem>
{catalogForMix.map(
(c: { model_name: string; display_name: string }) => (
<SelectItem key={c.model_name} value={c.model_name}>
{c.display_name}
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
))}
</div>
))}
</div>
</section>
</CardContent>
</Card>
);