Treat context metadata as a routing hint (#8944)

Validated in post-merge-train sweep (boards clean on release/v3.8.50 tip)
This commit is contained in:
Jan Leon
2026-08-06 16:07:56 +02:00
committed by GitHub
parent 274514405f
commit 714a315a1a
2 changed files with 32 additions and 61 deletions

View File

@@ -8,6 +8,8 @@
* getComboModelsFromData, validateComboDAG, resolveNestedComboModels,
* filterTargetsByRequestCompatibility) are re-exported from combo.ts for the
* ~20 external consumers (chatCore.ts, the /api/combos routes, embeddings, etc.).
* Context-window metadata is advisory: known-fitting targets are ordered first,
* while catalog-too-small targets remain available for runtime fallback.
* No barrel import — depends only on sibling leaves.
*/
@@ -531,9 +533,7 @@ function hasKnownCompatibleContextLimit(
return evaluateContextLimit(capabilities, requirements, target.modelStr) === true;
}
function hasOnlyContextWindowFailures(reasons: string[]): boolean {
return reasons.length > 0 && reasons.every((reason) => reason === "context_window");
}
const HARD_COMPAT_REASONS = new Set(["tools", "vision", "structured_output", "output_tokens"]);
/**
* #8332: vision is a hard requirement, not a soft preference — a target whose vision
@@ -694,67 +694,34 @@ export function filterTargetsByRequestCompatibility(
if (!needsFiltering) return targets;
const rejected: Array<{ target: ResolvedComboTarget; reasons: string[] }> = [];
const compatible = targets.filter((target) => {
const targetReasons = new Map<ResolvedComboTarget, string[]>();
for (const target of targets) {
const reasons = getTargetCompatibilityFailures(target, requirements);
if (reasons.length === 0) return true;
rejected.push({ target, reasons });
return false;
});
targetReasons.set(target, reasons);
if (reasons.length > 0) rejected.push({ target, reasons });
}
// Unknown context limits are safe only as a fallback. If this request already
// filtered at least one known-too-small target and known-good targets remain,
// prefer the known-good set over unknown metadata gaps. If no known-good
// context target remains, fall back to the strategy order for context-only
// candidates instead of letting unknown metadata be the only survivors.
const rejectedForContextWindow = rejected.some((entry) =>
// Context metadata is advisory. Keep every target that has no hard capability
// mismatch, but prefer targets whose known limit fits. A stale catalog entry must
// never remove the only target that could accept the request at runtime.
const compatible = targets.filter((target) => {
const reasons = targetReasons.get(target) || [];
return !reasons.some((reason) => HARD_COMPAT_REASONS.has(reason));
});
const hadKnownTooSmallContextTarget = rejected.some((entry) =>
entry.reasons.includes("context_window")
);
if (requirements.requiredContextTokens > 0 && rejectedForContextWindow) {
if (
requirements.requiredContextTokens > 0 &&
hadKnownTooSmallContextTarget &&
compatible.length > 1
) {
const knownContextCompatible = compatible.filter((target) =>
hasKnownCompatibleContextLimit(target, requirements)
);
if (knownContextCompatible.length > 0 && knownContextCompatible.length < compatible.length) {
const knownContextCompatibleTargets = new Set(knownContextCompatible);
for (const target of compatible) {
if (!knownContextCompatibleTargets.has(target)) {
rejected.push({ target, reasons: ["context_window_unknown"] });
}
}
log.info(
"COMBO",
`${label}: kept ${knownContextCompatible.length}/${targets.length} targets for request requirements`
);
log.debug?.(
"COMBO",
`${label}: rejected targets ${rejected
.map((entry) => `${entry.target.modelStr}(${entry.reasons.join("+")})`)
.join(", ")}`
);
return knownContextCompatible;
}
if (knownContextCompatible.length === 0 && compatible.length > 0) {
const rejectedByTarget = new Map(rejected.map((entry) => [entry.target, entry.reasons]));
const contextOnlyFallback = targets.filter((target) => {
const reasons = rejectedByTarget.get(target);
return !reasons || hasOnlyContextWindowFailures(reasons);
});
if (contextOnlyFallback.length > compatible.length) {
log.warn(
"COMBO",
`${label}: no known-compatible context target remains; preserving strategy order for context-only candidates`
);
log.debug?.(
"COMBO",
`${label}: rejected targets ${rejected
.map((entry) => `${entry.target.modelStr}(${entry.reasons.join("+")})`)
.join(", ")}`
);
return contextOnlyFallback;
}
const knownSet = new Set(knownContextCompatible);
return [...knownContextCompatible, ...compatible.filter((target) => !knownSet.has(target))];
}
}

View File

@@ -5,9 +5,8 @@ import os from "node:os";
import path from "node:path";
// Regression tests for the context-aware combo compatibility filter.
// Unknown context metadata is only safe as a fallback. Once the context filter
// has rejected known-too-small targets and a known-capacity target remains,
// unknown-context targets must not survive over it.
// Context metadata is advisory: known-fitting targets are preferred, while
// unknown and catalog-too-small targets remain available for runtime fallback.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-context-filter-"));
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
@@ -97,7 +96,7 @@ function bigContextBody(tokens: number) {
const noopLog = { info() {}, warn() {}, error() {}, debug() {} };
test("known compatible context target wins over unknown-context targets", () => {
test("known compatible context target is preferred while unknown targets remain fallback", () => {
saveModelsDevCapabilities({
"unit-known-context": {
tiny: capabilityEntry(8_000),
@@ -118,7 +117,12 @@ test("known compatible context target wins over unknown-context targets", () =>
assert.deepEqual(
out.map((entry) => entry.modelStr),
["unit-known-context/million"]
[
"unit-known-context/million",
"unit-unknown-context/mystery-a",
"unit-known-context/tiny",
"unit-unknown-context/mystery-b",
]
);
});