mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 06:42:12 +03:00
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.
132 lines
4.6 KiB
TypeScript
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;
|
|
}
|