Files
OmniRoute/open-sse/services/combo/contextRequirements.ts
Austin Liu 8a7b2467fd [v3.8.50] feat(combo): add maxContextWindow to contextRequirements (fixes #8777) (#8790)
Validated in local merge-train T6 (ungrouped batch 1)
2026-08-06 05:24:39 -03:00

166 lines
5.6 KiB
TypeScript

/**
* Context requirements filtering for combo targets.
* Applies minContextWindow, preferLargeContext, and contextFilterMode
* from combo config to filter and sort targets by context window size.
*/
import { getModelContextLimit } from "../../../src/lib/modelCapabilities";
import type { ComboLogger, ResolvedComboTarget } from "./types.ts";
export interface ContextRequirements {
minContextWindow?: number;
maxContextWindow?: number;
preferLargeContext?: boolean;
contextFilterMode?: "strict" | "lenient";
}
/**
* Get context window size for a target model.
* Returns null if unknown.
*/
function getTargetContextWindow(target: ResolvedComboTarget): number | null {
const limit = getModelContextLimit(target.provider, target.modelStr);
return typeof limit === "number" && limit > 0 ? limit : null;
}
/**
* Apply context requirements filtering and sorting to combo targets.
*
* Filtering logic:
* - If minContextWindow is set, filters out models below that threshold
* - contextFilterMode determines handling of unknown context limits:
* - "strict": excludes models with unknown context limits
* - "lenient": includes models with unknown context limits
* - #8786 fail-open: when "strict" would empty the pool and at least one
* unknown-context target exists, restore those unknowns instead of returning
* [] (which becomes a false 404 "no executable targets"). Known-too-small
* targets are never resurrected.
*
* Sorting logic:
* - If preferLargeContext is true, sorts remaining targets by context size (descending)
* - Unknown context limits sort to the end
*
* @param targets - Array of resolved combo targets
* @param requirements - Context requirements from combo config
* @param log - Combo logger for debug output
* @returns Filtered and sorted targets array
*/
export function applyContextRequirements(
targets: ResolvedComboTarget[],
requirements: ContextRequirements | undefined,
log: ComboLogger
): ResolvedComboTarget[] {
if (!requirements || targets.length === 0) return targets;
const {
minContextWindow,
maxContextWindow,
preferLargeContext,
contextFilterMode = "lenient",
} = requirements;
// No requirements specified
if (!minContextWindow && !maxContextWindow && !preferLargeContext) return targets;
let filtered = targets;
// Apply minContextWindow filtering
if (minContextWindow && minContextWindow > 0) {
const beforeFilterCount = filtered.length;
const classified = filtered.map((target) => ({
target,
contextWindow: getTargetContextWindow(target),
}));
filtered = classified
.filter(({ contextWindow }) => {
// Unknown context limit handling
if (contextWindow === null) {
return contextFilterMode === "lenient";
}
// Known context limit - check threshold
return contextWindow >= minContextWindow;
})
.map(({ target }) => target);
// #8786: strict must not turn an otherwise-executable combo into an empty
// pool solely because the capability catalog lacks context metadata. When
// no known-good target survives, fail open to the unknown-context set
// (same spirit as the request-compat context fail-open path).
if (filtered.length === 0 && beforeFilterCount > 0 && contextFilterMode === "strict") {
const unknowns = classified
.filter(({ contextWindow }) => contextWindow === null)
.map(({ target }) => target);
if (unknowns.length > 0) {
log.warn(
"COMBO",
`Context requirements: strict mode would empty the pool (${beforeFilterCount} targets, none known >= ${minContextWindow}); failing open to ${unknowns.length} unknown-context target(s)`
);
filtered = unknowns;
}
}
if (filtered.length < beforeFilterCount) {
log.info(
"COMBO",
`Context requirements: filtered ${beforeFilterCount}${filtered.length} targets (minContextWindow: ${minContextWindow}, mode: ${contextFilterMode})`
);
log.debug?.(
"COMBO",
`Context requirements: kept models ${filtered.map((t) => t.modelStr).join(", ")}`
);
}
}
// 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) => {
const aContext = getTargetContextWindow(a) ?? 0;
const bContext = getTargetContextWindow(b) ?? 0;
return bContext - aContext; // Descending order
});
log.debug?.(
"COMBO",
`Context requirements: sorted by context size (descending): ${filtered
.map((t) => {
const ctx = getTargetContextWindow(t);
return `${t.modelStr}(${ctx === null ? "unknown" : ctx})`;
})
.join(", ")}`
);
}
return filtered;
}