Files
OmniRoute/open-sse/services/combo/comboAttemptLoop.ts
Ravi Tharuma d6a61074dc fix(quota): align AUTH window labels with usage API and clarify 503 (#12884)
Um `ALL_TARGETS_SKIPPED` 503 que não diz qual janela esgotou é opaco justamente no momento em que o operador mais precisa saber. Alinhar os rótulos de janela AUTH com os da API de uso fecha a outra metade: dois nomes para a mesma coisa fazem o dashboard e o erro parecerem discordar.

Revalidei sobre o tip: **6/6**, typecheck:core limpo, check-file-size OK.

**Dois consertos meus na sua branch.**

1. `typecheck:core` falhava com `TS2345` em `comboAttemptLoop.ts` (linhas 130 e 416): o `QuotaSkipTarget` declarava `connectionId?: string`, mas o `ResolvedComboTarget` carrega `string | null` para alvo não-pinado. Alarguei para `string | null` no tipo de diagnóstico em vez de estreitar o call site — o módulo só **lê** o campo e a linha 29 já narrowa com `typeof === "string"`, então null não custa nada ali. Isso apareceu porque o `comboAttemptLoop` mudou de forma no #12746/#12811, mergeados nesta mesma campanha depois que você cortou a branch.

2. O `roundRobinCombo.ts` foi de 1198 para 1205 e cruzou o teto de 1200 para arquivo novo. Congelei com justificativa: o arquivo já nasceu em 1198 quando o #12811 o levantou de dentro do `combo.ts`, e os diagnósticos em si vivem no `quotaSkipDiagnostics.ts`, sob o cap. Registrei que a próxima extração natural é o corpo do attempt loop, mas que ele acabou de ser movido e deve assentar antes de ser cortado de novo.
2026-09-10 10:47:11 -03:00

585 lines
27 KiB
TypeScript

/**
* Set-try + speculative dispatch loop for handleComboChatInner.
* Extracted from combo.ts dispatchWithCooldownRetry (#11804 finally lives here).
*
* @internal — not part of the public combo.ts barrel.
*/
import { formatRetryAfter, getModelLockoutInfo } from "../accountFallback.ts";
import {
errorResponse,
errorResponseWithComboDiagnostics,
unavailableResponse,
} from "../../utils/error.ts";
import type { ComboDiagnostics } from "../../utils/error.ts";
import { COMBO_FAILURE_THRESHOLD, recordComboFailure } from "./failureTracker.ts";
import { buildNoUpstreamResponseDiagnostics, buildRecoveryHint } from "./pinRecovery.ts";
import { formatExhaustedConnectionKey } from "./comboDiagFormat.ts";
import { collectQuotaWindowExclusions, formatQuotaSkipMessage } from "./quotaSkipDiagnostics.ts";
import { recordComboRequest } from "../comboMetrics.ts";
import { notifyWebhookEvent } from "../../../src/lib/webhookDispatcher.ts";
import { parseModel } from "../model.ts";
import {
formatComboOutcomes,
buildRedactedSummary,
resolveComboTerminalStatus,
} from "./comboErrorAggregation.ts";
import {
resolveComboCooldownWaitDecision,
resolveCircuitOpenWaitDecision,
type ResolveComboCooldownDecisionResult,
} from "./comboCooldownRetry.ts";
import {
computeClosestRetryAfter,
waitForCooldownAwareRetry,
} from "../../../src/sse/services/cooldownAwareRetry.ts";
import { toRetryAfterDisplayValue } from "./validateQuality.ts";
import { finalizeComboTrace, finishComboTrace } from "./decisionTrace.ts";
import { isRetryAfterEligibleStatus } from "./unavailableRetryGate.ts";
import { withQuotaExhaustionClassification } from "./quotaExhaustion.ts";
import {
COMBO_LOOP_SAFETY_TIMEOUT_MS,
COMBO_SAFETY_DRAIN_MS,
resolveDelayMs,
} from "./comboPredicates.ts";
import { evaluateExecuteTargetGates } from "./executeTargetGates.ts";
import { executeTargetAttempt } from "./executeTargetAttempt.ts";
import type { AttemptLoopDeps, AttemptLoopState, ExecuteTargetResult } from "./attemptLoopTypes.ts";
export type DispatchWithCooldownRetryExtra = {
maxSetRetries: number;
setRetryDelayMs: number;
comboTimeoutMs: number;
comboStartTime: number;
comboCooldownWaitEnabled: boolean;
comboCooldownAttempt: { current: number };
comboCooldownBudgetLeftMs: { current: number };
evaluateGates: typeof evaluateExecuteTargetGates;
executeAttempt: typeof executeTargetAttempt;
};
export async function dispatchWithCooldownRetry(opts: {
state: AttemptLoopState;
deps: AttemptLoopDeps;
extra: DispatchWithCooldownRetryExtra;
}): Promise<Response> {
const { state, deps, extra } = opts;
// #7360: persist lastStatus/earliestRetryAfter across set retries; reset
// only on a fresh dispatch (including cooldown-aware re-dispatch).
state.lastError = null;
state.earliestRetryAfter = null;
state.lastStatus = null;
state.skippedForCircuitOpen = false;
state.earliestCircuitOpenRetryMs = 0;
// #11804: the loop-safety timer is armed per setTry iteration but must be
// cleared on EVERY exit path, not just the happy one. Hoisted to function
// scope so the `finally` at the end of this function always reaches it.
let activeLoopSafetyTimer: ReturnType<typeof setTimeout> | null = null;
try {
for (let setTry = 0; setTry <= extra.maxSetRetries; setTry++) {
// #1731: Per-set-iteration set of providers whose quota is fully exhausted.
// Reset each retry so providers excluded in a previous attempt get another chance.
state.exhaustedProviders = new Set<string>();
state.exhaustedConnections = new Set<string>();
state.transientRateLimitedProviders = new Set<string>();
state.skippedForCircuitOpen = false;
state.earliestCircuitOpenRetryMs = 0;
if (setTry > 0) {
deps.log.info(
"COMBO",
`All targets failed — retrying set (${setTry}/${extra.maxSetRetries})`
);
await new Promise((resolve) => {
const timer = setTimeout(resolve, extra.setRetryDelayMs);
deps.signal?.addEventListener(
"abort",
() => {
clearTimeout(timer);
resolve(undefined);
},
{ once: true }
);
});
if (deps.signal?.aborted) {
deps.log.info("COMBO", "Client disconnected during set retry delay — aborting");
return errorResponse(499, "Client disconnected");
}
}
deps.startTime = Date.now();
state.fallbackCount = 0;
state.recordedAttempts = 0;
state.comboErrors = [];
// QA P0: assemble a sanitized diagnostic trace from the state already in scope
// (pool size + this set-try's exhausted providers/connections + attempt order +
// a terminal-reason code). Never touches keys/tokens — provider/model ids only.
// Silent-stop fix: include a `recovery` hint (action verb + human next-step) so the
// OC plugin + non-header-aware clients can render an actionable error instead of an
// opaque 5xx. The optional `retryAfterSeconds` carries the upstream Retry-After hint.
const buildComboDiag = (
terminalReason: string,
retryAfterSeconds?: number
): ComboDiagnostics => ({
poolSize: state.orderedTargets.length,
attempted: state.recordedAttempts,
excluded: [
...[...state.exhaustedProviders].map((p) => ({ provider: p, reason: "exhausted" })),
...[...state.exhaustedConnections].map((c) => formatExhaustedConnectionKey(String(c))),
...(terminalReason === "all_targets_skipped"
? collectQuotaWindowExclusions(state.orderedTargets)
: []),
],
attemptOrder: state.comboAttemptOrder,
terminalReason,
recovery: buildRecoveryHint(terminalReason, retryAfterSeconds),
});
let globalResolve: ((res: Response) => void) | null = null;
const globalPromise = new Promise<Response>((res) => {
globalResolve = res;
});
// G1 (silent-stop fix): the speculative loop's `Promise.race` waits on
// `globalPromise`, which is ONLY resolved from inside a task (success or
// fatal error). If a target hangs — e.g. the operator disabled the per-model
// timeout (`targetTimeoutMs: 0`) and the upstream never settles — the race
// never resolves and the request hangs forever with no response. This safety
// promise force-resolves after the combo budget (extra.comboTimeoutMs when set,
// otherwise a hard ceiling) so the request ALWAYS terminates with an
// actionable 504 instead of dying silently. `state.comboExpired` is flipped so the
// target loop stops launching new work; the existing state.comboExpired branch
// returns the aggregated 504.
const loopSafetyMs =
extra.comboTimeoutMs > 0 ? extra.comboTimeoutMs : COMBO_LOOP_SAFETY_TIMEOUT_MS;
let loopSafetyFired = false;
let loopSafetyTimer: ReturnType<typeof setTimeout> | null = null;
const loopSafetyPromise = new Promise<Response>((resolve) => {
loopSafetyTimer = setTimeout(() => {
loopSafetyFired = true;
deps.log.warn(
"COMBO",
`Combo loop safety timeout (${loopSafetyMs}ms) reached without a terminal response — force-terminating`
);
resolve(
errorResponseWithComboDiagnostics(
504,
`Combo global timeout (${loopSafetyMs}ms) without a terminal response`,
buildComboDiag("combo_timeout"),
{ code: "COMBO_TIMEOUT", type: "server_error" }
)
);
}, loopSafetyMs);
loopSafetyTimer.unref?.();
activeLoopSafetyTimer = loopSafetyTimer;
});
const runningTasks = new Set<Promise<void>>();
let anySuccess = false;
// #10681: steps already recorded as dispatched (so per-target retries do not
// duplicate the decision).
state.dispatchedTargets = new Set<string>();
// G1: flip state.comboExpired as soon as the safety timer fires so the next loop
// iteration breaks instead of launching more targets after the budget, and
// abort every in-flight target so a hung upstream actually gets cancelled
// (not just "response stops").
const markLoopExpiredIfSafetyFired = () => {
if (loopSafetyFired) {
state.comboExpired = true;
for (const [, ac] of state.abortControllers.entries()) ac.abort();
}
};
state.abortControllers = new Map<number, AbortController>();
const zeroLatencyOptimizationsEnabled = deps.config.zeroLatencyOptimizationsEnabled === true;
const hasProtectedPriorityTarget =
deps.strategy === "priority" &&
state.orderedTargets.some((target) => target.fallbackOnlyOnQuotaExhaustion === true);
const executeTarget = async (i: number): Promise<ExecuteTargetResult> => {
const gate = await extra.evaluateGates({ index: i, state, deps });
if (gate.kind === "skip") return gate.result;
return extra.executeAttempt({
index: i,
state,
deps,
targetForAttempt: gate.targetForAttempt,
profile: gate.profile,
protectedPriorityTarget: gate.protectedPriorityTarget,
});
};
for (let i = 0; i < state.orderedTargets.length; i++) {
if (anySuccess || state.comboExpired) break;
const abortController = new AbortController();
state.abortControllers.set(i, abortController);
const onClientAbort = () => abortController.abort();
deps.signal?.addEventListener("abort", onClientAbort);
const task = (async () => {
try {
const res = await executeTarget(i);
if (res && !anySuccess) {
if (res.ok) {
anySuccess = true;
globalResolve!(res.response!);
for (const [idx, ac] of state.abortControllers.entries()) {
if (idx !== i) ac.abort();
}
} else if (res.response) {
// Fatal error, abort combo
anySuccess = true;
globalResolve!(res.response);
}
}
} finally {
deps.signal?.removeEventListener("abort", onClientAbort);
}
})().catch((err) => {
const logError = deps.log.error ?? deps.log.warn;
logError("COMBO", `Speculative task error for target ${i}`, err);
// G2 (silent-stop fix): never leave the speculative loop waiting on an
// unresolved globalPromise. If a task throws unexpectedly (outside
// executeTarget's error handling) and no other task succeeds, the post-loop
// `Promise.race([globalPromise, ...])` would hang forever. Resolve with a
// 502 so the request terminates with an actionable error.
if (!anySuccess && globalResolve) {
anySuccess = true;
globalResolve(errorResponse(502, `Combo target ${i} failed with an unexpected error`));
}
});
runningTasks.add(task);
task.finally(() => runningTasks.delete(task));
if (
zeroLatencyOptimizationsEnabled &&
deps.config.hedging &&
!hasProtectedPriorityTarget &&
i + 1 < state.orderedTargets.length
) {
const hedgeDelay = resolveDelayMs(deps.config.hedgeDelayMs, 500);
const timeoutPromise = new Promise<void>((r) => {
setTimeout(r, hedgeDelay);
});
await Promise.race([task, globalPromise, timeoutPromise, loopSafetyPromise]);
} else {
await Promise.race([task, globalPromise, loopSafetyPromise]);
}
markLoopExpiredIfSafetyFired();
// Global combo timeout check: after each target completes, stop trying
// further targets if the total elapsed time exceeds extra.comboTimeoutMs.
if (
!anySuccess &&
extra.comboTimeoutMs > 0 &&
Date.now() - extra.comboStartTime >= extra.comboTimeoutMs
) {
state.comboExpired = true;
deps.log.info(
"COMBO",
`Combo global timeout (${extra.comboTimeoutMs}ms) reached after ` +
`${i + 1}/${state.orderedTargets.length} targets (${state.recordedAttempts} attempted) — stopping`
);
}
}
if (!anySuccess && runningTasks.size > 0) {
// G1: include loopSafetyPromise so a hung last task (per-model timeout
// disabled) cannot freeze this post-loop race forever.
await Promise.race([globalPromise, Promise.all([...runningTasks]), loopSafetyPromise]);
markLoopExpiredIfSafetyFired();
}
// G1: if the safety timer won the race (request would otherwise hang), give
// in-flight tasks a short drain window to land their per-model errors into
// state.comboErrors so the 504 carries the same "tried: a (500)" summary the
// regular state.comboExpired branch produces — then return the safety 504.
if (loopSafetyFired && !anySuccess) {
if (runningTasks.size > 0) {
await Promise.race([
Promise.allSettled([...runningTasks]),
new Promise((resolve) => setTimeout(resolve, COMBO_SAFETY_DRAIN_MS)),
]);
}
const summary = state.comboErrors
.slice(0, 5)
.map((e) => `${e.model} (${e.status})`)
.join(", ");
const msg =
`Combo global timeout (${loopSafetyMs}ms) after ${state.recordedAttempts}/${state.orderedTargets.length} targets` +
(state.comboErrors.length > 0
? ` | tried: ${summary}${state.comboErrors.length > 5 ? `... (+${state.comboErrors.length - 5})` : ""}`
: "") +
" without a terminal response";
return errorResponseWithComboDiagnostics(504, msg, buildComboDiag("combo_timeout"), {
code: "COMBO_TIMEOUT",
type: "server_error",
});
}
// #10681: finalize the decision trace (success).
finalizeComboTrace(deps.traceInvocationId, state.orderedTargets);
finishComboTrace(deps.traceInvocationId, { status: 200 });
if (anySuccess) {
// G1: clear the safety timer on the happy path so a successful combo does
// not leave a 10-minute timer alive per request.
if (loopSafetyTimer) {
clearTimeout(loopSafetyTimer);
loopSafetyTimer = null;
}
return await globalPromise;
}
// #10681: finalize the decision trace (global timeout).
finalizeComboTrace(deps.traceInvocationId, state.orderedTargets);
finishComboTrace(deps.traceInvocationId, { status: 504 });
// Global combo timeout: return aggregated error immediately, skipping set retries.
if (state.comboExpired) {
const summary = buildRedactedSummary(state.comboErrors);
const msg =
`Combo global timeout (${extra.comboTimeoutMs}ms) after ${state.recordedAttempts}/${state.orderedTargets.length} targets` +
(state.comboErrors.length > 0 ? ` | tried: ${summary}` : "");
const latencyMs = Date.now() - deps.startTime;
if (state.recordedAttempts === 0) {
recordComboRequest(deps.combo.name, null, {
success: false,
latencyMs,
fallbackCount: state.fallbackCount,
strategy: deps.strategy,
});
}
notifyWebhookEvent("request.failed", {
combo: deps.combo.name,
reason: "COMBO_TIMEOUT",
latencyMs,
fallbackCount: state.fallbackCount,
});
return errorResponseWithComboDiagnostics(504, msg, buildComboDiag("combo_timeout"), {
code: "COMBO_TIMEOUT",
type: "server_error",
});
}
// All models failed in this set try
const latencyMs = Date.now() - deps.startTime;
if (state.recordedAttempts === 0) {
recordComboRequest(deps.combo.name, null, {
success: false,
latencyMs,
fallbackCount: state.fallbackCount,
strategy: deps.strategy,
});
}
// Retry the entire set if more attempts remain
if (setTry < extra.maxSetRetries) continue;
if (!state.lastStatus && state.recordedAttempts === 0 && extra.comboCooldownWaitEnabled) {
const circuitOpenWait = resolveCircuitOpenWaitDecision({
skippedForCircuitOpen: state.skippedForCircuitOpen,
retryAfterMs: state.earliestCircuitOpenRetryMs,
attempt: extra.comboCooldownAttempt.current,
budgetLeftMs: extra.comboCooldownBudgetLeftMs.current,
settings: deps.resilienceSettings.comboCooldownWait,
});
if (circuitOpenWait.wait) {
deps.log.info(
"COMBO",
`${deps.strategy} circuit-open wait: waiting ${Math.ceil(circuitOpenWait.waitMs / 1000)}s (reason=${circuitOpenWait.reason ?? "circuit_open"}) then retrying (attempt ${extra.comboCooldownAttempt.current + 1}/${deps.resilienceSettings.comboCooldownWait.maxAttempts})`
);
const completed = await waitForCooldownAwareRetry(circuitOpenWait.waitMs, deps.signal);
if (!completed) {
return errorResponse(499, "Request aborted");
}
extra.comboCooldownAttempt.current += 1;
extra.comboCooldownBudgetLeftMs.current = Math.max(
0,
extra.comboCooldownBudgetLeftMs.current - circuitOpenWait.waitMs
);
return dispatchWithCooldownRetry({ state, deps, extra });
}
}
// All set retries exhausted — return the final error
// #10681: finalize the decision trace (all targets failed or skipped).
finalizeComboTrace(deps.traceInvocationId, state.orderedTargets);
finishComboTrace(deps.traceInvocationId, { status: 503 });
if (!state.lastStatus) {
if (state.recordedAttempts === 0) {
notifyWebhookEvent("request.failed", {
combo: deps.combo.name,
reason: "ALL_TARGETS_SKIPPED",
latencyMs,
fallbackCount: state.fallbackCount,
});
const quotaSkip = formatQuotaSkipMessage(
collectQuotaWindowExclusions(state.orderedTargets)
);
return withQuotaExhaustionClassification(
errorResponseWithComboDiagnostics(
503,
quotaSkip
? `Service temporarily unavailable: all targets were skipped by pre-dispatch filters (${quotaSkip})`
: "Service temporarily unavailable: all targets were skipped by pre-dispatch filters",
buildComboDiag("all_targets_skipped"),
{ code: "ALL_TARGETS_SKIPPED", type: "service_unavailable" }
),
state.observedFailure ? state.allObservedFailuresQuota : null
);
}
notifyWebhookEvent("request.failed", {
combo: deps.combo.name,
reason: "ALL_ACCOUNTS_INACTIVE",
latencyMs,
fallbackCount: state.fallbackCount,
});
recordComboFailure(deps.effectiveSessionId, deps.combo.name);
return errorResponseWithComboDiagnostics(
503,
"Service temporarily unavailable: all upstream accounts are inactive",
buildComboDiag("all_accounts_inactive"),
{ code: "ALL_ACCOUNTS_INACTIVE", type: "service_unavailable" }
);
}
// #10501: derive the terminal HTTP status from the structured per-target
// outcomes instead of `state.lastStatus` (whichever target happened to fail
// LAST). A 4xx is preserved only when the request itself is genuinely
// invalid across every eligible target; a heterogeneous mix of failure
// classes (e.g. a quality failure + a sibling's 401) normalizes to a
// 5xx-class status reflecting an infra/provider problem, not a client
// error. See comboErrorAggregation.ts::resolveComboTerminalStatus.
const status = resolveComboTerminalStatus(state.comboErrors, state.lastStatus);
// #10314: build the terminal message from the structured per-target
// outcomes (each distinct class+reason listed separately) instead of
// mashing a single state.lastError with raw `[model (status)]` markers. Connection
// identifiers are redacted. Falls back to state.lastError when no target recorded
// a structured outcome.
const msg =
formatComboOutcomes(state.comboErrors) || state.lastError || "All combo models unavailable";
// Cooldown-aware retry: instead of crystallizing a transient failure, wait
// out a SHORT cooldown and re-run the whole set loop. Guarded by the helper
// (quota_exhausted/auth/not-found excluded, ceiling, attempts, budget).
// MAX_GLOBAL_ATTEMPTS still bounds total dispatches. Available to ALL combo
// strategies when enabled — entry is driven by earliestRetryAfter + the
// real model-lockout reason, NOT by whichever target last overwrote
// `status` (a later 403 must not skip the allow-list check for an earlier
// 429's retry-after hint). SECURITY (see comboCooldownRetry.ts header): the
// allow-list is the PRIMARY barrier and `maxWaitMs` only the SECOND one.
// Hardcoding reason:"rate_limit" would drop the primary barrier and leave
// only the ceiling — which does NOT cover a quota_exhausted lock carrying a
// SHORT upstream retry-after. Model lockouts are recorded for all strategies,
// so the real reason is always available.
if (extra.comboCooldownWaitEnabled && state.earliestRetryAfter) {
const decision: ResolveComboCooldownDecisionResult = resolveComboCooldownWaitDecision({
targets: state.orderedTargets,
earliestRetryAfter: state.earliestRetryAfter,
attempt: extra.comboCooldownAttempt.current,
budgetLeftMs: extra.comboCooldownBudgetLeftMs.current,
settings: deps.resilienceSettings.comboCooldownWait,
// Key each lookup on the TARGET's own model: quota-share combos are
// single-model/multi-account (so this is identical to the previous
// state.orderedTargets[0] behavior), but heterogeneous combos carry a
// different model per target.
lookupLock: (provider, connectionId, target) => {
const rawModel = parseModel(target?.modelStr ?? "").model || "";
if (!rawModel) return null;
return getModelLockoutInfo(provider, connectionId, rawModel);
},
computeWaitMs: (retryAfter) => computeClosestRetryAfter(retryAfter).waitMs,
});
if (decision.wait) {
deps.log.info(
"COMBO",
`${deps.strategy} cooldown wait: ${msg} — waiting ${Math.ceil(
decision.waitMs / 1000
)}s (reason=${decision.reason ?? "?"}) then retrying (attempt ${
extra.comboCooldownAttempt.current + 1
}/${deps.resilienceSettings.comboCooldownWait.maxAttempts})`
);
const completed = await waitForCooldownAwareRetry(decision.waitMs, deps.signal);
if (!completed) {
deps.log.info("COMBO", `${deps.strategy} cooldown wait aborted by client disconnect`);
return errorResponse(499, "Request aborted");
}
extra.comboCooldownAttempt.current += 1;
extra.comboCooldownBudgetLeftMs.current = Math.max(
0,
extra.comboCooldownBudgetLeftMs.current - decision.waitMs
);
return dispatchWithCooldownRetry({ state, deps, extra });
}
}
// #10681: finalize the decision trace with the aggregated terminal status.
finalizeComboTrace(deps.traceInvocationId, state.orderedTargets);
finishComboTrace(deps.traceInvocationId, { status });
// Retry-after decoration is separate from the wait decision above: only
// rate-limit-class final statuses may carry a `(reset after ...)` suffix
// (see unavailableRetryGate.ts — do not stitch a peer target's window onto
// a config-class status like 403/422).
if (state.earliestRetryAfter && isRetryAfterEligibleStatus(status)) {
const retryHuman = formatRetryAfter(toRetryAfterDisplayValue(state.earliestRetryAfter));
deps.log.warn("COMBO", `All models failed | ${msg} (${retryHuman})`);
return withQuotaExhaustionClassification(
unavailableResponse(status, msg, state.earliestRetryAfter, retryHuman),
state.observedFailure ? state.allObservedFailuresQuota : null
);
}
// Silent-stop fix: bump the failure counter (pin clears on 3rd consecutive) and emit
// `try-auto` recovery action via buildRecoveryHint so the OC plugin can show "→ Try
// model: auto" instead of an opaque 5xx. We pass the upstream retry-after seconds to
// the hint so the client can render a precise "wait Ns and retry" message.
deps.log.warn("COMBO", `All models failed | ${msg}`);
const { pinClearedNow } = recordComboFailure(deps.effectiveSessionId, deps.combo.name);
if (pinClearedNow) {
deps.log.info(
"COMBO",
`Auto-cleared session_model_history pin for combo "${deps.combo.name}" after ${COMBO_FAILURE_THRESHOLD} consecutive failures to break the silent-stop loop`
);
}
const retryAfterSeconds = undefined;
// #10966: when every observed failure was independently classified as quota/
// balance exhaustion (isQuotaExhaustionResponse, tracked via state.observeFailure's
// state.allObservedFailuresQuota accumulator), stamp a stable `quota_exhausted`
// terminalReason instead of forwarding the raw upstream error string. The raw
// string falls through buildRecoveryHint's default branch ("retry" / "failed
// transiently"), which is actively misleading for a durable wallet/quota
// exhaustion — retrying the same combo will never refill it.
const terminalReason =
state.observedFailure && state.allObservedFailuresQuota
? "quota_exhausted"
: (state.lastError ?? "all_models_failed");
return withQuotaExhaustionClassification(
errorResponseWithComboDiagnostics(
status,
msg,
buildComboDiag(terminalReason, retryAfterSeconds)
),
state.observedFailure ? state.allObservedFailuresQuota : null
);
}
// Final fallback — when the dispatch returned without crystallizing a status (rare).
// Surface the recovery hint with a generic retry recommendation so the client at least
// gets a non-opaque message instead of "Combo routing completed without an upstream response".
recordComboFailure(deps.effectiveSessionId, deps.combo.name);
return errorResponseWithComboDiagnostics(
503,
"Combo routing completed without an upstream response",
buildNoUpstreamResponseDiagnostics(state.orderedTargets.length)
);
} finally {
// #11804: always release the loop-safety timer. Covering every exit path by
// construction here means a future `return` added to this function cannot
// silently reintroduce the leak.
if (activeLoopSafetyTimer) {
clearTimeout(activeLoopSafetyTimer);
activeLoopSafetyTimer = null;
}
}
}