moeru-ai/airi · error · Error

Gemini TTS request failed

Error message

Gemini TTS request failed: ${response.status} ${await response.text().catch(() => '')}

What it means

The fetch to the Gemini TTS endpoint returned a non-OK status; the message embeds the status code and the response body text (empty string if reading the body throws). This is the provider's only HTTP-error surface for speech synthesis, and the status carries the diagnosis: 400 for bad voice/model/temperature, 401/403 for key problems, 429 for quota.

Solutions

  1. Map the embedded status: 401/403 re-enter the API key in provider settings; 429 wait or raise quota; 400 check voice and model values
  2. Log the body text included in the message for the exact Gemini error detail
  3. Verify billing and quota in Google AI Studio or Cloud Console
  4. Retry with the default voice 'Kore' and no temperature override to isolate parameter issues

Example fix

// before
voice: 'alloy' // OpenAI voice name → 400 Bad Request

// after
voice: 'Kore' // Gemini prebuilt voice, e.g. Kore, Puck, Aoede
Defensive patterns

Strategy: try-catch

Validate before calling

if (!config.apiKey?.trim())
  throw new Error('Gemini API key is required') // fail fast before the fetch

Try / catch

try {
  const audio = await geminiSpeech(body)
}
catch (err) {
  const [, status] = err.message.match(/Gemini TTS request failed: (\d+)/) ?? []
  if (status === '401' || status === '403') promptForApiKey()
  else if (status === '429') scheduleRetry(backoff)
}

Prevention

When it happens

Trigger: Invalid or revoked API key (401/403); project or free-tier quota exhausted (429); voiceName not supported by the model (400); wrong model id for audio output; billing disabled on the project.

Common situations: Key rotated but provider settings still hold the old one; RPM exceeded on the free tier; passing an OpenAI-style voice name where Gemini expects names like Kore; region-restricted key.

Understand the failure class

Background: "API request failed": what wrapped HTTP errors from external APIs mean and how to find the real cause — this error's family across 29 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/bbf050b8028db98d. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-ui/src/libs/providers/providers/google-gemini-audio-speech/index.ts:99

    if (!body.model)
      throw new Error('Missing model for Gemini TTS')

    const response = await globalThis.fetch(new URL(`models/${body.model}:generateContent`, baseUrl), {
      method: 'POST',
      headers: { 'x-goog-api-key': apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contents: [{ parts: [{ text: body.input }] }],
        generationConfig: {
          responseModalities: ['AUDIO'],
          speechConfig: {
            voiceConfig: { prebuiltVoiceConfig: { voiceName: body.voice || 'Kore' } },
          },
          ...(body.temperature !== undefined ? { temperature: body.temperature } : {}),
        },
      }),
    })
    if (!response.ok)
      throw new Error(`Gemini TTS request failed: ${response.status} ${await response.text().catch(() => '')}`)

    const data = await response.json() as {
      candidates?: Array<{ content?: { parts?: Array<{ inlineData?: { data?: string } }> } }>
    }
    const audio = data.candidates?.[0]?.content?.parts?.find(part => part.inlineData)?.inlineData?.data
    if (!audio)
      throw new Error('Gemini TTS response missing audio data')

    return new Response(toWavFromPCM16(decodeBase64(audio), 24000), {
      status: 200,
      headers: { 'Content-Type': 'audio/wav' },
    })
  }
}

export const providerGoogleGeminiAudioSpeech = defineProvider<GoogleGeminiSpeechConfig>({
  id: 'google-gemini-audio-speech',
  name: 'Google Gemini',

View on GitHub (pinned to 677329427f)