mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-24 05:45:11 +00:00
Merge 59273de63f into 82ca10cc2d
This commit is contained in:
commit
1844042a66
5 changed files with 253 additions and 312 deletions
|
|
@ -74,7 +74,7 @@
|
|||
},
|
||||
"lite": {
|
||||
"label": "Lite",
|
||||
"description": "Cost-optimized routing across providers: DeepSeek V4 Flash for simple queries, Muse Spark 1.3 at xhigh for medium, Kimi K3 at max for complex, Claude Opus 5.5 for reasoning. An LLM classifier with the agentic rubric assigns tiers.",
|
||||
"description": "Cost-optimized routing across providers: DeepSeek V4 Flash for simple queries, Muse Spark 1.3 at xhigh for medium, Kimi K3 at max for complex, Claude Opus 5.5 for reasoning.",
|
||||
"complexity_router_config": {
|
||||
"tiers": {
|
||||
"SIMPLE": ["deepseek-v4-flash"],
|
||||
|
|
|
|||
|
|
@ -192,6 +192,71 @@ describe("AddAutoRouterTab", () => {
|
|||
vi.mocked(modelCostMap).mockResolvedValue({});
|
||||
});
|
||||
|
||||
describe.each(["Decisions Model", "LLM", "Heuristics"])("preserves %s selections", (family) => {
|
||||
it.each(BUNDLED_PRESETS)("when applying $label and saving", async (preset) => {
|
||||
mockFetchAvailableModels.mockResolvedValue([
|
||||
...ALL_FAMILY_MODELS,
|
||||
{ model_group: "my-decision", mode: "evaluation" },
|
||||
{ model_group: "my-judge", mode: "chat" },
|
||||
]);
|
||||
renderWithProviders(<Harness />);
|
||||
await waitForAutomaticTiers();
|
||||
await userEvent.click(screen.getByRole("radio", { name: family }));
|
||||
if (family === "Decisions Model") {
|
||||
await selectAutoRouterOption("Decision model", "my-decision");
|
||||
}
|
||||
if (family === "LLM") {
|
||||
await selectAutoRouterOption("Judge model", "my-judge");
|
||||
}
|
||||
await selectAutoRouterOption("How often to classify", "Every new user message");
|
||||
await waitForPresetEnabled(preset.label);
|
||||
await selectTemplate(preset.label);
|
||||
expect(screen.getByRole("radio", { name: family })).toBeChecked();
|
||||
expect(screen.getByRole("combobox", { name: "How often to classify" })).toHaveTextContent(
|
||||
"Every new user message",
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText("Auto Router Name"), { target: { value: "preserved-router" } });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Add Auto Router" })).toBeEnabled());
|
||||
await userEvent.click(screen.getByRole("button", { name: "Add Auto Router" }));
|
||||
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalledOnce());
|
||||
const saved = vi.mocked(handleAddAutoRouterSubmit).mock.calls[0][0].complexity_router_config;
|
||||
expect(saved.classification_mode).toBe("user_turn");
|
||||
expect(saved.tiers).toEqual(preset.complexity_router_config.tiers);
|
||||
if (family === "Decisions Model") {
|
||||
expect(saved.classifier_type).toBe("oss_classifier");
|
||||
expect(saved.opensource_classifier_config).toMatchObject({ deployment_name: "my-decision" });
|
||||
expect(saved.classifier_llm_config).toBeUndefined();
|
||||
} else if (family === "LLM") {
|
||||
expect(saved.classifier_type).toBe("llm");
|
||||
expect(saved.classifier_llm_config).toMatchObject({ model: "my-judge" });
|
||||
expect(saved.opensource_classifier_config).toBeUndefined();
|
||||
} else {
|
||||
expect(saved.classifier_type).toBe("heuristic");
|
||||
expect(saved.classifier_llm_config).toBeUndefined();
|
||||
expect(saved.opensource_classifier_config).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("blocks templates from replacing custom tiers and allows an explicit reset", async () => {
|
||||
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
|
||||
renderWithProviders(<Harness />);
|
||||
await waitForAutomaticTiers();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Edit tiers" }));
|
||||
await userEvent.click(screen.getByRole("button", { name: "Add tier" }));
|
||||
fireEvent.change(screen.getByLabelText("Name for tier 5"), { target: { value: "SPECIAL" } });
|
||||
openTemplateDropdown();
|
||||
for (const preset of BUNDLED_PRESETS) {
|
||||
expect(optionByLabel(preset.label)).toHaveAttribute("aria-disabled", "true");
|
||||
expect(optionByLabel(preset.label)).toHaveTextContent("Restore built-in tiers to use a template");
|
||||
}
|
||||
await userEvent.click(optionByLabel("OpenAI Family")!);
|
||||
expect(screen.getByLabelText("Name for tier 5")).toHaveValue("SPECIAL");
|
||||
await selectTemplate("Start from scratch");
|
||||
expect(screen.queryByLabelText("Name for tier 5")).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("radio", { name: "Heuristics" })).toBeChecked();
|
||||
});
|
||||
|
||||
it("shows Jev essentials before tiers and saves tuning from its own disclosure", async () => {
|
||||
mockFetchAvailableModels.mockResolvedValue([
|
||||
...ALL_FAMILY_MODELS,
|
||||
|
|
@ -2055,7 +2120,7 @@ describe("getSubmitBlockedReason", () => {
|
|||
describe("preset catalog fetch states", () => {
|
||||
afterEach(() => vi.mocked(useAutoRouterPresets).mockReturnValue(LOADED_PRESETS_QUERY));
|
||||
|
||||
it("preserves a JEV preset's per-turn bound in the create request", async () => {
|
||||
it("does not replace the chosen classifier with a catalog classifier", async () => {
|
||||
vi.clearAllMocks();
|
||||
testQueryClient.clear();
|
||||
vi.mocked(handleAddAutoRouterSubmit).mockReset();
|
||||
|
|
@ -2082,16 +2147,14 @@ describe("preset catalog fetch states", () => {
|
|||
renderWithProviders(<Harness />);
|
||||
await waitForPresetEnabled("Bounded JEV");
|
||||
await selectTemplate("Bounded JEV");
|
||||
await userEvent.click(screen.getByRole("combobox", { name: "Decision model" }));
|
||||
await userEvent.click(await screen.findByRole("option", { name: "my-decision" }));
|
||||
expect(screen.getByRole("radio", { name: "Heuristics" })).toBeChecked();
|
||||
fireEvent.change(screen.getByLabelText("Auto Router Name"), { target: { value: "bounded-router" } });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Add Auto Router" })).toBeEnabled());
|
||||
fireEvent.click(screen.getByRole("button", { name: "Add Auto Router" }));
|
||||
|
||||
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalledOnce());
|
||||
expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls[0][0].complexity_router_config).toMatchObject({
|
||||
classifier_type: "oss_classifier",
|
||||
classifier_context_per_turn_chars: 450,
|
||||
classifier_type: "heuristic",
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ import {
|
|||
getMissingModelsInPreset,
|
||||
getReferencedModelsError,
|
||||
buildEmptyPrefill,
|
||||
buildPresetPrefill,
|
||||
applyPresetTiers,
|
||||
buildModelAvailability,
|
||||
deploymentRefsFromModelInfo,
|
||||
ModelAvailability,
|
||||
|
|
@ -96,6 +96,7 @@ interface AddAutoRouterTabProps {
|
|||
}
|
||||
|
||||
type PresetAvailability =
|
||||
| { kind: "custom_tiers" }
|
||||
| { kind: "available"; viaDeployments: boolean }
|
||||
| { kind: "loading" }
|
||||
| { kind: "unverifiable" }
|
||||
|
|
@ -105,6 +106,8 @@ type PresetAvailability =
|
|||
// (see presetAvailability below), so an option a caller can click is always one that can be applied.
|
||||
const presetDisabledHint = (availability: PresetAvailability): string | null => {
|
||||
switch (availability.kind) {
|
||||
case "custom_tiers":
|
||||
return "Restore built-in tiers to use a template";
|
||||
case "available":
|
||||
return null;
|
||||
case "loading":
|
||||
|
|
@ -310,6 +313,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
// makes the load-race (pick during loading, then discover a missing model) unrepresentable.
|
||||
const presetAvailability = React.useCallback(
|
||||
(preset: AutoRouterPreset): PresetAvailability => {
|
||||
if (complexityRouterConfig.custom_tier_set) return { kind: "custom_tiers" };
|
||||
if (modelsLoading) return { kind: "loading" };
|
||||
if (modelsUnverifiable) return { kind: "unverifiable" };
|
||||
const missing = getMissingModelsInPreset(preset, availability);
|
||||
|
|
@ -319,7 +323,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
viaDeployments: getMissingModelsInPreset(preset, groupsOnlyAvailability).length > 0,
|
||||
};
|
||||
},
|
||||
[modelsLoading, modelsUnverifiable, availability, groupsOnlyAvailability],
|
||||
[modelsLoading, modelsUnverifiable, availability, groupsOnlyAvailability, complexityRouterConfig.custom_tier_set],
|
||||
);
|
||||
|
||||
const sortedPresetOptions = React.useMemo(
|
||||
|
|
@ -374,7 +378,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
|
||||
setPrefillAttempted(true);
|
||||
setSelectedPreset(presetKey);
|
||||
applyPrefill(buildPresetPrefill(preset.complexity_router_config, availability));
|
||||
setComplexityRouterConfig((current) => applyPresetTiers(current, preset.complexity_router_config, availability));
|
||||
};
|
||||
|
||||
const referencedModelsParams = {
|
||||
|
|
@ -569,7 +573,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
<div>
|
||||
<div className="font-medium">{preset.label}</div>
|
||||
<div className="text-xs text-muted-foreground">{preset.description}</div>
|
||||
<div className="text-xs text-muted-foreground">Replaces classifier and model choices</div>
|
||||
<div className="text-xs text-muted-foreground">Replaces tier models and their parameters</div>
|
||||
{disabledHint && <div className={`text-xs mt-1 ${hintClass}`}>{disabledHint}</div>}
|
||||
{matchedHint && <div className="text-xs mt-1 text-success">{matchedHint}</div>}
|
||||
</div>
|
||||
|
|
@ -587,7 +591,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
|
|||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">Replaces classifier and model choices</p>
|
||||
<p className="text-xs text-muted-foreground">Replaces tier models and their parameters</p>
|
||||
{presetsPending && <div className="text-xs mt-1 text-muted-foreground">Loading templates...</div>}
|
||||
{presetsUnavailable && (
|
||||
<div className="text-xs mt-1 text-destructive">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import type { ComplexityRouterConfigValue } from "@/components/add_model/ComplexityRouterConfig";
|
||||
import { BUNDLED_PRESETS_RESPONSE } from "../../tests/mocks/autoRouterPresets";
|
||||
import {
|
||||
hydratePresets,
|
||||
|
|
@ -9,7 +10,7 @@ import {
|
|||
getMissingModels,
|
||||
getReferencedModelsError,
|
||||
buildEmptyPrefill,
|
||||
buildPresetPrefill,
|
||||
applyPresetTiers,
|
||||
buildModelAvailability,
|
||||
deploymentRefsFromModelInfo,
|
||||
normalizeModelName,
|
||||
|
|
@ -43,7 +44,7 @@ describe("autorouter_presets", () => {
|
|||
}
|
||||
});
|
||||
|
||||
// buildPresetPrefill resolves every model reference through normalizeModelName, so two spellings
|
||||
// applyPresetTiers resolves every model reference through normalizeModelName, so two spellings
|
||||
// of the same model in one tier (e.g. "claude-sonnet-4-5" and "claude-sonnet-4.5") collapse to one
|
||||
// key. For tier_model_configs that silently drops one model's litellm_params; catch it in the
|
||||
// bundled data itself, since nothing else validates preset authoring.
|
||||
|
|
@ -94,7 +95,9 @@ describe("autorouter_presets", () => {
|
|||
expect(config.custom_dimensions).toBeUndefined();
|
||||
}
|
||||
const config = getPresetByKey("anthropic_family")!.complexity_router_config;
|
||||
expect(buildPresetPrefill(config, groupsOnly([])).complexityRouterConfig.custom_dimensions).toBeUndefined();
|
||||
expect(
|
||||
applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, groupsOnly([])).custom_dimensions,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("resets both scoring overrides when the form falls back to an empty prefill", () => {
|
||||
|
|
@ -102,28 +105,18 @@ describe("autorouter_presets", () => {
|
|||
expect(buildEmptyPrefill().complexityRouterConfig.dimension_weights).toBeUndefined();
|
||||
});
|
||||
|
||||
it("carries a preset's session affinity idle window into the prefilled form state", () => {
|
||||
const config = getPresetByKey("anthropic_family")!.complexity_router_config;
|
||||
const prefill = buildPresetPrefill({ ...config, session_affinity_ttl_seconds: 300 }, groupsOnly([]));
|
||||
expect(prefill.complexityRouterConfig.session_affinity_ttl_seconds).toBe(300);
|
||||
expect(
|
||||
buildPresetPrefill(config, groupsOnly([])).complexityRouterConfig.session_affinity_ttl_seconds,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("carries a preset's stored weights and custom dimensions into the prefill without rebalancing them", () => {
|
||||
const config = getPresetByKey("anthropic_family")!.complexity_router_config;
|
||||
const weights = { codePresence: 0.4 };
|
||||
const dimension = { name: "domain", weight: 0.9, keywords: ["orbitmesh"] };
|
||||
const prefill = buildPresetPrefill(
|
||||
{ ...config, dimension_weights: weights, custom_dimensions: [dimension] },
|
||||
groupsOnly([]),
|
||||
).complexityRouterConfig;
|
||||
expect(prefill.dimension_weights).toEqual(weights);
|
||||
expect(prefill.custom_dimensions).toEqual([{ ...dimension, id: "stored-0" }]);
|
||||
const plain = buildPresetPrefill(config, groupsOnly([])).complexityRouterConfig;
|
||||
expect(plain.dimension_weights).toBeUndefined();
|
||||
expect(plain.custom_dimensions).toBeUndefined();
|
||||
it("preserves session affinity and custom scoring settings", () => {
|
||||
const config = PRESETS[0].complexity_router_config;
|
||||
const current = {
|
||||
...buildEmptyPrefill().complexityRouterConfig,
|
||||
session_affinity_ttl_seconds: 300,
|
||||
dimension_weights: { codePresence: 0.4 },
|
||||
custom_dimensions: [{ id: "domain", name: "domain", weight: 0.9, keywords: ["orbitmesh"] }],
|
||||
};
|
||||
const selected = applyPresetTiers(current, config, groupsOnly([]));
|
||||
expect(selected.session_affinity_ttl_seconds).toBe(300);
|
||||
expect(selected.dimension_weights).toEqual(current.dimension_weights);
|
||||
expect(selected.custom_dimensions).toEqual(current.custom_dimensions);
|
||||
});
|
||||
|
||||
it("keeps the model-family presets on the heuristic classifier", () => {
|
||||
|
|
@ -180,28 +173,20 @@ describe("autorouter_presets", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("carries a preset's modality_routing into the prefilled form state", () => {
|
||||
const preset = getPresetByKey("anthropic_family")!;
|
||||
const withFlag = { ...preset.complexity_router_config, modality_routing: true };
|
||||
const prefill = buildPresetPrefill(withFlag, groupsOnly(getRequiredModelsInPreset(preset)));
|
||||
expect(prefill.complexityRouterConfig.modality_routing).toBe(true);
|
||||
const withoutFlag = buildPresetPrefill(
|
||||
it.each([false, true])("preserves the chosen modality settings %s", (enabled) => {
|
||||
const preset = PRESETS[0];
|
||||
const current = {
|
||||
...buildEmptyPrefill().complexityRouterConfig,
|
||||
modality_routing: enabled,
|
||||
modality_pin_override: enabled,
|
||||
};
|
||||
const selected = applyPresetTiers(
|
||||
current,
|
||||
preset.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(preset)),
|
||||
);
|
||||
expect(withoutFlag.complexityRouterConfig.modality_routing).toBe(false);
|
||||
});
|
||||
|
||||
it("carries a preset's modality_pin_override into the prefilled form state", () => {
|
||||
const preset = getPresetByKey("anthropic_family")!;
|
||||
const withFlag = { ...preset.complexity_router_config, modality_routing: true, modality_pin_override: true };
|
||||
const prefill = buildPresetPrefill(withFlag, groupsOnly(getRequiredModelsInPreset(preset)));
|
||||
expect(prefill.complexityRouterConfig.modality_pin_override).toBe(true);
|
||||
const withoutFlag = buildPresetPrefill(
|
||||
preset.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(preset)),
|
||||
);
|
||||
expect(withoutFlag.complexityRouterConfig.modality_pin_override).toBe(false);
|
||||
expect(selected.modality_routing).toBe(enabled);
|
||||
expect(selected.modality_pin_override).toBe(enabled);
|
||||
});
|
||||
|
||||
it("ships every bundled preset with both modality flags written out, since the payload type requires them", () => {
|
||||
|
|
@ -213,16 +198,24 @@ describe("autorouter_presets", () => {
|
|||
|
||||
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({
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
preset.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(preset)),
|
||||
);
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
REASONING: { "claude-fable-5-1": { reasoning_effort: "high" } },
|
||||
});
|
||||
});
|
||||
|
||||
it("prefills the lite preset's efforts through to tier_model_params", () => {
|
||||
const lite = getPresetByKey("lite")!;
|
||||
const prefill = buildPresetPrefill(lite.complexity_router_config, groupsOnly(getRequiredModelsInPreset(lite)));
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
lite.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(lite)),
|
||||
);
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
MEDIUM: { "muse-spark-1.3": { reasoning_effort: "xhigh" } },
|
||||
COMPLEX: { "kimi-k3": { reasoning_effort: "max" } },
|
||||
});
|
||||
|
|
@ -240,8 +233,12 @@ describe("autorouter_presets", () => {
|
|||
expect(preset.complexity_router_config.tier_model_configs).toEqual({
|
||||
REASONING: [{ model_name: "gpt-6-astra", litellm_params: { reasoning_effort: "xhigh" } }],
|
||||
});
|
||||
const prefill = buildPresetPrefill(preset.complexity_router_config, groupsOnly(getRequiredModelsInPreset(preset)));
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
preset.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(preset)),
|
||||
);
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
REASONING: { "gpt-6-astra": { reasoning_effort: "xhigh" } },
|
||||
});
|
||||
});
|
||||
|
|
@ -259,8 +256,12 @@ describe("autorouter_presets", () => {
|
|||
expect(preset.complexity_router_config.tier_model_configs).toEqual({
|
||||
REASONING: [{ model_name: "claude-opus-5-5", litellm_params: { reasoning_effort: "high" } }],
|
||||
});
|
||||
const prefill = buildPresetPrefill(preset.complexity_router_config, groupsOnly(getRequiredModelsInPreset(preset)));
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
preset.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(preset)),
|
||||
);
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
REASONING: { "claude-opus-5-5": { reasoning_effort: "high" } },
|
||||
});
|
||||
});
|
||||
|
|
@ -354,7 +355,9 @@ describe("autorouter_presets", () => {
|
|||
deployment_affinity: true,
|
||||
};
|
||||
expect(getMissingModels(config, availability)).toEqual([]);
|
||||
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual([group]);
|
||||
expect(applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, availability).tiers.SIMPLE).toEqual([
|
||||
group,
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
|
@ -431,11 +434,11 @@ describe("autorouter_presets", () => {
|
|||
[["a-cloud"], "a-cloud"],
|
||||
] as const) {
|
||||
const availability = buildModelAvailability(groups, deployments);
|
||||
const prefill = buildPresetPrefill(config, availability).complexityRouterConfig;
|
||||
const prefill = applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, availability);
|
||||
|
||||
expect(prefill.tiers.SIMPLE).toEqual([selected]);
|
||||
expect(prefill.tier_model_params).toEqual({ SIMPLE: { [selected]: { reasoning_effort: "high" } } });
|
||||
expect(prefill.classifier_llm_config).toEqual({ model: selected, timeout_ms: 3000 });
|
||||
expect(prefill.classifier_llm_config).toBeUndefined();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -536,7 +539,9 @@ describe("autorouter_presets", () => {
|
|||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-opus-5"]);
|
||||
expect(applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, availability).tiers.SIMPLE).toEqual([
|
||||
"claude-opus-5",
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(getAllPresets().map((preset) => [preset.key, preset] as const))(
|
||||
|
|
@ -552,8 +557,12 @@ describe("autorouter_presets", () => {
|
|||
})),
|
||||
);
|
||||
expect(getMissingModelsInPreset(preset, availability)).toEqual([]);
|
||||
const prefilled = buildPresetPrefill(preset.complexity_router_config, availability);
|
||||
const prefilledModels = Object.values(prefilled.complexityRouterConfig.tiers).flat();
|
||||
const prefilled = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
preset.complexity_router_config,
|
||||
availability,
|
||||
);
|
||||
const prefilledModels = Object.values(prefilled.tiers).flat();
|
||||
expect(prefilledModels.length).toBeGreaterThan(0);
|
||||
for (const model of prefilledModels) expect(groups).toContain(model);
|
||||
},
|
||||
|
|
@ -578,7 +587,7 @@ describe("autorouter_presets", () => {
|
|||
);
|
||||
const config = simpleTierConfig("claude-opus-5");
|
||||
expect(getMissingModels(config, availability)).toEqual([]);
|
||||
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual([
|
||||
expect(applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, availability).tiers.SIMPLE).toEqual([
|
||||
"anthropic/claude-opus-5",
|
||||
]);
|
||||
});
|
||||
|
|
@ -681,7 +690,7 @@ describe("autorouter_presets", () => {
|
|||
[wildcardDeployment("anthropic/*"), wildcardDeployment("bedrock/*")],
|
||||
);
|
||||
const config = simpleTierConfig("claude-opus-5");
|
||||
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual([
|
||||
expect(applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, availability).tiers.SIMPLE).toEqual([
|
||||
"anthropic/claude-opus-5",
|
||||
]);
|
||||
});
|
||||
|
|
@ -760,8 +769,12 @@ describe("autorouter_presets", () => {
|
|||
[wildcardDeployment("someprovider/*")],
|
||||
);
|
||||
expect(getMissingModelsInPreset(preset, availability)).toEqual([]);
|
||||
const prefilled = buildPresetPrefill(preset.complexity_router_config, availability);
|
||||
const prefilledModels = Object.values(prefilled.complexityRouterConfig.tiers).flat();
|
||||
const prefilled = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
preset.complexity_router_config,
|
||||
availability,
|
||||
);
|
||||
const prefilledModels = Object.values(prefilled.tiers).flat();
|
||||
expect(prefilledModels.length).toBeGreaterThan(0);
|
||||
for (const model of prefilledModels) expect(expandedGroups).toContain(model);
|
||||
},
|
||||
|
|
@ -876,165 +889,66 @@ describe("autorouter_presets", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("buildPresetPrefill", () => {
|
||||
it("preserves Laya settings and drops inactive classifier settings when prefilling", () => {
|
||||
const config = {
|
||||
tiers: { SIMPLE: ["fast"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "oss_classifier" as const,
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
modality_routing: false,
|
||||
modality_pin_override: false,
|
||||
opensource_classifier_config: {
|
||||
provider: "laya" as const,
|
||||
model: "english",
|
||||
timeout_ms: 4000,
|
||||
circuit_breaker_enabled: false,
|
||||
describe("applyPresetTiers", () => {
|
||||
const initial: ComplexityRouterConfigValue = {
|
||||
classifier_type: "jev",
|
||||
jev_classifier_config: { deployment_name: "decision", timeout_ms: 4200 },
|
||||
classifier_llm_config: { model: "judge", classification_rubric: "business" },
|
||||
classification_mode: "user_turn",
|
||||
session_affinity: false,
|
||||
classifier_context_window_size: 8,
|
||||
heuristic_v2_success_threshold: 0.7,
|
||||
tiers: { SIMPLE: ["old"], MEDIUM: [], COMPLEX: [], REASONING: [], NON_REASONING: ["relay"] },
|
||||
tier_model_params: { SIMPLE: { old: { temperature: 0.3 } }, NON_REASONING: { relay: { max_tokens: 10 } } },
|
||||
enable_non_reasoning_tier: true,
|
||||
tier_labels: { SIMPLE: "Quick" },
|
||||
default_model: "fallback",
|
||||
plan_mode_min_tier: "REASONING",
|
||||
adaptive: true,
|
||||
deployment_affinity: false,
|
||||
};
|
||||
|
||||
describe.each(["jev", "heuristic", "heuristic_v2", "llm"] as const)("%s classifier", (classifier_type) => {
|
||||
it.each(PRESETS)("keeps non-template settings when selecting $label repeatedly", (preset) => {
|
||||
const current = { ...initial, classifier_type };
|
||||
const availability = groupsOnly(PRESETS.flatMap((item) => [...getRequiredModelsInPreset(item)]));
|
||||
const selected = applyPresetTiers(current, preset.complexity_router_config, availability);
|
||||
const { tiers: _oldTiers, tier_model_params: _oldParams, ...settings } = current;
|
||||
const { tiers, tier_model_params, ...selectedSettings } = selected;
|
||||
expect(selectedSettings).toEqual(settings);
|
||||
expect(tiers).toEqual({ ...preset.complexity_router_config.tiers, NON_REASONING: ["relay"] });
|
||||
expect(tier_model_params?.NON_REASONING).toEqual(initial.tier_model_params?.NON_REASONING);
|
||||
expect(tier_model_params?.SIMPLE?.old).toBeUndefined();
|
||||
const nextPreset = PRESETS.find((item) => item.key !== preset.key)!;
|
||||
const next = applyPresetTiers(selected, nextPreset.complexity_router_config, availability);
|
||||
expect(next).toEqual(applyPresetTiers(current, nextPreset.complexity_router_config, availability));
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves a custom tier draft untouched", () => {
|
||||
const current = {
|
||||
...initial,
|
||||
custom_tier_set: {
|
||||
tiers: [{ id: "custom", name: "CUSTOM", definition: "custom work", models: ["old"] }],
|
||||
fallback_tier_id: "custom",
|
||||
},
|
||||
classifier_llm_config: { model: "stale-judge", timeout_ms: 6000 },
|
||||
classifier_context_window_size: 6,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["fast"]));
|
||||
const expectedJevConfig = {
|
||||
classifier_type: "jev",
|
||||
jev_classifier_config: config.opensource_classifier_config,
|
||||
classifier_context_window_size: 6,
|
||||
classifier_llm_config: undefined,
|
||||
};
|
||||
expect(prefill.complexityRouterConfig).toMatchObject(expectedJevConfig);
|
||||
const llmConfig = { ...config, classifier_type: "llm" as const };
|
||||
const llmPrefill = buildPresetPrefill(llmConfig, groupsOnly(["fast"]));
|
||||
expect(llmPrefill.complexityRouterConfig.jev_classifier_config).toBeUndefined();
|
||||
expect(llmPrefill.complexityRouterConfig.classifier_llm_config).toEqual(config.classifier_llm_config);
|
||||
expect(applyPresetTiers(current, PRESETS[0].complexity_router_config, groupsOnly([]))).toBe(current);
|
||||
});
|
||||
|
||||
it.each([undefined, 0, 0.95])("carries a preset's success threshold %s into the form", (threshold) => {
|
||||
const preset = getPresetByKey("anthropic_family")!;
|
||||
const config = {
|
||||
...preset.complexity_router_config,
|
||||
classifier_type: "heuristic_v2" as const,
|
||||
heuristic_v2_success_threshold: threshold,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(getRequiredModelsInPreset(preset)));
|
||||
expect(prefill.complexityRouterConfig.heuristic_v2_success_threshold).toBe(threshold);
|
||||
});
|
||||
|
||||
it("prefills a real bundled preset's tiers into the config", () => {
|
||||
const preset = getPresetByKey("anthropic_family")!;
|
||||
const prefill = buildPresetPrefill(
|
||||
preset.complexity_router_config,
|
||||
groupsOnly(getRequiredModelsInPreset(preset)),
|
||||
);
|
||||
expect(prefill.complexityRouterConfig.tiers).toEqual(preset.complexity_router_config.tiers);
|
||||
});
|
||||
|
||||
// `??`, not `||`: match_threshold: 0 and an empty escalation_keywords array are deliberate,
|
||||
// falsy preset values. A prefill that used `||` would silently replace both with the default,
|
||||
// which is exactly the kind of bug this test would have caught before either bundled preset
|
||||
// happened to avoid the case.
|
||||
it("keeps a preset's falsy match_threshold and escalation_keywords instead of defaulting them", () => {
|
||||
const config = {
|
||||
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "heuristic" as const,
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
match_threshold: 0,
|
||||
escalation_keywords: [],
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["gpt-5-nano"]));
|
||||
expect(prefill.matchThreshold).toBe(0);
|
||||
expect(prefill.escalationKeywords).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([undefined, false, true])("preserves a preset's context-window escalation setting: %s", (enabled) => {
|
||||
const prefill = buildPresetPrefill(
|
||||
{
|
||||
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "heuristic",
|
||||
classification_mode: "every_request",
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
enable_context_window_escalation: enabled,
|
||||
context_window_escalation_buffer: 0.9,
|
||||
it("checks only tier models when determining template availability", () => {
|
||||
const preset = {
|
||||
...PRESETS[0],
|
||||
complexity_router_config: {
|
||||
...PRESETS[0].complexity_router_config,
|
||||
classifier_llm_config: { model: "unavailable-judge" },
|
||||
embedding_model: "unavailable-embedding",
|
||||
default_model: "unavailable-default",
|
||||
},
|
||||
groupsOnly(["gpt-5-nano"]),
|
||||
);
|
||||
expect(prefill.complexityRouterConfig.enable_context_window_escalation).toBe(enabled);
|
||||
expect(prefill.complexityRouterConfig.context_window_escalation_buffer).toBe(0.9);
|
||||
});
|
||||
|
||||
it.each([undefined, false, true])(
|
||||
"preserves cache-routing settings and scalar tier models from a preset: %s",
|
||||
(enabled) => {
|
||||
const config = {
|
||||
...getPresetByKey("anthropic_family")!.complexity_router_config,
|
||||
tiers: { SIMPLE: "small", MEDIUM: "large", COMPLEX: "large", REASONING: "large" },
|
||||
cache_aware_routing: enabled,
|
||||
cache_aware_routing_output_tokens: 0,
|
||||
cache_aware_routing_timeout_ms: 750,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["small", "large"]));
|
||||
const expected = {
|
||||
tiers: { SIMPLE: ["small"], MEDIUM: ["large"], COMPLEX: ["large"], REASONING: ["large"] },
|
||||
cache_aware_routing: enabled,
|
||||
cache_aware_routing_output_tokens: 0,
|
||||
cache_aware_routing_timeout_ms: 750,
|
||||
};
|
||||
expect(prefill.complexityRouterConfig).toMatchObject(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("carries a preset's classification_mode and defaults it when the preset omits one", () => {
|
||||
const tiers = { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] };
|
||||
const base = {
|
||||
tiers,
|
||||
classifier_type: "heuristic" as const,
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const availability = groupsOnly(["gpt-5-nano"]);
|
||||
expect(
|
||||
buildPresetPrefill({ ...base, classification_mode: "user_turn" }, availability).complexityRouterConfig
|
||||
.classification_mode,
|
||||
).toBe("user_turn");
|
||||
expect(buildPresetPrefill(base, availability).complexityRouterConfig.classification_mode).toBe("every_request");
|
||||
});
|
||||
|
||||
it("falls back to the defaults when a preset omits match_threshold and escalation_keywords", () => {
|
||||
const prefill = buildPresetPrefill(
|
||||
{
|
||||
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "heuristic",
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
},
|
||||
groupsOnly(["gpt-5-nano"]),
|
||||
);
|
||||
expect(prefill.matchThreshold).toBe(DEFAULT_MATCH_THRESHOLD);
|
||||
expect(prefill.escalationKeywords).toEqual(DEFAULT_ESCALATION_KEYWORDS);
|
||||
});
|
||||
|
||||
// The whole point of the separator normalization: a caller whose proxy only registered the
|
||||
// dotted form of a version number still gets that model written into the tier, not the
|
||||
// preset's own hyphenated spelling (which the caller never actually registered).
|
||||
it("prefills a preset's tier_labels and leaves them undefined when the preset has none", () => {
|
||||
const base = {
|
||||
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "heuristic" as const,
|
||||
classification_mode: "every_request" as const,
|
||||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const labeled = buildPresetPrefill(
|
||||
{ ...base, tier_labels: { SIMPLE: "Cheap", REASONING: "Deep" } },
|
||||
groupsOnly(["gpt-5-nano"]),
|
||||
);
|
||||
expect(labeled.complexityRouterConfig.tier_labels).toEqual({ SIMPLE: "Cheap", REASONING: "Deep" });
|
||||
expect(buildPresetPrefill(base, groupsOnly(["gpt-5-nano"])).complexityRouterConfig.tier_labels).toBeUndefined();
|
||||
const tierModels = Object.values(preset.complexity_router_config.tiers).flat();
|
||||
expect(getRequiredModelsInPreset(preset)).toEqual(new Set(tierModels));
|
||||
expect(getMissingModelsInPreset(preset, groupsOnly(tierModels))).toEqual([]);
|
||||
});
|
||||
|
||||
it("rewrites a preset's model name to the caller's differently-punctuated registered spelling", () => {
|
||||
|
|
@ -1045,8 +959,12 @@ describe("autorouter_presets", () => {
|
|||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["claude-sonnet-4.5"]));
|
||||
expect(prefill.complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-sonnet-4.5"]);
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
config,
|
||||
groupsOnly(["claude-sonnet-4.5"]),
|
||||
);
|
||||
expect(prefill.tiers.SIMPLE).toEqual(["claude-sonnet-4.5"]);
|
||||
});
|
||||
|
||||
it("prefills the per-model litellm_params a preset carries in tier_model_configs", () => {
|
||||
|
|
@ -1060,8 +978,12 @@ describe("autorouter_presets", () => {
|
|||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["gpt-5-nano", "o3"]));
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
config,
|
||||
groupsOnly(["gpt-5-nano", "o3"]),
|
||||
);
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
REASONING: { o3: { reasoning_effort: "high" } },
|
||||
});
|
||||
});
|
||||
|
|
@ -1080,8 +1002,12 @@ describe("autorouter_presets", () => {
|
|||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["claude-sonnet-4.5"]));
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
config,
|
||||
groupsOnly(["claude-sonnet-4.5"]),
|
||||
);
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
REASONING: { "claude-sonnet-4.5": { reasoning_effort: "high" } },
|
||||
});
|
||||
});
|
||||
|
|
@ -1103,15 +1029,19 @@ describe("autorouter_presets", () => {
|
|||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["claude-sonnet-4.5"]));
|
||||
const prefill = applyPresetTiers(
|
||||
buildEmptyPrefill().complexityRouterConfig,
|
||||
config,
|
||||
groupsOnly(["claude-sonnet-4.5"]),
|
||||
);
|
||||
// temperature survives from the spelling that would otherwise have been overwritten;
|
||||
// reasoning_effort, set by both, resolves last-wins.
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toEqual({
|
||||
expect(prefill.tier_model_params).toEqual({
|
||||
REASONING: { "claude-sonnet-4.5": { reasoning_effort: "low", temperature: 0.2 } },
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves tier_model_params undefined for a preset that carries no per-model params", () => {
|
||||
it("clears old tier parameters for a preset that carries no per-model params", () => {
|
||||
const config = {
|
||||
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
|
||||
classifier_type: "heuristic" as const,
|
||||
|
|
@ -1119,8 +1049,8 @@ describe("autorouter_presets", () => {
|
|||
session_affinity: false,
|
||||
deployment_affinity: true,
|
||||
};
|
||||
const prefill = buildPresetPrefill(config, groupsOnly(["gpt-5-nano"]));
|
||||
expect(prefill.complexityRouterConfig.tier_model_params).toBeUndefined();
|
||||
const prefill = applyPresetTiers(buildEmptyPrefill().complexityRouterConfig, config, groupsOnly(["gpt-5-nano"]));
|
||||
expect(prefill.tier_model_params).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,26 +1,11 @@
|
|||
import { hydrateOssClassifier } from "@/components/add_model/jev_classifier_config";
|
||||
import {
|
||||
ComplexityRouterConfigPayload,
|
||||
hydrateTierLabels,
|
||||
} from "@/components/add_model/build_complexity_router_config";
|
||||
import { ComplexityRouterConfigPayload } from "@/components/add_model/build_complexity_router_config";
|
||||
import {
|
||||
ComplexityRouterConfigValue,
|
||||
ClassifierType,
|
||||
ClassifierLLMConfig,
|
||||
DEFAULT_CLASSIFICATION_MODE,
|
||||
DEFAULT_SESSION_AFFINITY,
|
||||
DEFAULT_DEPLOYMENT_AFFINITY,
|
||||
usesLlmClassifier,
|
||||
} from "@/components/add_model/ComplexityRouterConfig";
|
||||
import { KeywordTierRule } from "@/components/add_model/KeywordTierRules";
|
||||
import { hydrateKeywordTierRules } from "@/components/add_model/complexity_router_keywords";
|
||||
import { hydrateCustomDimensions } from "@/components/add_model/custom_dimensions";
|
||||
import {
|
||||
hydrateDimensionWeights,
|
||||
hydrateTierBoundaries,
|
||||
hydrateTokenThresholds,
|
||||
hydrateReasoningOverrideMinScore,
|
||||
} from "@/components/add_model/heuristic_scoring_knobs";
|
||||
import {
|
||||
TierModelParams,
|
||||
TierModelParamsByTier,
|
||||
|
|
@ -232,10 +217,10 @@ export const getMissingModels = (
|
|||
[...getRequiredModels(config)].filter((model) => resolveAvailableModel(model, availability) === undefined).sort();
|
||||
|
||||
export const getRequiredModelsInPreset = (preset: AutoRouterPreset): Set<string> =>
|
||||
getRequiredModels(preset.complexity_router_config);
|
||||
getRequiredModels({ tiers: preset.complexity_router_config.tiers });
|
||||
|
||||
export const getMissingModelsInPreset = (preset: AutoRouterPreset, availability: ModelAvailability): string[] =>
|
||||
getMissingModels(preset.complexity_router_config, availability);
|
||||
getMissingModels({ tiers: preset.complexity_router_config.tiers }, availability);
|
||||
|
||||
// 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
|
||||
|
|
@ -298,19 +283,12 @@ export const buildEmptyPrefill = (): PresetPrefill => ({
|
|||
escalationKeywords: DEFAULT_ESCALATION_KEYWORDS,
|
||||
});
|
||||
|
||||
// `??`, never `||`: a preset's match_threshold: 0 or escalation_keywords: [] is a deliberate,
|
||||
// falsy value that must survive the prefill, not get silently replaced by the default.
|
||||
//
|
||||
// `availability` is required, not optional: every model reference gets rewritten to the
|
||||
// caller's actual registered spelling (resolveAvailableModel), which may differ from the preset's
|
||||
// literal string by version-separator punctuation alone. Called only after presetAvailability has
|
||||
// already confirmed every required model resolves, so falling back to the preset's own string
|
||||
// when a model somehow doesn't resolve is unreachable in practice, not a silent-failure path.
|
||||
export const buildPresetPrefill = (
|
||||
export const applyPresetTiers = (
|
||||
current: ComplexityRouterConfigValue,
|
||||
config: ComplexityRouterConfigPayload,
|
||||
availability: ModelAvailability,
|
||||
): PresetPrefill => {
|
||||
const classifier = hydrateOssClassifier(config);
|
||||
): ComplexityRouterConfigValue => {
|
||||
if (current.custom_tier_set) return current;
|
||||
const resolve = (model: string): string => resolveAvailableModel(model, availability) ?? model;
|
||||
const resolveTier = (models: string | string[]): string[] => normalizeTierModels(models).map(resolve);
|
||||
// Params key on the model name the preset spells while every tier entry is rewritten to the
|
||||
|
|
@ -333,53 +311,19 @@ export const buildPresetPrefill = (
|
|||
]),
|
||||
);
|
||||
|
||||
const params = resolveParamKeys(hydrateTierModelParams(config.tiers, config.tier_model_configs));
|
||||
return {
|
||||
complexityRouterConfig: {
|
||||
tiers: {
|
||||
SIMPLE: resolveTier(config.tiers.SIMPLE),
|
||||
MEDIUM: resolveTier(config.tiers.MEDIUM),
|
||||
COMPLEX: resolveTier(config.tiers.COMPLEX),
|
||||
REASONING: resolveTier(config.tiers.REASONING),
|
||||
},
|
||||
tier_model_params: resolveParamKeys(hydrateTierModelParams(config.tiers, config.tier_model_configs)),
|
||||
tier_labels: hydrateTierLabels(config.tier_labels),
|
||||
...classifier,
|
||||
heuristic_v2_success_threshold: config.heuristic_v2_success_threshold,
|
||||
classifier_llm_config:
|
||||
classifier.classifier_type !== "jev" && config.classifier_llm_config
|
||||
? { ...config.classifier_llm_config, model: resolve(config.classifier_llm_config.model) }
|
||||
: undefined,
|
||||
classifier_context_window_size: config.classifier_context_window_size,
|
||||
classifier_context_budget_chars: config.classifier_context_budget_chars,
|
||||
classifier_context_per_turn_chars: config.classifier_context_per_turn_chars,
|
||||
classifier_context_include_assistant_turns: config.classifier_context_include_assistant_turns,
|
||||
classification_mode: config.classification_mode ?? DEFAULT_CLASSIFICATION_MODE,
|
||||
session_affinity: config.session_affinity ?? DEFAULT_SESSION_AFFINITY,
|
||||
session_affinity_ttl_seconds: config.session_affinity_ttl_seconds,
|
||||
deployment_affinity: config.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
|
||||
modality_routing: config.modality_routing ?? false,
|
||||
modality_pin_override: config.modality_pin_override ?? false,
|
||||
adaptive: config.adaptive,
|
||||
adaptive_weights: config.adaptive_weights,
|
||||
tier_distance_penalty: config.tier_distance_penalty,
|
||||
adaptive_eligible: config.adaptive_eligible,
|
||||
return_raw_model_name: config.return_raw_model_name,
|
||||
dimension_weights: hydrateDimensionWeights(config.dimension_weights),
|
||||
custom_dimensions: hydrateCustomDimensions(config.custom_dimensions),
|
||||
tier_boundaries: hydrateTierBoundaries(config.tier_boundaries),
|
||||
token_thresholds: hydrateTokenThresholds(config.token_thresholds),
|
||||
reasoning_override_min_score: hydrateReasoningOverrideMinScore(config.reasoning_override_min_score),
|
||||
enable_context_window_escalation: config.enable_context_window_escalation,
|
||||
context_window_escalation_buffer: config.context_window_escalation_buffer,
|
||||
cache_aware_routing: config.cache_aware_routing,
|
||||
cache_aware_routing_output_tokens: config.cache_aware_routing_output_tokens,
|
||||
cache_aware_routing_timeout_ms: config.cache_aware_routing_timeout_ms,
|
||||
...current,
|
||||
tiers: {
|
||||
...current.tiers,
|
||||
SIMPLE: resolveTier(config.tiers.SIMPLE),
|
||||
MEDIUM: resolveTier(config.tiers.MEDIUM),
|
||||
COMPLEX: resolveTier(config.tiers.COMPLEX),
|
||||
REASONING: resolveTier(config.tiers.REASONING),
|
||||
},
|
||||
tier_model_params: {
|
||||
...(current.tier_model_params?.NON_REASONING && { NON_REASONING: current.tier_model_params.NON_REASONING }),
|
||||
...params,
|
||||
},
|
||||
customTechnicalKeywords: config.custom_technical_keywords ?? [],
|
||||
keywordTierRules: hydrateKeywordTierRules(config.keyword_tier_rules ?? []),
|
||||
semanticMatchingEnabled: config.semantic_keyword_matching ?? false,
|
||||
embeddingModel: config.embedding_model && resolve(config.embedding_model),
|
||||
matchThreshold: config.match_threshold ?? DEFAULT_MATCH_THRESHOLD,
|
||||
escalationKeywords: config.escalation_keywords ?? DEFAULT_ESCALATION_KEYWORDS,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue