Files
OmniRoute/open-sse/services/combo/contextRequirements.ts
AmirHossein Rezaei 1c2143182c fix(combo): fail open when strict context filter empties unknown-only pools (#8786) (#8798)
Strict contextFilterMode excluded every target whose context limit was missing
from the capability catalog, so otherwise-executable combos returned 404
no_executable_targets. Restore unknown-context targets when no known-good
survivor remains, surface context_requirements_exhausted from targetResolution,
and keep the empty-pool payload in pinRecovery after the #8592 split.
2026-07-27 22:37:48 -03:00

132 lines
4.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;
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, preferLargeContext, contextFilterMode = "lenient" } = requirements;
// No requirements specified
if (!minContextWindow && !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 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;
}