mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
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
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:
parent
79d412efc2
commit
6f36bee6ba
13 changed files with 210 additions and 14 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'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's tier.
|
||||
Keeps a session on its first turn's model instead of re-classifying each turn. Also pins the
|
||||
deployment.
|
||||
</Text>
|
||||
</>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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" } };
|
||||
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue