feat(ui): auto-router controls for context-window escalation (#39054)

* feat(ui): auto-router controls for context-window escalation

Adds an Advanced: Context Window Escalation section to the auto-router
form, both create and edit arms, with the toggle for
enable_context_window_escalation and a clamped decimal input for
context_window_escalation_buffer. An untouched control keeps both keys
out of the payload so the router tracks the backend defaults; an
explicit opt-out (false) survives the edit round-trip through the
managed-keys projection and the hydrator, and preset prefill maps both
keys straight through so a preset cannot silently drop them

Resolves LIT-6601

* fix(ui): clearing the context-window buffer removes it from the payload

Both review bots converged on the same defect: an emptied buffer field
early-returned in commitBuffer, the draft was discarded on blur, and the
stale number reappeared and stayed in the saved config, contradicting
the copy that an empty field tracks the backend default. An empty commit
now removes the key, which the managed-keys projection propagates as a
real deletion on edit. Also trims the narrative comments the review
flagged as restating behavior
This commit is contained in:
tin-berri 2026-08-31 20:02:08 -07:00 committed by GitHub
parent 0565d33fa5
commit 502b3a2f79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 196 additions and 0 deletions

View file

@ -29,6 +29,7 @@ import React from "react";
import { ModelGroup } from "@/components/llm_calls/fetch_models";
import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig";
import ClassificationMethodConfig from "./ClassificationMethodConfig";
import ContextWindowEscalationConfig from "./ContextWindowEscalationConfig";
import { Restricted, restrictedBy } from "./TierRestrictions";
import { type TierSetAction, applyTierSetAction, setFallbackTier } from "./tier_set_actions";
import {
@ -392,6 +393,13 @@ export interface ComplexityRouterConfigValue {
tier_distance_penalty?: number;
adaptive_eligible?: AdaptiveEligible;
return_raw_model_name?: boolean;
/**
* Context-window escalation gate. Undefined means untouched, which keeps both keys out of the
* payload so the router tracks the backend defaults (enabled, 0.95 buffer); an explicit false
* is a real opt-out and must survive the edit round-trip.
*/
enable_context_window_escalation?: boolean;
context_window_escalation_buffer?: number;
/**
* Heuristic scorer knobs. Undefined means the operator never touched them, which keeps the key out of the
* payload so the router tracks the backend defaults rather than freezing today's numbers.
@ -827,6 +835,11 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
<PlanModeOverrideControls value={value} onChange={onChange} planModeTierOptions={planModeTierOptions} />
),
},
{
key: "context-window",
label: <strong className="text-foreground font-semibold">Advanced: Context Window Escalation</strong>,
children: <ContextWindowEscalationConfig value={value} onChange={onChange} />,
},
{
key: "response",
label: <strong className="text-foreground font-semibold">Advanced: Response Format</strong>,

View file

@ -0,0 +1,60 @@
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import React from "react";
import type { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
const ContextWindowEscalationConfig: React.FC<{
value: ComplexityRouterConfigValue;
onChange: (value: ComplexityRouterConfigValue) => void;
}> = ({ value, onChange }) => {
const enabled = value.enable_context_window_escalation ?? true;
// A number input renders Number("0.") as "0", so a decimal cannot be typed without a local draft.
const [bufferDraft, setBufferDraft] = React.useState<string | null>(null);
const commitBuffer = (raw: string) => {
setBufferDraft(null);
if (raw.trim() === "") {
onChange({ ...value, context_window_escalation_buffer: undefined });
return;
}
const parsed = Number(raw);
if (!Number.isFinite(parsed)) return;
onChange({ ...value, context_window_escalation_buffer: Math.min(1, Math.max(0.01, parsed)) });
};
return (
<>
<div className="flex items-center gap-2 mb-2">
<Switch
checked={enabled}
onCheckedChange={(next) => onChange({ ...value, enable_context_window_escalation: next })}
aria-label="Escalate oversized prompts to a tier that fits"
/>
<strong className="font-semibold">Escalate oversized prompts to a tier that fits</strong>
</div>
<span className="block text-xs mb-3 text-muted-foreground">
When a prompt provably cannot fit the decided tier&apos;s context windows, route it to the lowest tier whose
window holds it instead of letting the provider reject it. Off means requests dispatch on complexity alone.
</span>
{enabled && (
<div style={{ maxWidth: 320 }}>
<label className="block text-sm font-medium mb-1" htmlFor="context-window-escalation-buffer">
Window fit buffer
</label>
<Input
id="context-window-escalation-buffer"
inputMode="decimal"
value={bufferDraft ?? value.context_window_escalation_buffer ?? ""}
placeholder="0.95"
onChange={(event) => setBufferDraft(event.target.value)}
onBlur={(event) => commitBuffer(event.target.value)}
/>
<span className="block text-xs mt-1 text-muted-foreground">
Fraction of a model&apos;s window the counted prompt must fit within, above 0 up to 1. Empty tracks the
backend default of 0.95.
</span>
</div>
)}
</>
);
};
export default ContextWindowEscalationConfig;

View file

@ -373,6 +373,71 @@ describe("AddAutoRouterTab", () => {
});
});
it("carries a context-window escalation opt-out 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), "ctx-window-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Context Window Escalation"));
const toggle = await screen.findByRole("switch", { name: "Escalate oversized prompts to a tier that fits" });
expect(toggle).toBeChecked();
await user.click(toggle);
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({
enable_context_window_escalation: false,
});
});
it("clamps the context-window buffer to 1 and keeps an untouched buffer out of the payload", async () => {
const user = userEvent.setup();
vi.mocked(getMissingTiersError).mockReturnValue(null);
renderWithProviders(<Harness />);
await user.type(screen.getByPlaceholderText(/smart_router/i), "ctx-buffer-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Context Window Escalation"));
const buffer = await screen.findByLabelText("Window fit buffer");
fireEvent.change(buffer, { target: { value: "1.5" } });
fireEvent.blur(buffer, { target: { value: "1.5" } });
await user.click(screen.getByRole("button", { name: /add auto router/i }));
await waitFor(() => expect(handleAddAutoRouterSubmit).toHaveBeenCalled());
const config = vi.mocked(handleAddAutoRouterSubmit).mock.calls.at(-1)?.[0].complexity_router_config;
expect(config).toMatchObject({ context_window_escalation_buffer: 1 });
expect(config).not.toHaveProperty("enable_context_window_escalation");
});
it("clearing the buffer removes it from the payload so the router tracks the backend default", async () => {
const user = userEvent.setup();
vi.mocked(getMissingTiersError).mockReturnValue(null);
renderWithProviders(<Harness />);
await user.type(screen.getByPlaceholderText(/smart_router/i), "ctx-clear-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Context Window Escalation"));
const buffer = await screen.findByLabelText("Window fit buffer");
fireEvent.change(buffer, { target: { value: "0.8" } });
fireEvent.blur(buffer, { target: { value: "0.8" } });
fireEvent.change(buffer, { target: { value: "" } });
fireEvent.blur(buffer, { target: { value: "" } });
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).not.toHaveProperty(
"context_window_escalation_buffer",
);
});
// The scalar floor is the one scorer knob with no group dict behind it, so its wiring into the create
// payload is only proven end to end. 0 is the case a truthy check would silently drop.
it("carries a reasoning override floor of 0 through to the create payload", async () => {

View file

@ -367,6 +367,8 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
tokenThresholds: complexityRouterConfig.token_thresholds,
dimensionWeights: complexityRouterConfig.dimension_weights,
reasoningOverrideMinScore: complexityRouterConfig.reasoning_override_min_score,
enableContextWindowEscalation: complexityRouterConfig.enable_context_window_escalation,
contextWindowEscalationBuffer: complexityRouterConfig.context_window_escalation_buffer,
};
const submitRecommendedRouter = async (name: string) => {

View file

@ -59,6 +59,16 @@ describe("buildComplexityRouterConfig", () => {
expect(config).toEqual(expected);
});
it("carries an explicit context-window escalation opt-out and buffer, false included", () => {
const config = buildComplexityRouterConfig({
...baseParams,
enableContextWindowEscalation: false,
contextWindowEscalationBuffer: 0.9,
});
expect(config.enable_context_window_escalation).toBe(false);
expect(config.context_window_escalation_buffer).toBe(0.9);
});
it("trims escalation keywords and drops blank entries", () => {
const config = buildComplexityRouterConfig({
...baseParams,

View file

@ -123,6 +123,8 @@ export interface BuildComplexityRouterConfigParams {
dimensionWeights?: DimensionWeights;
reasoningOverrideMinScore?: number;
tierModelParams?: TierModelParamsByTier;
enableContextWindowEscalation?: boolean;
contextWindowEscalationBuffer?: number;
}
/**
@ -174,6 +176,8 @@ export interface ComplexityRouterConfigPayload {
token_thresholds?: TokenThresholds;
dimension_weights?: DimensionWeights;
reasoning_override_min_score?: number;
enable_context_window_escalation?: boolean;
context_window_escalation_buffer?: number;
tier_model_configs?: Record<string, { model_name: string; litellm_params: TierModelParams }[]>;
}
@ -407,6 +411,8 @@ export const buildComplexityRouterConfig = ({
dimensionWeights,
reasoningOverrideMinScore,
tierModelParams,
enableContextWindowEscalation,
contextWindowEscalationBuffer,
}: BuildComplexityRouterConfigParams): ComplexityRouterConfigPayload => {
const serializedTierModelConfigs = customTierSet
? serializeTierModelConfigs(
@ -463,6 +469,12 @@ export const buildComplexityRouterConfig = ({
adaptive_eligible: adaptiveEligible,
}),
...(returnRawModelName && { return_raw_model_name: true }),
...(enableContextWindowEscalation !== undefined && {
enable_context_window_escalation: enableContextWindowEscalation,
}),
...(contextWindowEscalationBuffer !== undefined && {
context_window_escalation_buffer: contextWindowEscalationBuffer,
}),
...scorerKnobs,
};
if (!customTierSet) return payload;

View file

@ -487,6 +487,8 @@ describe("managed keys survive an untouched open-and-save", () => {
token_thresholds: { simple: 20, complex: 500 },
dimension_weights: { tokenCount: 0.1 },
reasoning_override_min_score: 0.3,
enable_context_window_escalation: false,
context_window_escalation_buffer: 0.9,
};
// tier_definitions, fallback_tier and classification_prompt cannot sit beside heuristic_first, which

View file

@ -107,6 +107,8 @@ export interface StoredComplexityRouterConfig {
tier_distance_penalty?: number;
adaptive_eligible?: AdaptiveEligible;
return_raw_model_name?: boolean;
enable_context_window_escalation?: unknown;
context_window_escalation_buffer?: unknown;
}
/**
@ -178,6 +180,14 @@ export const hydrateComplexityRouterConfig = (
tier_distance_penalty: parsedConfig.tier_distance_penalty,
adaptive_eligible: parsedConfig.adaptive_eligible || "all",
return_raw_model_name: parsedConfig.return_raw_model_name || false,
enable_context_window_escalation:
typeof parsedConfig.enable_context_window_escalation === "boolean"
? parsedConfig.enable_context_window_escalation
: undefined,
context_window_escalation_buffer:
typeof parsedConfig.context_window_escalation_buffer === "number"
? parsedConfig.context_window_escalation_buffer
: undefined,
};
};
@ -208,6 +218,8 @@ export const MANAGED_COMPLEXITY_ROUTER_KEYS = new Set([
"token_thresholds",
"dimension_weights",
"reasoning_override_min_score",
"enable_context_window_escalation",
"context_window_escalation_buffer",
]);
// Managed only when the caller passes the corresponding state. A caller that does not render
@ -307,6 +319,8 @@ export const buildUpdatedComplexityRouterConfig = (
dimensionWeights: value.dimension_weights,
reasoningOverrideMinScore: value.reasoning_override_min_score,
tierModelParams: value.tier_model_params,
enableContextWindowEscalation: value.enable_context_window_escalation,
contextWindowEscalationBuffer: value.context_window_escalation_buffer,
};
const built = buildComplexityRouterConfig(builderParams);

View file

@ -573,6 +573,22 @@ describe("autorouter_presets", () => {
expect(prefill.escalationKeywords).toEqual([]);
});
it("carries a preset's context-window escalation opt-out and buffer through the prefill", () => {
const prefill = buildPresetPrefill(
{
tiers: { SIMPLE: ["gpt-5-nano"], MEDIUM: [], COMPLEX: [], REASONING: [] },
classifier_type: "heuristic",
session_affinity: false,
deployment_affinity: true,
enable_context_window_escalation: false,
context_window_escalation_buffer: 0.9,
},
groupsOnly(["gpt-5-nano"]),
);
expect(prefill.complexityRouterConfig.enable_context_window_escalation).toBe(false);
expect(prefill.complexityRouterConfig.context_window_escalation_buffer).toBe(0.9);
});
it("falls back to the defaults when a preset omits match_threshold and escalation_keywords", () => {
const prefill = buildPresetPrefill(
{

View file

@ -295,6 +295,8 @@ export const buildPresetPrefill = (
tier_distance_penalty: config.tier_distance_penalty,
adaptive_eligible: config.adaptive_eligible,
return_raw_model_name: config.return_raw_model_name,
enable_context_window_escalation: config.enable_context_window_escalation,
context_window_escalation_buffer: config.context_window_escalation_buffer,
},
customTechnicalKeywords: config.custom_technical_keywords ?? [],
keywordTierRules: hydrateKeywordTierRules(config.keyword_tier_rules ?? []),