mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
refactor(auto_router): drop the shunt preset, keep it as an Advanced-only toggle
Shunt is three litellm_params fields layered onto any complexity router, not its own routing strategy. Presenting it as a peer of anthropic_family/ openai_family/etc. in the template dropdown made it look mutually exclusive with a model-family choice when the two were never actually in conflict, and picking it meant swapping in shunt's own hardcoded tier list. Removes the "shunt" entry from autorouter_presets.json and the partial-fit preset machinery that existed only to support it (isPartialFitPreset, PARTIAL_FIT_PRESET_KEYS, hasNoUsableModelsAtAll, dropUnresolvedTierEntries, shuntStateFromPreset). The Advanced: Shunt section on the create form is unaffected and stays off by default: a caller composes it with whichever model-family preset (or Custom Configuration) they actually want.
This commit is contained in:
parent
0798a83d36
commit
6a5c9234c4
8 changed files with 155 additions and 270 deletions
|
|
@ -4,16 +4,33 @@
|
|||
"description": "Routes across models with 1M-token context windows: Luna for simple queries, Terra for medium, Sol for complex, Opus 5 at high thinking for reasoning.",
|
||||
"complexity_router_config": {
|
||||
"tiers": {
|
||||
"SIMPLE": ["gpt-5.6-luna"],
|
||||
"MEDIUM": ["gpt-5.6-terra"],
|
||||
"COMPLEX": ["gpt-5.6-sol"],
|
||||
"REASONING": ["claude-opus-5"]
|
||||
"SIMPLE": [
|
||||
"gpt-5.6-luna"
|
||||
],
|
||||
"MEDIUM": [
|
||||
"gpt-5.6-terra"
|
||||
],
|
||||
"COMPLEX": [
|
||||
"gpt-5.6-sol"
|
||||
],
|
||||
"REASONING": [
|
||||
"claude-opus-5"
|
||||
]
|
||||
},
|
||||
"tier_model_configs": {
|
||||
"REASONING": [{ "model_name": "claude-opus-5", "litellm_params": { "reasoning_effort": "high" } }]
|
||||
"REASONING": [
|
||||
{
|
||||
"model_name": "claude-opus-5",
|
||||
"litellm_params": {
|
||||
"reasoning_effort": "high"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"classifier_type": "heuristic_v2",
|
||||
"escalation_keywords": ["LITELLM ESCALATE"],
|
||||
"escalation_keywords": [
|
||||
"LITELLM ESCALATE"
|
||||
],
|
||||
"classification_mode": "every_request",
|
||||
"session_affinity": false,
|
||||
"modality_routing": false,
|
||||
|
|
@ -26,16 +43,33 @@
|
|||
"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-opus-5"]
|
||||
"SIMPLE": [
|
||||
"claude-haiku-4-5"
|
||||
],
|
||||
"MEDIUM": [
|
||||
"claude-sonnet-5"
|
||||
],
|
||||
"COMPLEX": [
|
||||
"claude-opus-5"
|
||||
],
|
||||
"REASONING": [
|
||||
"claude-opus-5"
|
||||
]
|
||||
},
|
||||
"tier_model_configs": {
|
||||
"REASONING": [{ "model_name": "claude-opus-5", "litellm_params": { "reasoning_effort": "high" } }]
|
||||
"REASONING": [
|
||||
{
|
||||
"model_name": "claude-opus-5",
|
||||
"litellm_params": {
|
||||
"reasoning_effort": "high"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"classifier_type": "heuristic",
|
||||
"escalation_keywords": ["LITELLM ESCALATE"],
|
||||
"escalation_keywords": [
|
||||
"LITELLM ESCALATE"
|
||||
],
|
||||
"classification_mode": "every_request",
|
||||
"session_affinity": false,
|
||||
"modality_routing": false,
|
||||
|
|
@ -48,13 +82,23 @@
|
|||
"description": "Routes across the Gemini model family: Flash Lite 2.5 for simple queries, Flash Lite 3.1 for medium, Flash 3.7 for complex, Pro 3.1 for reasoning-heavy requests.",
|
||||
"complexity_router_config": {
|
||||
"tiers": {
|
||||
"SIMPLE": ["gemini-2.5-flash-lite"],
|
||||
"MEDIUM": ["gemini-3.1-flash-lite"],
|
||||
"COMPLEX": ["gemini-3.7-flash"],
|
||||
"REASONING": ["gemini-3.1-pro-preview"]
|
||||
"SIMPLE": [
|
||||
"gemini-2.5-flash-lite"
|
||||
],
|
||||
"MEDIUM": [
|
||||
"gemini-3.1-flash-lite"
|
||||
],
|
||||
"COMPLEX": [
|
||||
"gemini-3.7-flash"
|
||||
],
|
||||
"REASONING": [
|
||||
"gemini-3.1-pro-preview"
|
||||
]
|
||||
},
|
||||
"classifier_type": "heuristic",
|
||||
"escalation_keywords": ["LITELLM ESCALATE"],
|
||||
"escalation_keywords": [
|
||||
"LITELLM ESCALATE"
|
||||
],
|
||||
"classification_mode": "every_request",
|
||||
"session_affinity": false,
|
||||
"modality_routing": false,
|
||||
|
|
@ -67,14 +111,36 @@
|
|||
"description": "Cost-optimized routing across providers: DeepSeek V4 Flash for simple queries, Muse Spark 1.2 at xhigh for medium, Kimi K3 at max for complex, Claude Opus 5 for reasoning. An LLM classifier with the agentic rubric assigns tiers.",
|
||||
"complexity_router_config": {
|
||||
"tiers": {
|
||||
"SIMPLE": ["deepseek-v4-flash"],
|
||||
"MEDIUM": ["muse-spark-1.2"],
|
||||
"COMPLEX": ["kimi-k3"],
|
||||
"REASONING": ["claude-opus-5"]
|
||||
"SIMPLE": [
|
||||
"deepseek-v4-flash"
|
||||
],
|
||||
"MEDIUM": [
|
||||
"muse-spark-1.2"
|
||||
],
|
||||
"COMPLEX": [
|
||||
"kimi-k3"
|
||||
],
|
||||
"REASONING": [
|
||||
"claude-opus-5"
|
||||
]
|
||||
},
|
||||
"tier_model_configs": {
|
||||
"MEDIUM": [{ "model_name": "muse-spark-1.2", "litellm_params": { "reasoning_effort": "xhigh" } }],
|
||||
"COMPLEX": [{ "model_name": "kimi-k3", "litellm_params": { "reasoning_effort": "max" } }]
|
||||
"MEDIUM": [
|
||||
{
|
||||
"model_name": "muse-spark-1.2",
|
||||
"litellm_params": {
|
||||
"reasoning_effort": "xhigh"
|
||||
}
|
||||
}
|
||||
],
|
||||
"COMPLEX": [
|
||||
{
|
||||
"model_name": "kimi-k3",
|
||||
"litellm_params": {
|
||||
"reasoning_effort": "max"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"classifier_type": "llm",
|
||||
"classifier_llm_config": {
|
||||
|
|
@ -83,7 +149,9 @@
|
|||
"classification_rubric": "agentic"
|
||||
},
|
||||
"classifier_context_window_size": 0,
|
||||
"escalation_keywords": ["LITELLM ESCALATE"],
|
||||
"escalation_keywords": [
|
||||
"LITELLM ESCALATE"
|
||||
],
|
||||
"classification_mode": "every_request",
|
||||
"session_affinity": false,
|
||||
"modality_routing": false,
|
||||
|
|
@ -96,41 +164,38 @@
|
|||
"description": "Routes across the GPT model family: Luna for simple queries, Terra for medium, Sol for complex, Sol at xhigh thinking for reasoning.",
|
||||
"complexity_router_config": {
|
||||
"tiers": {
|
||||
"SIMPLE": ["gpt-5.6-luna"],
|
||||
"MEDIUM": ["gpt-5.6-terra"],
|
||||
"COMPLEX": ["gpt-5.6-sol"],
|
||||
"REASONING": ["gpt-5.6-sol"]
|
||||
"SIMPLE": [
|
||||
"gpt-5.6-luna"
|
||||
],
|
||||
"MEDIUM": [
|
||||
"gpt-5.6-terra"
|
||||
],
|
||||
"COMPLEX": [
|
||||
"gpt-5.6-sol"
|
||||
],
|
||||
"REASONING": [
|
||||
"gpt-5.6-sol"
|
||||
]
|
||||
},
|
||||
"tier_model_configs": {
|
||||
"REASONING": [{ "model_name": "gpt-5.6-sol", "litellm_params": { "reasoning_effort": "xhigh" } }]
|
||||
"REASONING": [
|
||||
{
|
||||
"model_name": "gpt-5.6-sol",
|
||||
"litellm_params": {
|
||||
"reasoning_effort": "xhigh"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"classifier_type": "heuristic",
|
||||
"escalation_keywords": ["LITELLM ESCALATE"],
|
||||
"escalation_keywords": [
|
||||
"LITELLM ESCALATE"
|
||||
],
|
||||
"classification_mode": "every_request",
|
||||
"session_affinity": false,
|
||||
"modality_routing": false,
|
||||
"modality_pin_override": false,
|
||||
"deployment_affinity": true
|
||||
}
|
||||
},
|
||||
"shunt": {
|
||||
"label": "Shunt",
|
||||
"description": "Bounds large file/shell reads and delegates bulk-read and boilerplate-codegen tool calls to a cheap model, based on the shunt technique for AI coding agents (https://engineering.atspotify.com/2026/9/portal-by-spotify-cut-my-claude-code-token-usage-by-90). Ported server-side: works with any client, no plugin install. classifier_type is heuristic so this never counts against the licensed heuristic_v2 limit.",
|
||||
"complexity_router_config": {
|
||||
"tiers": {
|
||||
"SIMPLE": ["claude-haiku-4-5", "gpt-5.6-luna", "gemini-2.5-flash-lite", "deepseek-v4-flash"],
|
||||
"MEDIUM": ["claude-sonnet-5", "gpt-5.6-terra", "gemini-3.1-flash-lite"],
|
||||
"COMPLEX": ["claude-opus-5", "gpt-5.6-sol", "gemini-3.7-flash"],
|
||||
"REASONING": ["claude-opus-5", "gpt-5.6-sol", "gemini-3.1-pro-preview"]
|
||||
},
|
||||
"classifier_type": "heuristic",
|
||||
"escalation_keywords": ["LITELLM ESCALATE"],
|
||||
"classification_mode": "every_request",
|
||||
"session_affinity": false,
|
||||
"modality_routing": false,
|
||||
"modality_pin_override": false,
|
||||
"deployment_affinity": true
|
||||
},
|
||||
"auto_router_shunt_min_lines": 350
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -771,7 +771,6 @@ describe("AddAutoRouterTab", () => {
|
|||
"Gemini Family",
|
||||
"Lite",
|
||||
"OpenAI Family",
|
||||
"Shunt",
|
||||
"Custom Configuration",
|
||||
]);
|
||||
});
|
||||
|
|
@ -1028,68 +1027,11 @@ describe("AddAutoRouterTab", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Shunt is a partial-fit preset (see isPartialFitPreset in @/lib/autorouter_presets): unlike
|
||||
// the family presets above, it stays selectable even when none of its own named tier models
|
||||
// are available, as long as the caller has some chat model at all.
|
||||
describe("shunt preset", () => {
|
||||
it("stays selectable with only unrelated chat models, unlike an ordinary family preset", async () => {
|
||||
mockFetchAvailableModels.mockResolvedValue([{ model_group: "unknown-model-a", mode: "chat" }]);
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
openTemplateDropdown();
|
||||
|
||||
await waitFor(() => expect(optionByLabel("Anthropic Family")).toHaveTextContent("Missing:"));
|
||||
await waitFor(() => expect(isOptionDisabled(optionByLabel("Shunt")!)).toBe(false));
|
||||
});
|
||||
|
||||
it("greys out only when the caller has no chat model at all", async () => {
|
||||
mockFetchAvailableModels.mockResolvedValue([]);
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
openTemplateDropdown();
|
||||
|
||||
await waitFor(() => expect(isOptionDisabled(optionByLabel("Shunt")!)).toBe(true));
|
||||
});
|
||||
|
||||
it("expands detailed configuration with empty tiers when none of its own models resolve", async () => {
|
||||
const user = userEvent.setup();
|
||||
const actual = await vi.importActual<typeof import("./build_complexity_router_config")>(
|
||||
"./build_complexity_router_config",
|
||||
);
|
||||
vi.mocked(getMissingTiersError).mockImplementation(actual.getMissingTiersError);
|
||||
mockFetchAvailableModels.mockResolvedValue([{ model_group: "unknown-model-a", mode: "chat" }]);
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
await waitForPresetEnabled("Shunt");
|
||||
await selectTemplate("Shunt");
|
||||
|
||||
// Detailed Configuration auto-expands (none of Shunt's own tier models resolved, the same
|
||||
// "needs your input" signal a deployment-matched family preset also expands for), and the
|
||||
// submit stays blocked until a caller fills in a model for every tier - the same gate
|
||||
// Custom Configuration is held to, since an unfilled tier is unfilled either way.
|
||||
expect(screen.getByText("Complexity Tier Configuration")).toBeInTheDocument();
|
||||
await user.type(screen.getByPlaceholderText(/smart_router/i), "shunt-empty-tiers");
|
||||
expect(screen.getByRole("button", { name: /add auto router/i })).toBeDisabled();
|
||||
});
|
||||
|
||||
it("carries the bounded-read threshold through to the create payload", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getMissingTiersError).mockReturnValue(null);
|
||||
mockFetchAvailableModels.mockResolvedValue([{ model_group: "some-chat-model", mode: "chat" }]);
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
await waitForPresetEnabled("Shunt");
|
||||
await selectTemplate("Shunt");
|
||||
|
||||
await user.type(screen.getByPlaceholderText(/smart_router/i), "shunt-router");
|
||||
await user.click(screen.getByRole("button", { name: /add auto router/i }));
|
||||
|
||||
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
|
||||
const submitted = vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0];
|
||||
expect(submitted?.auto_router_shunt_min_lines).toBe(350);
|
||||
});
|
||||
|
||||
it("omits shunt fields entirely for a router built from Custom Configuration", async () => {
|
||||
// Shunt is not a preset: it's an Advanced toggle available on any router, off by default, so
|
||||
// these exercise the toggle directly against an ordinary family preset rather than a preset
|
||||
// of its own.
|
||||
describe("shunt (Advanced)", () => {
|
||||
it("omits shunt fields entirely until a caller opens the section and sets a threshold", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getMissingTiersError).mockReturnValue(null);
|
||||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
|
|
@ -1106,17 +1048,45 @@ describe("AddAutoRouterTab", () => {
|
|||
expect(submitted?.auto_router_shunt_min_lines).toBeUndefined();
|
||||
});
|
||||
|
||||
it("carries the bounded-read threshold through to the create payload once set", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getMissingTiersError).mockReturnValue(null);
|
||||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
await waitForPresetEnabled("Anthropic Family");
|
||||
await selectTemplate("Anthropic Family");
|
||||
|
||||
await user.type(screen.getByPlaceholderText(/smart_router/i), "shunt-router");
|
||||
expandDetailedConfiguration();
|
||||
await user.click(screen.getByText("Advanced: Shunt"));
|
||||
const threshold = screen.getByRole("spinbutton", { name: "Bounded-read threshold" });
|
||||
fireEvent.change(threshold, { target: { value: "350" } });
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /add auto router/i }));
|
||||
|
||||
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
|
||||
const submitted = vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0];
|
||||
expect(submitted?.auto_router_shunt_min_lines).toBe(350);
|
||||
});
|
||||
|
||||
it("carries a chosen worker model through to the create payload", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(getMissingTiersError).mockReturnValue(null);
|
||||
mockFetchAvailableModels.mockResolvedValue([{ model_group: "some-chat-model", mode: "chat" }]);
|
||||
mockFetchAvailableModels.mockResolvedValue([
|
||||
...ALL_FAMILY_MODELS,
|
||||
{ model_group: "some-chat-model", mode: "chat" },
|
||||
]);
|
||||
|
||||
renderWithProviders(<Harness />);
|
||||
await waitForPresetEnabled("Shunt");
|
||||
await selectTemplate("Shunt");
|
||||
await waitForPresetEnabled("Anthropic Family");
|
||||
await selectTemplate("Anthropic Family");
|
||||
|
||||
await user.type(screen.getByPlaceholderText(/smart_router/i), "shunt-with-worker-model");
|
||||
expandDetailedConfiguration();
|
||||
await user.click(screen.getByText("Advanced: Shunt"));
|
||||
const threshold = screen.getByRole("spinbutton", { name: "Bounded-read threshold" });
|
||||
fireEvent.change(threshold, { target: { value: "350" } });
|
||||
await chooseSelectOption(user, screen.getByRole("combobox", { name: "Bulk-read model" }), "some-chat-model");
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /add auto router/i }));
|
||||
|
|
@ -1296,12 +1266,8 @@ describe("AddAutoRouterTab", () => {
|
|||
expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(false);
|
||||
});
|
||||
const labels = visibleOptions().map((option) => option.querySelector(".font-medium")?.textContent);
|
||||
// Shunt is a partial-fit preset (see isPartialFitPreset): it stays available as long as SOME
|
||||
// chat model is registered, regardless of whether it's one of Shunt's own named models, so it
|
||||
// sorts alongside Anthropic Family here rather than with the family presets that stay disabled.
|
||||
expect(labels).toEqual([
|
||||
"Anthropic Family",
|
||||
"Shunt",
|
||||
"1M Context",
|
||||
"Gemini Family",
|
||||
"Lite",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import {
|
|||
type AutoRouterShuntState,
|
||||
buildAutoRouterShuntParams,
|
||||
DEFAULT_AUTO_ROUTER_SHUNT,
|
||||
shuntStateFromPreset,
|
||||
} from "./buildAutoRouterShunt";
|
||||
import { DEFAULT_MATCH_THRESHOLD } from "./SemanticKeywordMatching";
|
||||
import {
|
||||
|
|
@ -69,8 +68,6 @@ import {
|
|||
buildPresetPrefill,
|
||||
buildModelAvailability,
|
||||
deploymentRefsFromModelInfo,
|
||||
hasNoUsableModelsAtAll,
|
||||
isPartialFitPreset,
|
||||
ModelAvailability,
|
||||
PresetPrefill,
|
||||
AutoRouterPreset,
|
||||
|
|
@ -299,23 +296,10 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
// have been applied: while loading we withhold selection rather than let a caller pick a preset
|
||||
// whose models we cannot yet verify, and a failed fetch leaves every preset unverifiable. This
|
||||
// makes the load-race (pick during loading, then discover a missing model) unrepresentable.
|
||||
//
|
||||
// A partial-fit preset (isPartialFitPreset) is gated differently: its interception settings
|
||||
// apply regardless of tier models, so it greys out only when the caller has no usable chat
|
||||
// model at all, never merely because none of its own named models match the caller's fleet.
|
||||
// Selecting it then leaves whichever tiers didn't resolve empty for the caller to fill in
|
||||
// (see handlePresetChange), rather than blocking selection the way every other preset does.
|
||||
const presetAvailability = React.useCallback(
|
||||
(preset: AutoRouterPreset): PresetAvailability => {
|
||||
if (modelsLoading) return { kind: "loading" };
|
||||
if (modelsUnverifiable) return { kind: "unverifiable" };
|
||||
if (isPartialFitPreset(preset)) {
|
||||
if (hasNoUsableModelsAtAll(availability)) return { kind: "missing_models", models: ["any chat model"] };
|
||||
// Expand Detailed Configuration when at least one tier didn't resolve to any of the
|
||||
// preset's own models, the same "needs your input" signal viaDeployments already means
|
||||
// for a fully-resolving preset - here it can mean the caller has to fill a tier by hand.
|
||||
return { kind: "available", viaDeployments: getMissingModelsInPreset(preset, availability).length > 0 };
|
||||
}
|
||||
const missing = getMissingModelsInPreset(preset, availability);
|
||||
if (missing.length > 0) return { kind: "missing_models", models: missing };
|
||||
return {
|
||||
|
|
@ -380,12 +364,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
if (presetState.kind !== "available") return;
|
||||
|
||||
setSelectedPreset(presetKey);
|
||||
applyPrefill(
|
||||
buildPresetPrefill(preset.complexity_router_config, availability, {
|
||||
dropUnresolvedTierEntries: isPartialFitPreset(preset),
|
||||
}),
|
||||
shuntStateFromPreset(preset.auto_router_shunt_min_lines),
|
||||
);
|
||||
applyPrefill(buildPresetPrefill(preset.complexity_router_config, availability));
|
||||
setDetailsExpanded(presetState.viaDeployments);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
buildAutoRouterShuntParams,
|
||||
DEFAULT_AUTO_ROUTER_SHUNT,
|
||||
hydrateAutoRouterShunt,
|
||||
shuntStateFromPreset,
|
||||
} from "./buildAutoRouterShunt";
|
||||
import { buildAutoRouterShuntParams, DEFAULT_AUTO_ROUTER_SHUNT, hydrateAutoRouterShunt } from "./buildAutoRouterShunt";
|
||||
|
||||
describe("buildAutoRouterShuntParams", () => {
|
||||
it("omits all three keys when the threshold was never configured", () => {
|
||||
|
|
@ -72,13 +67,3 @@ describe("hydrateAutoRouterShunt", () => {
|
|||
expect(rebuilt).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shuntStateFromPreset", () => {
|
||||
it("stays untouched when the preset carries no threshold", () => {
|
||||
expect(shuntStateFromPreset(undefined)).toEqual(DEFAULT_AUTO_ROUTER_SHUNT);
|
||||
});
|
||||
|
||||
it("arms with the preset's threshold and no worker-model overrides", () => {
|
||||
expect(shuntStateFromPreset(350)).toEqual({ minLines: 350, bulkReadModel: undefined, codeWriteModel: undefined });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,10 +51,3 @@ export const hydrateAutoRouterShunt = (litellmParams: {
|
|||
codeWriteModel: litellmParams.auto_router_shunt_code_write_model ?? undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/** From a partial-fit preset's own top-level auto_router_shunt_min_lines (see
|
||||
* autorouter_presets.ts's AutoRouterPreset.auto_router_shunt_min_lines), or untouched. */
|
||||
export const shuntStateFromPreset = (presetMinLines: number | undefined): AutoRouterShuntState =>
|
||||
presetMinLines === undefined
|
||||
? DEFAULT_AUTO_ROUTER_SHUNT
|
||||
: { minLines: presetMinLines, bulkReadModel: undefined, codeWriteModel: undefined };
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ import {
|
|||
buildModelAvailability,
|
||||
deploymentRefsFromModelInfo,
|
||||
normalizeModelName,
|
||||
isPartialFitPreset,
|
||||
hasNoUsableModelsAtAll,
|
||||
} from "./autorouter_presets";
|
||||
import { DEFAULT_MATCH_THRESHOLD } from "@/components/add_model/SemanticKeywordMatching";
|
||||
import { DEFAULT_ESCALATION_KEYWORDS } from "@/components/add_model/EscalationKeywords";
|
||||
|
|
@ -35,7 +33,6 @@ describe("autorouter_presets", () => {
|
|||
"Gemini Family",
|
||||
"Lite",
|
||||
"OpenAI Family",
|
||||
"Shunt",
|
||||
]);
|
||||
// Every preset carries all four fields the UI relies on; a JSON typo dropping one fails here.
|
||||
for (const p of presets) {
|
||||
|
|
@ -825,72 +822,4 @@ describe("autorouter_presets", () => {
|
|||
expect(prefill.complexityRouterConfig.tier_model_params).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isPartialFitPreset", () => {
|
||||
it("marks the shunt preset as partial-fit", () => {
|
||||
expect(isPartialFitPreset(getPresetByKey("shunt")!)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not mark an ordinary family preset as partial-fit", () => {
|
||||
expect(isPartialFitPreset(getPresetByKey("anthropic_family")!)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasNoUsableModelsAtAll", () => {
|
||||
it("is true when the caller has registered no chat models", () => {
|
||||
expect(hasNoUsableModelsAtAll(groupsOnly([]))).toBe(true);
|
||||
});
|
||||
|
||||
it("is false as soon as any model group is registered, whether or not the preset names it", () => {
|
||||
expect(hasNoUsableModelsAtAll(groupsOnly(["some-unrelated-model"]))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildPresetPrefill with dropUnresolvedTierEntries", () => {
|
||||
const config = {
|
||||
tiers: {
|
||||
SIMPLE: ["model-a", "model-b"],
|
||||
MEDIUM: ["model-c"],
|
||||
COMPLEX: ["model-d"],
|
||||
REASONING: ["model-e"],
|
||||
},
|
||||
classifier_type: "heuristic" as const,
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
|
||||
it("drops a tier entry that fails to resolve instead of keeping the preset's literal string", () => {
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["model-a"]), { dropUnresolvedTierEntries: true });
|
||||
expect(prefill.complexityRouterConfig.tiers).toEqual({
|
||||
SIMPLE: ["model-a"],
|
||||
MEDIUM: [],
|
||||
COMPLEX: [],
|
||||
REASONING: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps every resolved entry in a tier that partially resolves", () => {
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["model-a", "model-b"]), {
|
||||
dropUnresolvedTierEntries: true,
|
||||
});
|
||||
expect(prefill.complexityRouterConfig.tiers.SIMPLE).toEqual(["model-a", "model-b"]);
|
||||
});
|
||||
|
||||
it("defaults to the literal-fallback behavior when the option is omitted", () => {
|
||||
const prefill = buildPresetPrefill(config, groupsOnly([]));
|
||||
expect(prefill.complexityRouterConfig.tiers.SIMPLE).toEqual(["model-a", "model-b"]);
|
||||
});
|
||||
|
||||
it("still rewrites a resolved entry to the caller's registered spelling", () => {
|
||||
const versioned = {
|
||||
...config,
|
||||
tiers: { ...config.tiers, SIMPLE: ["claude-sonnet-4.5"] },
|
||||
};
|
||||
const prefill = buildPresetPrefill(versioned, groupsOnly(["claude-sonnet-4-5"]), {
|
||||
dropUnresolvedTierEntries: true,
|
||||
});
|
||||
expect(prefill.complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-sonnet-4-5"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,22 +28,8 @@ export interface AutoRouterPreset {
|
|||
label: string;
|
||||
description: string;
|
||||
complexity_router_config: ComplexityRouterConfigPayload;
|
||||
// Present only on shunt-shaped presets: arms the shunt guardrail's bounded-read threshold.
|
||||
// Lives at this top level, not inside complexity_router_config, because it maps to litellm_params
|
||||
// fields (auto_router_shunt_*) rather than complexity_router_config settings.
|
||||
auto_router_shunt_min_lines?: number;
|
||||
}
|
||||
|
||||
// Presets whose tier models are suggestions rather than a hard requirement: shunt's value is the
|
||||
// I/O interception it arms regardless of which tier models resolve, so a preset carrying this key
|
||||
// should never grey out purely because none of ITS named tier models match the caller's fleet -
|
||||
// only when the caller has no usable chat model at all. Keyed by preset.key rather than a field on
|
||||
// the catalog JSON: this is a dashboard policy decision about how strictly to gate selection, not
|
||||
// something a preset publisher should be able to opt into by editing the catalog.
|
||||
const PARTIAL_FIT_PRESET_KEYS: ReadonlySet<string> = new Set(["shunt"]);
|
||||
|
||||
export const isPartialFitPreset = (preset: AutoRouterPreset): boolean => PARTIAL_FIT_PRESET_KEYS.has(preset.key);
|
||||
|
||||
export type AutoRouterPresetsResponse = Record<string, Omit<AutoRouterPreset, "key">>;
|
||||
|
||||
export const hydratePresets = (raw: AutoRouterPresetsResponse): AutoRouterPreset[] =>
|
||||
|
|
@ -188,11 +174,6 @@ export const getRequiredModelsInPreset = (preset: AutoRouterPreset): Set<string>
|
|||
export const getMissingModelsInPreset = (preset: AutoRouterPreset, availability: ModelAvailability): string[] =>
|
||||
getMissingModels(preset.complexity_router_config, availability);
|
||||
|
||||
// The gate a partial-fit preset (see isPartialFitPreset) is selectable under: the proxy has to
|
||||
// have registered SOME chat model, but not specifically one this preset names - unlike
|
||||
// getMissingModelsInPreset's all-or-nothing check, which is still what every other preset uses.
|
||||
export const hasNoUsableModelsAtAll = (availability: ModelAvailability): boolean => availability.modelGroups.size === 0;
|
||||
|
||||
// Checks the config actually being built (whether it arrived via a preset prefill or was typed by
|
||||
// hand - the two are indistinguishable once the caller has started editing), not a preset's
|
||||
// original bundled model list. Only counts classifier_llm_config/embedding_model as referenced
|
||||
|
|
@ -257,22 +238,9 @@ export const buildEmptyPrefill = (): PresetPrefill => ({
|
|||
export const buildPresetPrefill = (
|
||||
config: ComplexityRouterConfigPayload,
|
||||
availability: ModelAvailability,
|
||||
// Partial-fit presets (see isPartialFitPreset) drop a tier entry that fails to resolve
|
||||
// instead of keeping the preset's own unresolved string, so a caller who owns none of the
|
||||
// preset's named models for a tier gets an empty picker to fill in rather than a config that
|
||||
// silently names a model they don't have. Off by default: every other preset call site
|
||||
// already guarantees full resolution before calling this (presetAvailability), so keeping the
|
||||
// literal fallback there is unreachable, not a behavior change.
|
||||
{ dropUnresolvedTierEntries = false }: { dropUnresolvedTierEntries?: boolean } = {},
|
||||
): PresetPrefill => {
|
||||
const resolve = (model: string): string => resolveAvailableModel(model, availability) ?? model;
|
||||
const resolveTier = (models: string[]): string[] =>
|
||||
dropUnresolvedTierEntries
|
||||
? models.flatMap((model) => {
|
||||
const resolved = resolveAvailableModel(model, availability);
|
||||
return resolved ? [resolved] : [];
|
||||
})
|
||||
: models.map(resolve);
|
||||
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.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue