diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index a1b1477618..127fcbb6ae 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -1512,6 +1512,7 @@ export async function handleComboChat({ if (result.ok) { const quality = await validateResponseQuality(result, clientRequestedStream, log); if (!quality.valid) { + const qualityFailureReason = `Upstream response failed quality validation: ${quality.reason}`; log.warn( "COMBO", `Model ${modelStr} returned 200 but failed quality check: ${quality.reason}` @@ -1871,6 +1872,7 @@ async function handleRoundRobinCombo({ if (result.ok) { const quality = await validateResponseQuality(result, clientRequestedStream, log); if (!quality.valid) { + const qualityFailureReason = `Upstream response failed quality validation: ${quality.reason}`; log.warn( "COMBO-RR", `${modelStr} returned 200 but failed quality check: ${quality.reason}` diff --git a/tests/unit/combo-routing-engine.test.ts b/tests/unit/combo-routing-engine.test.ts index 53f34b233f..ca40725d0d 100644 --- a/tests/unit/combo-routing-engine.test.ts +++ b/tests/unit/combo-routing-engine.test.ts @@ -1839,6 +1839,60 @@ test("handleComboChat round-robin recovers from 400s when a later model succeeds assert.deepEqual(calls, ["model-a", "model-b"]); }); +test("handleComboChat single-target quality failure returns explicit quality error instead of ALL_ACCOUNTS_INACTIVE", async () => { + const result = await handleComboChat({ + body: {}, + combo: { + name: "single-target-quality-failure", + strategy: "priority", + models: ["openai/model-a"], + config: { maxRetries: 0 }, + }, + handleSingleModel: async () => + new Response('{"choices":[{"message":{"content":"unterminated"}}', { + status: 200, + headers: { "content-type": "application/json" }, + }), + isModelAvailable: async () => true, + log: createLog(), + settings: null, + relayOptions: null as any, + allCombos: null, + }); + + const payload = (await result.json()) as any; + assert.equal(result.status, 502); + assert.match(payload.error.message, /quality validation/i); + assert.notEqual(payload.error.code, "ALL_ACCOUNTS_INACTIVE"); +}); + +test("handleComboChat round-robin single-target quality failure returns explicit quality error instead of ALL_ACCOUNTS_INACTIVE", async () => { + const result = await handleComboChat({ + body: {}, + combo: { + name: "rr-single-target-quality-failure", + strategy: "round-robin", + models: ["openai/model-a"], + config: { maxRetries: 0, retryDelayMs: 1, concurrencyPerModel: 1, queueTimeoutMs: 5 }, + }, + handleSingleModel: async () => + new Response('{"choices":[{"message":{"content":"unterminated"}}', { + status: 200, + headers: { "content-type": "application/json" }, + }), + isModelAvailable: async () => true, + log: createLog(), + settings: null, + relayOptions: null as any, + allCombos: null, + }); + + const payload = (await result.json()) as any; + assert.equal(result.status, 502); + assert.match(payload.error.message, /quality validation/i); + assert.notEqual(payload.error.code, "ALL_ACCOUNTS_INACTIVE"); +}); + test("handleComboChat falls back to next model when first model returns all-accounts-rate-limited 503", async () => { const calls: any[] = [];