diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 2b09178737..b2556848b1 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -955,6 +955,7 @@ async function handleComboChatInner({ // Surface a recovery hint + auto-clear the session pin after enough consecutive // no-target failures (silent-stop fix). Threshold of 3 prevents a one-off account // wipe from destroying the prompt-cache pin benefit on the next request. + recordComboFailure(effectiveSessionId, combo.name); // #11371: same early-exit release as the pinned-turn path above. targetResolution.quotaShareRelease?.(); return errorResponseWithComboDiagnostics( diff --git a/tests/unit/combo-routing-engine.test.ts b/tests/unit/combo-routing-engine.test.ts index bcf8f5dc0d..3818199a6b 100644 --- a/tests/unit/combo-routing-engine.test.ts +++ b/tests/unit/combo-routing-engine.test.ts @@ -16,6 +16,9 @@ const { handleComboChat, } = await import("../../open-sse/services/combo.ts"); const { resolveComboTargets } = await import("../../open-sse/services/combo/comboStructure.ts"); +const { getComboFailureCount, __resetComboFailureTrackerForTests } = await import( + "../../open-sse/services/combo/failureTracker.ts" +); const { applyPromptCacheAffinity } = await import("../../open-sse/services/combo/promptCacheAffinity.ts"); const { resolveReasoningBufferedMaxTokens } = @@ -1327,6 +1330,43 @@ test("handleComboChat returns 404 model_not_found when a combo has no executable assert.match(payload.error.message, /Combo has no executable targets/); }); +test("#11408 guard: no-executable-targets early exit still records the combo failure (silent-stop counter, #5923)", async () => { + __resetComboFailureTrackerForTests(); + const result = await handleComboChat({ + body: {}, + combo: { + name: "guard-empty-11408", + strategy: "priority", + models: [], + context_cache_protection: true, + }, + handleSingleModel: async () => { + throw new Error("handleSingleModel should not run for empty combos"); + }, + isModelAvailable: async () => true, + log: createLog(), + settings: { + comboDefaults: { + maxRetries: 0, + retryDelayMs: 1, + }, + }, + relayOptions: { sessionId: "sess-guard-11408" } as any, + allCombos: null, + }); + + const payload = (await result.json()) as any; + assert.equal(result.status, 404); + // The quota-share slot release must not replace the #5923 silent-stop + // bookkeeping: the consecutive-failure counter must still advance so the + // session pin auto-clears after COMBO_FAILURE_THRESHOLD no-target failures. + assert.equal( + getComboFailureCount("sess-guard-11408", "guard-empty-11408"), + 1, + "recordComboFailure must run on the no-executable-targets early exit" + ); +}); + test("handleComboChat round-robin returns 404 when no models are configured", async () => { const result = await handleComboChat({ body: {},