decolua/9router · error · Error

OpenRouter TTS failed: ${res.status}

Error message

OpenRouter TTS failed: ${res.status}

What it means

Thrown when OpenRouter's TTS (chat-completions-with-audio) endpoint returns a non-2xx HTTP status. Like the OpenAI variant it prefers the JSON error.message from the body and falls back to a status-code template. Any upstream rejection (auth, model access, moderation, quota) surfaces here.

Source

Thrown at open-sse/handlers/ttsProviders/openrouter.js:45

    const res = await fetch(TTS_CFG.baseUrl, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Authorization": `Bearer ${credentials.apiKey}`,
        ...(TTS_CFG.headers || {}),
      },
      body: JSON.stringify({
        model: ttsModel,
        modalities: ["text", "audio"],
        audio: { voice, format: "wav" },
        stream: true,
        messages: [{ role: "user", content: text }],
      }),
    });

    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err?.error?.message || `OpenRouter TTS failed: ${res.status}`);
    }

    // Parse SSE stream, accumulate base64 audio chunks
    const chunks = [];
    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split("\n");
      buffer = lines.pop();
      for (const line of lines) {
        if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
        try {
          const json = JSON.parse(line.slice(6));

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Read the status code: 401 -> fix key, 402 -> add OpenRouter credits, 404/400 -> check model slug supports audio output
  2. Verify the model ID in the TTS config exists on OpenRouter and your account can use it
  3. Check openrouter.ai activity page for rejected-request details
  4. Retry with backoff if 429 or 5xx
  5. Confirm the request body shape still matches OpenRouter's audio-modality API if versions changed
Defensive patterns

Strategy: retry

Validate before calling

if (!credentials?.apiKey) throw new Error("OpenRouter TTS: missing API key");
if (typeof text !== "string" || text.length === 0) throw new Error("OpenRouter TTS: text required");

Type guard

function isOpenRouterHttpError(e) {
  return e instanceof Error && /^OpenRouter TTS failed: \d{3}$/.test(e.message);
}

Try / catch

try {
  return await openrouterTts.synthesize(text, model, creds);
} catch (e) {
  const m = e.message.match(/OpenRouter TTS failed: (\d{3})/);
  if (m && ["429", "500", "502", "503", "504"].includes(m[1])) return retryWithBackoff(() => openrouterTts.synthesize(text, model, creds));
  throw e;
}

Prevention

When it happens

Trigger: Non-OK response from OpenRouter chat completions with audio modality: 401 bad key, 404 model not found/not permitted, 402 insufficient credits, 429 rate limited, 5xx from the upstream provider OpenRouter proxies to.

Common situations: Free-tier OpenRouter account lacking access to the TTS model, credits exhausted, model slug misspelled (e.g. openai/gpt-4o-mini-tts unavailable), or OpenRouter returning HTML error pages that fail JSON parsing.

Related errors


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