moeru-ai/airi · error · TtsUpstreamResponseError

TtsUpstreamResponseError (rethrown upstream response body wi

Error message

TtsUpstreamResponseError (rethrown upstream response body with upstream status/headers)

What it means

`TtsUpstreamResponseError` is the adapter's normalized wrapper for a non-OK response returned by the upstream TTS provider reached through the unspeech SDK. `sendSpeechViaUnSpeech` catches `UnSpeechAPIError` and rethrows it as this error carrying a reconstructed `Response` with the upstream status, body, and headers, so the router can apply its HTTP 500 fallback policy while preserving the upstream's actual error payload.

Source

Thrown at server/apps/api/src/services/adapters/tts/unspeech.ts:76

      speed,
      voice,
      abortSignal: ctx.abortSignal,
      extraBody,
    })

    return {
      contentType: result.contentType ?? fallbackContentType,
      body: result.body,
    }
  }
  catch (error) {
    // Keep abort identity intact so the router can apply `onTimeout`
    // independently from HTTP 500 fallback policy.
    if (ctx.abortSignal?.aborted)
      throw error

    if (error instanceof UnSpeechAPIError) {
      throw new TtsUpstreamResponseError(new Response(error.responseBody, {
        status: error.status,
        headers: error.responseHeaders,
      }))
    }

    throw createInternalError(`${providerLabel} tts fetch failed: ${errorMessageFrom(error) ?? 'unknown'}`)
  }
}

interface ListVoicesOptions {
  ctx: TtsVoiceCatalogContext
  query: string
  providerLabel: string
}

/**
 * Lists unspeech voices and maps SDK failures into AIRI gateway errors.
 *

View on GitHub (pinned to f679616c34)

Solutions

  1. Inspect the wrapped Response's status and body to read the upstream's own error message, then fix the matching cause (key, voice, model).
  2. Verify the upstream API key stored in LLM router config is valid and not expired.
  3. Check that the requested voice/model exist in the provider catalog (list voices via the adapter's /audio/voices path).
  4. If status is 429 or 5xx, retry after backoff; the router may fall back to the next upstream in the chain.
  5. Confirm `ctx.unspeechBaseURL` points to the correct unspeech-compatible endpoint.

Example fix

null
Defensive patterns

Strategy: try-catch

Validate before calling

// validate upstream config before dispatch
if (!ctx.keyPlaintext?.length) throw createInternalError('missing upstream TTS API key')
if (!ctx.unspeechBaseURL) throw createInternalError('missing unspeech base URL')

Type guard

function isTtsUpstreamResponseError(e: unknown): e is TtsUpstreamResponseError { return e instanceof TtsUpstreamResponseError }

Try / catch

try {
  const result = await sendSpeechViaUnSpeech(options)
} catch (error) {
  if (error instanceof TtsUpstreamResponseError) {
    const status = error.response.status
    if (status === 429 || status >= 500) return retryWithNextUpstream(error)
    return new Response(error.response.body, { status, headers: error.response.headers })
  }
  throw error
}

Prevention

When it happens

Trigger: The unspeech `generateSpeechResponse` call receives an HTTP error response from the upstream provider: invalid/expired upstream API key, unknown model or voice id for that provider, provider-side quota/rate limit, or malformed `extraBody` rejected by the upstream.

Common situations: Encrypted upstream key (`ctx.keyPlaintext`) is wrong or revoked; requested voice does not exist for the configured provider/region; upstream provider outage returning 4xx/5xx; base URL misconfigured so requests hit the wrong upstream endpoint.

Related errors


AI-assisted analysis of moeru-ai/airi@f679616c34 (2026-09-08). Data as JSON: /api/errors/eaacbc306196eb89. Report an issue: GitHub.