feat(ui): deployment affinity toggle for the auto-router (#36302)
Some checks failed
Unit Tests: Integrations (Callbacks & Logging) / integrations (push) Waiting to run
Unit Tests: LLM Provider Transformations / Vertex AI (push) Waiting to run
CI Coverage / assert-ci-coverage (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Unit Tests: Core Utilities / core-utils (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Enterprise, Google GenAI & Routing / enterprise-routing (push) Waiting to run
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-endpoints (push) Waiting to run
Unit Tests: LLM Provider Transformations / All Other Providers (push) Waiting to run
Unit Tests: MCP, Secrets, Containers & Misc / misc (push) Waiting to run
Unit Tests: Proxy Auth & Key Management / proxy-auth (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy API Endpoints / proxy-server (push) Waiting to run
Unit Tests: Proxy Infrastructure / proxy-infra (push) Waiting to run
Unit Tests: Proxy Legacy Tests / auth-and-jwt (push) Waiting to run
Unit Tests: Proxy Legacy Tests / key-generation (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-config (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-response-and-misc (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-server (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-server-extras (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-token-counter (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-user-auth-and-spend (push) Waiting to run
Unit Tests: Proxy Legacy Tests / proxy-utils (push) Waiting to run
Unit Tests: Responses, Caching & Types / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled

This commit is contained in:
tin-berri 2026-08-10 19:31:20 -07:00 committed by GitHub
parent 79d412efc2
commit 6f36bee6ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 210 additions and 14 deletions

View file

@ -11,7 +11,8 @@
},
"classifier_type": "heuristic",
"escalation_keywords": ["LITELLM ESCALATE"],
"session_affinity": false
"session_affinity": false,
"deployment_affinity": true
}
},
"openai_family": {
@ -26,7 +27,8 @@
},
"classifier_type": "heuristic",
"escalation_keywords": ["LITELLM ESCALATE"],
"session_affinity": false
"session_affinity": false,
"deployment_affinity": true
}
}
}

View file

@ -602,3 +602,32 @@ describe("ComplexityRouterConfig tier labels", () => {
expect(screen.getByTitle("Deep")).toBeInTheDocument();
});
});
describe("ComplexityRouterConfig affinity panel", () => {
it("holds both affinity switches with their backend defaults", () => {
renderWithProviders(<ComplexityRouterConfig {...baseProps} />);
fireEvent.click(screen.getByText("Advanced: Affinity"));
expect(screen.getByRole("switch", { name: "Pin a session to one deployment per model group" })).toBeChecked();
expect(screen.getByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked();
});
it("writes deployment_affinity through onChange without touching other keys", () => {
const onChange = vi.fn();
renderWithProviders(<ComplexityRouterConfig {...baseProps} onChange={onChange} />);
fireEvent.click(screen.getByText("Advanced: Affinity"));
fireEvent.click(screen.getByRole("switch", { name: "Pin a session to one deployment per model group" }));
expect(onChange).toHaveBeenCalledWith({ ...defaultValue, deployment_affinity: false });
});
it("renders a stored deployment_affinity=false as off", () => {
renderWithProviders(
<ComplexityRouterConfig {...baseProps} value={{ ...defaultValue, deployment_affinity: false }} />,
);
fireEvent.click(screen.getByText("Advanced: Affinity"));
expect(screen.getByRole("switch", { name: "Pin a session to one deployment per model group" })).not.toBeChecked();
});
});

View file

@ -15,6 +15,7 @@ export const DEFAULT_TIER_DISTANCE_PENALTY = 0.5;
export const DEFAULT_CLASSIFIER_CONTEXT_WINDOW_SIZE = 3;
export const DEFAULT_CLASSIFIER_CONTEXT_PER_TURN_CHARS = 200;
export const DEFAULT_SESSION_AFFINITY = false;
export const DEFAULT_DEPLOYMENT_AFFINITY = true;
export interface ComplexityTiers {
SIMPLE: string[];
@ -56,6 +57,7 @@ export interface ComplexityRouterConfigValue {
classifier_context_include_assistant_turns?: boolean;
classifier_fallback?: ClassifierFallback;
session_affinity?: boolean;
deployment_affinity?: boolean;
adaptive?: boolean;
adaptive_weights?: AdaptiveRouterWeights;
tier_distance_penalty?: number;
@ -277,14 +279,26 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
children: <AdaptiveRoutingConfig value={value} onChange={onChange} />,
},
{
key: "session-affinity",
key: "affinity",
label: (
<Text strong style={{ color: "#374151" }}>
Advanced: Session Affinity
Advanced: Affinity
</Text>
),
children: (
<>
<div className="flex items-center gap-2 mb-2">
<Switch
checked={value.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY}
onChange={(deploymentAffinity) => onChange({ ...value, deployment_affinity: deploymentAffinity })}
aria-label="Pin a session to one deployment per model group"
/>
<Text strong>Pin a session to one deployment per model group</Text>
</div>
<Text type="secondary" style={{ display: "block", fontSize: 12, marginBottom: 12 }}>
Keeps a session on the same deployment within a group, so provider prompt caches stay warm. Turn off
to load-balance every turn.
</Text>
<div className="flex items-center gap-2 mb-2">
<Switch
checked={value.session_affinity ?? DEFAULT_SESSION_AFFINITY}
@ -294,10 +308,8 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
<Text strong>Pin a session to its first model</Text>
</div>
<Text type="secondary" style={{ display: "block", fontSize: 12 }}>
Off by default: every turn is classified on its own merits and routed to the cheapest adequate tier.
Turn this on to reuse the model chosen on a session&apos;s first turn for every later turn, which
preserves provider prompt caches and avoids cross-model conversation-history errors, at the cost of
keeping the whole session on the first turn&apos;s tier.
Keeps a session on its first turn&apos;s model instead of re-classifying each turn. Also pins the
deployment.
</Text>
</>
),

View file

@ -273,7 +273,7 @@ describe("AddAutoRouterTab", () => {
await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Session Affinity"));
await user.click(screen.getByText("Advanced: Affinity"));
expect(await screen.findByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked();
await user.click(screen.getByRole("button", { name: /add auto router/i }));
@ -292,7 +292,7 @@ describe("AddAutoRouterTab", () => {
await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Session Affinity"));
await user.click(screen.getByText("Advanced: Affinity"));
await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" }));
await user.click(screen.getByRole("button", { name: /add auto router/i }));
@ -303,6 +303,46 @@ describe("AddAutoRouterTab", () => {
});
});
it("defaults a new router to deployment affinity on, matching the backend field default", async () => {
const user = userEvent.setup();
vi.mocked(getMissingTiersError).mockReturnValue(null);
renderWithProviders(<Harness />);
await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Affinity"));
expect(
await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }),
).toBeChecked();
await user.click(screen.getByRole("button", { name: /add auto router/i }));
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0].complexity_router_config).toMatchObject({
deployment_affinity: true,
});
});
it("carries deployment affinity turned off through to the create payload", async () => {
const user = userEvent.setup();
vi.mocked(getMissingTiersError).mockReturnValue(null);
renderWithProviders(<Harness />);
await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Affinity"));
await user.click(await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }));
await user.click(screen.getByRole("button", { name: /add auto router/i }));
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
expect(vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0].complexity_router_config).toMatchObject({
deployment_affinity: false,
});
});
// Custom is the escape hatch, not the headline choice, so it's listed after every bundled preset
// rather than first.
it("lists Custom Configuration after the bundled presets", () => {

View file

@ -15,6 +15,7 @@ import ComplexityRouterConfig, {
ComplexityTiers,
DEFAULT_ADAPTIVE_WEIGHTS,
DEFAULT_SESSION_AFFINITY,
DEFAULT_DEPLOYMENT_AFFINITY,
DEFAULT_TIER_DISTANCE_PENALTY,
} from "./ComplexityRouterConfig";
import { KeywordTierRule } from "./KeywordTierRules";
@ -290,6 +291,7 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
classifierContextIncludeAssistantTurns: complexityRouterConfig.classifier_context_include_assistant_turns,
classifierFallback: complexityRouterConfig.classifier_fallback,
sessionAffinity: complexityRouterConfig.session_affinity ?? DEFAULT_SESSION_AFFINITY,
deploymentAffinity: complexityRouterConfig.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
customTechnicalKeywords,
keywordTierRules,
semanticMatchingEnabled,

View file

@ -26,6 +26,7 @@ const baseParams: BuildComplexityRouterConfigParams = {
classifierContextIncludeAssistantTurns: undefined,
classifierFallback: undefined,
sessionAffinity: false,
deploymentAffinity: true,
customTechnicalKeywords: [],
keywordTierRules: [],
semanticMatchingEnabled: false,
@ -46,6 +47,7 @@ describe("buildComplexityRouterConfig", () => {
tiers,
classifier_type: "heuristic",
session_affinity: false,
deployment_affinity: true,
escalation_keywords: ["LITELLM ESCALATE"],
});
});

View file

@ -30,6 +30,7 @@ export interface BuildComplexityRouterConfigParams {
classifierContextIncludeAssistantTurns: boolean | undefined;
classifierFallback: ClassifierFallback | undefined;
sessionAffinity: boolean;
deploymentAffinity: boolean;
customTechnicalKeywords: string[];
keywordTierRules: KeywordTierRule[];
semanticMatchingEnabled: boolean;
@ -53,6 +54,7 @@ export interface ComplexityRouterConfigPayload {
classifier_context_include_assistant_turns?: boolean;
classifier_fallback?: ClassifierFallback;
session_affinity: boolean;
deployment_affinity: boolean;
custom_technical_keywords?: string[];
keyword_tier_rules?: { keywords: string[]; tier: KeywordTierRule["tier"] }[];
semantic_keyword_matching?: boolean;
@ -137,6 +139,7 @@ export const buildComplexityRouterConfig = ({
classifierContextIncludeAssistantTurns,
classifierFallback,
sessionAffinity,
deploymentAffinity,
customTechnicalKeywords,
keywordTierRules,
semanticMatchingEnabled,
@ -173,6 +176,7 @@ export const buildComplexityRouterConfig = ({
classifier_context_include_assistant_turns: classifierContextIncludeAssistantTurns,
}),
session_affinity: sessionAffinity,
deployment_affinity: deploymentAffinity,
...(customTechnicalKeywords.length > 0 && { custom_technical_keywords: customTechnicalKeywords }),
...(cleanedKeywordTierRules.length > 0 && { keyword_tier_rules: cleanedKeywordTierRules }),
escalation_keywords: cleanedEscalationKeywords,

View file

@ -228,6 +228,31 @@ describe("buildUpdatedComplexityRouterConfig session affinity", () => {
});
});
describe("buildUpdatedComplexityRouterConfig deployment affinity", () => {
it("writes deployment_affinity=false when the toggle is off", () => {
const result = buildUpdatedComplexityRouterConfig(STORED, { ...FORM_VALUE, deployment_affinity: false });
expect(result.deployment_affinity).toBe(false);
});
it("writes deployment_affinity=true when the toggle is on", () => {
const result = buildUpdatedComplexityRouterConfig(STORED, { ...FORM_VALUE, deployment_affinity: true });
expect(result.deployment_affinity).toBe(true);
});
it("re-asserts the backend's on-by-default when the form value is absent, rather than dropping the key", () => {
const result = buildUpdatedComplexityRouterConfig({ ...STORED, deployment_affinity: false }, FORM_VALUE);
expect(result.deployment_affinity).toBe(true);
});
it("stops a stored deployment_affinity=false from surviving a save that turned the toggle back on", () => {
const result = buildUpdatedComplexityRouterConfig(
{ ...STORED, deployment_affinity: false },
{ ...FORM_VALUE, deployment_affinity: true },
);
expect(result.deployment_affinity).toBe(true);
});
});
describe("buildUpdatedComplexityRouterConfig tier labels", () => {
const RENAMED = { ...STORED, tier_labels: { SIMPLE: "Cheap", REASONING: "Deep" } };

View file

@ -48,6 +48,7 @@ const expectedClassifiedTierConfig = {
embedding_model: "voyage-4-large",
match_threshold: 0.65,
session_affinity: false,
deployment_affinity: true,
adaptive: true,
adaptive_weights: { quality: 0.4, cost: 0.6 },
adaptive_eligible: "classified_tier",
@ -68,6 +69,7 @@ const expectedAdaptiveDisabledConfig = {
embedding_model: "voyage-4-large",
match_threshold: 0.65,
session_affinity: false,
deployment_affinity: true,
};
describe("buildUpdatedComplexityRouterConfig", () => {

View file

@ -365,7 +365,7 @@ describe("EditAutoRouterModal session affinity", () => {
const user = userEvent.setup();
renderWithStoredConfig(STORED_CONFIG);
await user.click(await screen.findByText("Advanced: Session Affinity"));
await user.click(await screen.findByText("Advanced: Affinity"));
expect(await screen.findByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked();
await user.click(screen.getByRole("button", { name: /save changes/i }));
@ -378,7 +378,7 @@ describe("EditAutoRouterModal session affinity", () => {
const user = userEvent.setup();
renderWithStoredConfig({ ...STORED_CONFIG, session_affinity: true });
await user.click(await screen.findByText("Advanced: Session Affinity"));
await user.click(await screen.findByText("Advanced: Affinity"));
expect(await screen.findByRole("switch", { name: "Pin a session to its first model" })).toBeChecked();
await user.click(screen.getByRole("button", { name: /save changes/i }));
@ -391,7 +391,7 @@ describe("EditAutoRouterModal session affinity", () => {
const user = userEvent.setup();
renderWithStoredConfig(STORED_CONFIG);
await user.click(await screen.findByText("Advanced: Session Affinity"));
await user.click(await screen.findByText("Advanced: Affinity"));
await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" }));
await user.click(screen.getByRole("button", { name: /save changes/i }));
@ -404,7 +404,7 @@ describe("EditAutoRouterModal session affinity", () => {
const user = userEvent.setup();
renderWithStoredConfig({ ...STORED_CONFIG, session_affinity: true });
await user.click(await screen.findByText("Advanced: Session Affinity"));
await user.click(await screen.findByText("Advanced: Affinity"));
await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" }));
await user.click(screen.getByRole("button", { name: /save changes/i }));
@ -414,6 +414,67 @@ describe("EditAutoRouterModal session affinity", () => {
});
});
describe("EditAutoRouterModal deployment affinity", () => {
beforeEach(() => {
modelPatchUpdateCall.mockClear();
});
const renderWithStoredConfig = (complexity_router_config: Record<string, unknown>) =>
renderWithProviders(
<EditAutoRouterModal
isVisible
onCancel={vi.fn()}
onSuccess={vi.fn()}
modelData={{ ...MODEL_DATA, litellm_params: { ...MODEL_DATA.litellm_params, complexity_router_config } }}
accessToken="token"
userRole="Admin"
/>,
);
it("shows a stored config with no deployment_affinity key as on, matching the backend default", async () => {
const user = userEvent.setup();
renderWithStoredConfig(STORED_CONFIG);
await user.click(await screen.findByText("Advanced: Affinity"));
expect(
await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }),
).toBeChecked();
await user.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled());
expect(savedConfig().deployment_affinity).toBe(true);
});
it("shows a stored deployment_affinity=false as off and preserves it through an untouched save", async () => {
const user = userEvent.setup();
renderWithStoredConfig({ ...STORED_CONFIG, deployment_affinity: false });
await user.click(await screen.findByText("Advanced: Affinity"));
expect(
await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }),
).not.toBeChecked();
await user.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled());
expect(savedConfig().deployment_affinity).toBe(false);
});
it("persists turning deployment affinity off", async () => {
const user = userEvent.setup();
renderWithStoredConfig(STORED_CONFIG);
await user.click(await screen.findByText("Advanced: Affinity"));
await user.click(await screen.findByRole("switch", { name: "Pin a session to one deployment per model group" }));
await user.click(screen.getByRole("button", { name: /save changes/i }));
await waitFor(() => expect(modelPatchUpdateCall).toHaveBeenCalled());
expect(savedConfig().deployment_affinity).toBe(false);
});
});
describe("EditAutoRouterModal custom classifier prompt and fallback", () => {
beforeEach(() => {
modelPatchUpdateCall.mockClear();

View file

@ -21,6 +21,7 @@ import ComplexityRouterConfig, {
ComplexityRouterConfigValue,
DEFAULT_ADAPTIVE_WEIGHTS,
DEFAULT_SESSION_AFFINITY,
DEFAULT_DEPLOYMENT_AFFINITY,
DEFAULT_TIER_DISTANCE_PENALTY,
} from "../add_model/ComplexityRouterConfig";
import NotificationsManager from "../molecules/notifications_manager";
@ -55,6 +56,7 @@ const MANAGED_COMPLEXITY_ROUTER_KEYS = new Set([
"classifier_context_include_assistant_turns",
"classifier_fallback",
"session_affinity",
"deployment_affinity",
"adaptive",
"adaptive_weights",
"tier_distance_penalty",
@ -127,6 +129,7 @@ export const buildUpdatedComplexityRouterConfig = (
classifier_context_include_assistant_turns: value.classifier_context_include_assistant_turns,
}),
session_affinity: value.session_affinity ?? DEFAULT_SESSION_AFFINITY,
deployment_affinity: value.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
...(customTechnicalKeywords &&
customTechnicalKeywords.length > 0 && {
custom_technical_keywords: customTechnicalKeywords,
@ -263,6 +266,10 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
typeof parsedConfig.session_affinity === "boolean"
? parsedConfig.session_affinity
: DEFAULT_SESSION_AFFINITY,
deployment_affinity:
typeof parsedConfig.deployment_affinity === "boolean"
? parsedConfig.deployment_affinity
: DEFAULT_DEPLOYMENT_AFFINITY,
adaptive: parsedConfig.adaptive || false,
adaptive_weights: parsedConfig.adaptive_weights,
tier_distance_penalty: parsedConfig.tier_distance_penalty,

View file

@ -111,6 +111,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: [presetModel], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
expect(getMissingModels(config, availability)).toEqual([]);
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual([group]);
@ -153,6 +154,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: ["claude-opus-5"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual(["a-group"]);
});
@ -166,6 +168,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: ["claude-opus-5"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
expect(buildPresetPrefill(config, availability).complexityRouterConfig.tiers.SIMPLE).toEqual(["claude-opus-5"]);
});
@ -198,6 +201,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: [presetModel], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
});
it("resolves a preset model to a group expanded from a wildcard deployment", () => {
@ -440,6 +444,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
match_threshold: 0,
escalation_keywords: [],
};
@ -454,6 +459,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic",
session_affinity: false,
deployment_affinity: true,
},
groupsOnly(["gpt-5-nano"]),
);
@ -469,6 +475,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
session_affinity: false,
deployment_affinity: true,
};
const labeled = buildPresetPrefill(
{ ...base, tier_labels: { SIMPLE: "Cheap", REASONING: "Deep" } },
@ -483,6 +490,7 @@ describe("autorouter_presets", () => {
tiers: { SIMPLE: ["claude-sonnet-4-5"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic" as const,
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"]);

View file

@ -8,6 +8,7 @@ import {
ClassifierType,
ClassifierLLMConfig,
DEFAULT_SESSION_AFFINITY,
DEFAULT_DEPLOYMENT_AFFINITY,
} from "@/components/add_model/ComplexityRouterConfig";
import { KeywordTierRule } from "@/components/add_model/KeywordTierRules";
import { hydrateKeywordTierRules } from "@/components/add_model/complexity_router_keywords";
@ -260,6 +261,7 @@ export const buildPresetPrefill = (
classifier_context_per_turn_chars: config.classifier_context_per_turn_chars,
classifier_context_include_assistant_turns: config.classifier_context_include_assistant_turns,
session_affinity: config.session_affinity ?? DEFAULT_SESSION_AFFINITY,
deployment_affinity: config.deployment_affinity ?? DEFAULT_DEPLOYMENT_AFFINITY,
adaptive: config.adaptive,
adaptive_weights: config.adaptive_weights,
tier_distance_penalty: config.tier_distance_penalty,