decolua/9router · error

Upstream error (${res.status})

Error message

Upstream error (${res.status})

What it means

throwUpstreamError() normalizes a non-OK upstream HTTP response into a thrown Error. It prefers the provider's structured error message (error.message, message, detail) and falls back to the raw body text, wrapping or replacing the generic 'Upstream error (<status>)' message. Providers like hyperbolic, deepgram, nvidia, huggingface, fishAudio, and inworld call this whenever the TTS endpoint returns a non-2xx status.

Source

Thrown at open-sse/handlers/ttsProviders/_base.js:25

export async function responseToBase64(res, defaultFormat = "mp3") {
  const buf = await res.arrayBuffer();
  if (buf.byteLength < 100) throw new Error("Upstream returned empty audio");
  const ctype = res.headers.get("content-type") || "";
  let format = defaultFormat;
  if (ctype.includes("wav")) format = "wav";
  else if (ctype.includes("mpeg") || ctype.includes("mp3")) format = "mp3";
  else if (ctype.includes("ogg")) format = "ogg";
  return { base64: Buffer.from(buf).toString("base64"), format };
}

export async function throwUpstreamError(res) {
  const text = await res.text().catch(() => "");
  let msg = `Upstream error (${res.status})`;
  try {
    const parsed = JSON.parse(text);
    msg = parsed?.error?.message || parsed?.message || parsed?.detail?.message || (typeof parsed?.detail === "string" ? parsed.detail : null) || text || msg;
  } catch { msg = text || msg; }
  throw new Error(msg);
}

// Parse `model` string as "modelId/voiceId" — match against known model list (longest prefix wins)
export function parseModelVoice(model, defaultModel = "", defaultVoice = "", knownModels = []) {
  if (!model) return { modelId: defaultModel, voiceId: defaultVoice };
  const known = knownModels.map((m) => m.id || m).filter(Boolean).sort((a, b) => b.length - a.length);
  for (const id of known) {
    if (model === id) return { modelId: id, voiceId: defaultVoice };
    if (model.startsWith(`${id}/`)) return { modelId: id, voiceId: model.slice(id.length + 1) };
  }
  const idx = model.lastIndexOf("/");
  if (idx > 0) return { modelId: model.slice(0, idx), voiceId: model.slice(idx + 1) };
  return { modelId: defaultModel || model, voiceId: defaultVoice || model };
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the thrown message — it contains the provider's own error detail and status; fix what it names first
  2. Check the provider API key in the TTS provider config is present, valid, and for the right account
  3. Verify modelId/voiceId against the provider's current catalog
  4. If 429/402, wait for rate-limit reset or top up quota; add retry/backoff for 429 and 5xx
  5. Check the provider status page if the status is 5xx

Example fix

// before
await synthesize({ provider: "deepgram", apiKey: process.env.OLD_KEY });
// after
const apiKey = process.env.DEEPGRAM_API_KEY; // refreshed, valid key
if (!apiKey) throw new Error("DEEPGRAM_API_KEY missing");
await synthesize({ provider: "deepgram", apiKey });
Defensive patterns

Strategy: try-catch

Validate before calling

if (!apiKey) throw new Error("Provider API key missing before TTS call");
if (!knownModels.some(m => m.id === modelId)) throw new Error(`Unknown model ${modelId} for provider`);

Type guard

function isUpstreamHttpError(e) { return e instanceof Error && /^Upstream error \(\d+\)/.test(e.message) || e instanceof Error && /\(\d{3}\)/.test(e.message); }

Try / catch

try {
  await providerSynthesize(req);
} catch (e) {
  const m = e.message.match(/\((\d{3})\)/);
  const status = m ? Number(m[1]) : 0;
  if (status === 429 || status >= 500) return retryWithBackoff(req);
  if (status === 401 || status === 403) throw new Error("Check provider API key: " + e.message);
  throw e;
}

Prevention

When it happens

Trigger: Any TTS call to those providers where res.ok is false: 401 invalid/expired API key, 402/429 quota or rate limit, 404 unknown model or voice id, 400 malformed request body (bad voice settings, text too long), 5xx provider outage.

Common situations: API key rotated or revoked upstream; free tier exhausted (deepgram/nvidia give 429/402); model id removed by provider; voice not available for the chosen model (fishAudio/inworld); regional outage returning 502/503.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/f95a456bd9934f15. Report an issue: GitHub.