[v3.8.50] feat(combo): add maxContextWindow to contextRequirements (fixes #8777) (#8790)

Validated in local merge-train T6 (ungrouped batch 1)
This commit is contained in:
Austin Liu
2026-08-06 17:54:39 +09:30
committed by GitHub
parent a65887eec0
commit 8a7b2467fd
6 changed files with 109 additions and 4 deletions

View File

@@ -9,6 +9,7 @@ import type { ComboLogger, ResolvedComboTarget } from "./types.ts";
export interface ContextRequirements {
minContextWindow?: number;
maxContextWindow?: number;
preferLargeContext?: boolean;
contextFilterMode?: "strict" | "lenient";
}
@@ -51,10 +52,15 @@ export function applyContextRequirements(
): ResolvedComboTarget[] {
if (!requirements || targets.length === 0) return targets;
const { minContextWindow, preferLargeContext, contextFilterMode = "lenient" } = requirements;
const {
minContextWindow,
maxContextWindow,
preferLargeContext,
contextFilterMode = "lenient",
} = requirements;
// No requirements specified
if (!minContextWindow && !preferLargeContext) return targets;
if (!minContextWindow && !maxContextWindow && !preferLargeContext) return targets;
let filtered = targets;
@@ -108,6 +114,34 @@ export function applyContextRequirements(
}
}
// Apply maxContextWindow filtering
if (maxContextWindow && maxContextWindow > 0) {
const beforeFilterCount = filtered.length;
filtered = filtered.filter((target) => {
const contextWindow = getTargetContextWindow(target);
// Unknown context limit handling
if (contextWindow === null) {
return contextFilterMode === "lenient";
}
// Known context limit - check threshold
return contextWindow <= maxContextWindow;
});
if (filtered.length < beforeFilterCount) {
log.info(
"COMBO",
`Context requirements: filtered ${beforeFilterCount}${filtered.length} targets (maxContextWindow: ${maxContextWindow}, mode: ${contextFilterMode})`
);
log.debug?.(
"COMBO",
`Context requirements: kept models ${filtered.map((t) => t.modelStr).join(", ")}`
);
}
}
// Apply preferLargeContext sorting
if (preferLargeContext && filtered.length > 1) {
filtered = [...filtered].sort((a, b) => {

View File

@@ -179,6 +179,7 @@ const DEFAULT_COMBO_CONFIG = {
contextRequirements: undefined as
| {
minContextWindow?: number;
maxContextWindow?: number;
preferLargeContext?: boolean;
contextFilterMode?: "strict" | "lenient";
}

View File

@@ -239,11 +239,13 @@ export const comboRuntimeConfigSchema = z
.optional(),
// Context window requirements for combo target filtering and sorting.
// minContextWindow: filters out models with context windows below this threshold.
// maxContextWindow: filters out models with context windows above this threshold.
// preferLargeContext: sorts remaining targets by context size (descending).
// contextFilterMode: "strict" excludes unknown-context models, "lenient" includes them.
contextRequirements: z
.object({
minContextWindow: z.coerce.number().int().min(0).max(10_000_000).optional(),
maxContextWindow: z.coerce.number().int().min(0).max(10_000_000).optional(),
preferLargeContext: z.boolean().optional(),
contextFilterMode: z.enum(["strict", "lenient"]).optional(),
})

View File

@@ -7,11 +7,13 @@ import { comboRuntimeConfigSchema } from "@/shared/validation/schemas/combo";
// production schema changes shape.
describe("Combo Context Requirements", () => {
describe("Schema Validation", () => {
it("should accept valid minContextWindow", () => {
it("should accept valid minContextWindow and maxContextWindow", () => {
const schema = comboRuntimeConfigSchema;
const valid = [
{ contextRequirements: { minContextWindow: 8192 } },
{ contextRequirements: { maxContextWindow: 32000 } },
{ contextRequirements: { minContextWindow: 8192, maxContextWindow: 128000 } },
{ contextRequirements: { minContextWindow: 32000 } },
{ contextRequirements: { minContextWindow: 128000 } },
{ contextRequirements: { minContextWindow: 1000000 } },
@@ -25,13 +27,16 @@ describe("Combo Context Requirements", () => {
}
});
it("should reject invalid minContextWindow", () => {
it("should reject invalid minContextWindow or maxContextWindow", () => {
const schema = comboRuntimeConfigSchema;
const invalid = [
{ contextRequirements: { minContextWindow: -1 } },
{ contextRequirements: { minContextWindow: 20_000_000 } },
{ contextRequirements: { minContextWindow: "invalid" } },
{ contextRequirements: { maxContextWindow: -1 } },
{ contextRequirements: { maxContextWindow: 20_000_000 } },
{ contextRequirements: { maxContextWindow: "invalid" } },
];
for (const input of invalid) {

View File

@@ -146,3 +146,43 @@ test("request exceeding every known context window returns a 400 earlyResponse",
assert.equal(body.diagnostics?.terminalReason, "context_length_exceeded");
assert.equal(body.diagnostics?.attempted, 0);
});
// #8790: maxContextWindow rejects every target whose known context window
// exceeds the configured ceiling. When that empties the pool, the
// context-requirements guard (applyContinuityFilters → #8786's
// buildEmptyComboTargetsPayload) must surface a 404 context_requirements_exhausted
// early response instead of letting an empty orderedTargets[] fall through to the
// attempt loop.
test("maxContextWindow rejecting every target returns a 404 context_requirements_exhausted earlyResponse", async () => {
saveModelsDevCapabilities({
"unit-target-resolution-max": {
big1: capabilityEntry(500_000),
big2: capabilityEntry(1_000_000),
},
});
const result = await resolveComboTargetPipeline(
deps({
combo: {
id: "c3",
name: "max-context-window-exhausted",
models: ["unit-target-resolution-max/big1", "unit-target-resolution-max/big2"],
config: {},
},
config: {
contextRequirements: { maxContextWindow: 128_000, contextFilterMode: "strict" },
},
})
);
assert.ok("earlyResponse" in result, "expected a context-requirements-exhausted early response");
if (!("earlyResponse" in result)) return;
assert.equal(result.earlyResponse.status, 404);
const body = (await result.earlyResponse.json()) as {
error?: { code?: string };
diagnostics?: { terminalReason?: string; excluded?: unknown[] };
};
assert.equal(body.error?.code, "model_not_found");
assert.equal(body.diagnostics?.terminalReason, "context_requirements_exhausted");
assert.equal(body.diagnostics?.excluded?.length, 2);
});

View File

@@ -65,6 +65,29 @@ describe("Context Requirements Integration", () => {
assert.equal(result.length, 0);
});
it("should filter targets above maxContextWindow in strict mode", () => {
const targets = [
{ modelStr: "claude-opus-4-5", provider: "anthropic", weight: 1 }, // 200k
{ modelStr: "claude-sonnet-4-5", provider: "anthropic", weight: 1 }, // 200k
{ modelStr: "claude-sonnet-4-6", provider: "anthropic", weight: 1 }, // 1000k (1M)
{ modelStr: "claude-opus-4-6", provider: "anthropic", weight: 1 }, // 1000k (1M)
];
const requirements = {
maxContextWindow: 500000,
contextFilterMode: "strict" as const,
};
const result = applyContextRequirements(targets, requirements, mockLog);
assert.equal(result.length, 2);
assert.ok(
result.every(
(target) => target.modelStr === "claude-opus-4-5" || target.modelStr === "claude-sonnet-4-5"
),
"Should only include targets with context window <= 500000"
);
});
it("should handle lenient mode with unknown context models", () => {
const targets = [
{ modelStr: "gpt-4o", provider: "openai", weight: 1 },