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
- 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
- Log the body text included in the message for the exact Gemini error detail
- Verify billing and quota in Google AI Studio or Cloud Console
- 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
- Validate the API key before invoking synthesis
- Pin voice names to the documented Gemini set
- Cache quota state and back off on 429
- Surface response body text in logs — it names the exact field at fault
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
- Gemini TTS request failed
- MiMo TTS request failed
- audio voices upstream
- audio voices upstream returned malformed body
- Failed to fetch voices
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)