From 250423a0e96030ed195b3844f30c7b387bdc87db Mon Sep 17 00:00:00 2001 From: Chirag Singhal Date: Tue, 7 Jul 2026 10:31:50 +0530 Subject: [PATCH 1/2] fix(providers): fail fast on empty auto-combo pool instead of 15s timeout (#6458) --- src/sse/handlers/chatHelpers.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/sse/handlers/chatHelpers.ts b/src/sse/handlers/chatHelpers.ts index 87eb47a3894..1db399f4fc3 100644 --- a/src/sse/handlers/chatHelpers.ts +++ b/src/sse/handlers/chatHelpers.ts @@ -204,10 +204,20 @@ export async function resolveModelOrError( try { const virtualCombo = await createBuiltinAutoCombo(modelStr, suffix); + const poolSize = virtualCombo.candidatePool?.length || 0; log.info( "AUTO", - `"auto" provider → built-in virtual combo "${modelStr}" (${virtualCombo.candidatePool?.length || 0} candidates)` + `"auto" provider → built-in virtual combo "${modelStr}" (${poolSize} candidates)` ); + // #6458: fail fast instead of leaking a silent 15s upstream timeout when + // the category/tier filter (e.g. auto/coding:pro, auto/reasoning) matches + // zero connected candidates. An empty virtual combo has no targets to + // dispatch to, so downstream combo routing stalls on an empty set. + if (poolSize === 0) { + const msg = `No connected providers match '${modelStr}'. Connect a provider whose models satisfy this category/tier, or use a different auto combo.`; + log.warn("AUTO", msg, { model: modelStr }); + return { error: errorResponse(HTTP_STATUS.SERVICE_UNAVAILABLE, msg) }; + } return { combo: virtualCombo, provider: "auto", model: suffix }; } catch (err) { log.warn("CHAT", `Failed to create built-in auto combo "${modelStr}"`, { err }); From 4417d8307f801a245c00a2958faa4dbc2e6f2f39 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Tue, 7 Jul 2026 19:59:22 -0300 Subject: [PATCH 2/2] =?UTF-8?q?test(sse):=20regression=20guard=20for=20#64?= =?UTF-8?q?58=20empty=20auto-pool=20fast-fail=20503=20+=20rebaseline=20cha?= =?UTF-8?q?tHelpers=20=E2=80=94=20owner-approved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/quality/file-size-baseline.json | 5 +-- .../auto-empty-pool-fastfail-6458.test.ts | 31 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 tests/unit/auto-empty-pool-fastfail-6458.test.ts diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index d9a132e8e65..a9d8b908833 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -259,7 +259,7 @@ "src/shared/validation/schemas.ts": 2523, "_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791993 (+4 = Zed hosted native-app sign-in modal branch). Cohesive UI growth for the zed-hosted OAuth provider; not extractable. The prior 6118 comment set the note but left the frozen value at 989.", "_rebaseline_2026_07_06_6351_glm_team_quota": "PR #6351 own growth (GLM team-plan quota fields threaded through the connection modals; new GlmTeamQuotaFields.tsx extracted): AddApiKeyModal.tsx ->951 (+9), EditConnectionModal.tsx ->1277 (+18). Absorbs the pre-existing session base-red on these frozen modals; release captain rebaseline-at-release supersedes.", "_rebaseline_2026_07_06_6499_unique_default_name": "PR #6499 own growth: AddApiKeyModal.tsx 952->959 (+7 = a unique default connection name so a second API key for the same provider does not reuse 'main' and trigger the backend name-based upsert that silently overwrote the first connection). The pure name derivation was extracted to computeConnectionDefaultName.ts (unit-tested) to keep the growth minimal; the contributor's original full-form-reset rewrite was trimmed to a spread reset to avoid dropping the GLM team-quota fields #6351 added and to hold the frozen god-file growth down. Release captain rebaseline-at-release supersedes.", - "_rebaseline_2026_07_07_6523_chirag_cooldown_body": "PR #6523 (@chirag127, #6460) own growth: chatHelpers.ts 860->866 (+6 = retryAfterAt/credentialsCoolingCount fields on modelCooldownResponse) and auth.ts 2447->2448 (+1 = connectionsCount threaded through no-credentials fallback). Owner-approved rebaseline (file-size cap for contributor PR). Frozen (cannot grow further); release captain's rebaseline-at-release supersedes." + "_rebaseline_2026_07_07_6523_chirag_cooldown_body": "PR #6523 (@chirag127, #6460) own growth: chatHelpers.ts 860->866 (+6 = retryAfterAt/credentialsCoolingCount fields on modelCooldownResponse) and auth.ts 2447->2448 (+1 = connectionsCount threaded through no-credentials fallback). Owner-approved rebaseline (file-size cap for contributor PR). Frozen (cannot grow further); release captain's rebaseline-at-release supersedes.", + "_rebaseline_2026_07_07_6546_chirag_failfast_pool": "PR #6546 (@chirag127, #6458) own growth: chatHelpers.ts ->876 (fail-fast 503 on empty auto-combo pool instead of a silent 15s upstream timeout). Owner-approved rebaseline. Frozen. (stacks on #6523's chatHelpers growth this cycle)" } \ No newline at end of file diff --git a/tests/unit/auto-empty-pool-fastfail-6458.test.ts b/tests/unit/auto-empty-pool-fastfail-6458.test.ts new file mode 100644 index 00000000000..4db304bab9e --- /dev/null +++ b/tests/unit/auto-empty-pool-fastfail-6458.test.ts @@ -0,0 +1,31 @@ +/** + * #6458 — an `auto/*` category/tier combo that matches ZERO connected + * candidates must fail fast with a clear 503, instead of building an empty + * virtual combo whose downstream routing stalls on a silent ~15s upstream + * timeout. Regression guard (Rule #18). + */ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-auto-empty-pool-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const { resolveModelOrError } = await import("../../src/sse/handlers/chatHelpers.ts"); + +test.beforeEach(() => core.resetDbInstance()); +test.after(() => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#6458 empty auto-combo pool returns a 503 instead of an empty combo", async () => { + // No provider connections seeded → any auto category resolves to an empty pool. + const result = await resolveModelOrError("auto/coding:pro", { messages: [{ role: "user", content: "hi" }] }); + assert.ok(result.error, "expected an error result, not a combo"); + assert.equal(result.error.status, 503, "empty auto pool must fail fast with 503"); + assert.equal(result.combo, undefined, "must not return a combo for an empty pool"); +});