zeroclaw-labs/zeroclaw · error
OpenAI TTS API error ({}): {}
Error message
OpenAI TTS API error ({}): {} What it means
Raised by OpenAiTtsProvider::synthesize when the POST to the configured TTS endpoint returns a non-2xx status. The endpoint defaults to https://api.openai.com/v1/audio/speech and can be overridden via [providers.tts.openai.<alias>].uri to target any OpenAI-compatible backend (Groq, Azure, proxies). The provider parses the JSON error body and bails with the HTTP status plus the upstream error.message ("unknown error" when the body has no such field).
Source
Thrown at crates/zeroclaw-channels/src/tts.rs:142
let resp = self
.client
.post(&self.base_url)
.bearer_auth(&self.api_key)
.json(&body)
.send()
.await
.context("Failed to send OpenAI TTS request")?;
let status = resp.status();
if !status.is_success() {
let error_body: serde_json::Value = resp
.json()
.await
.unwrap_or_else(|_| serde_json::json!({"error": "unknown"}));
let msg = error_body["error"]["message"]
.as_str()
.unwrap_or("unknown error");
bail!("OpenAI TTS API error ({}): {}", status, msg);
}
let bytes = resp
.bytes()
.await
.context("Failed to read OpenAI TTS response body")?;
Ok(bytes.to_vec())
}
fn supported_voices(&self) -> Vec<String> {
["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
.iter()
.map(|s| (*s).to_string())
.collect()
}
fn supported_formats(&self) -> Vec<String> {
["mp3", "opus", "aac", "flac", "wav", "pcm"]View on GitHub (pinned to 88bb9c8533)
Solutions
- Match the status in the message: 401 means the api_key under [providers.tts.openai.<alias>].api_key (env grammar ZEROCLAW_providers__tts__openai__<alias>__api_key) is wrong or empty; set a valid key and restart.
- For 429/403, check the OpenAI organization quota and billing, then retry after the rate window.
- For 400 naming the voice or model, set model (tts-1/tts-1-hd/gpt-4o-mini-tts) and voice (alloy/echo/fable/onyx/nova/shimmer) to supported values.
- For 400 on response_format, switch response_format to mp3 or wav, or pick a model that supports opus.
- If uri is overridden, confirm the backend implements POST /v1/audio/speech with the same JSON contract before blaming the key.
Example fix
# before — voice is not an OpenAI voice, backend rejects the request [providers.tts.openai.main] api_key = "sk-..." voice = "Sarah" response_format = "opus" # after [providers.tts.openai.main] api_key = "sk-..." voice = "shimmer" # alloy | echo | fable | onyx | nova | shimmer response_format = "mp3" # broadest compatibility
Defensive patterns
Strategy: retry
Validate before calling
// Fail fast on obviously broken OpenAI TTS config before first synthesis.
fn validate_openai_tts(cfg: &TtsProviderConfig) -> anyhow::Result<()> {
let key = cfg.api_key.as_deref().map(str::trim).filter(|k| !k.is_empty());
anyhow::ensure!(key.is_some(), "api_key missing under [providers.tts.openai.<alias>]");
if let Some(fmt) = cfg.response_format.as_deref() {
const OK: &[&str] = &["mp3", "opus", "aac", "flac", "wav", "pcm"];
anyhow::ensure!(OK.contains(&fmt), "unsupported response_format {fmt}");
}
Ok(())
} Try / catch
match mgr.synthesize(text).await {
Ok(audio) => { /* send */ }
Err(err) => {
let msg = err.to_string();
if msg.contains("OpenAI TTS API error (401)") {
return Err(err.context("fix api_key in [providers.tts.openai.<alias>]")); // not retryable
}
if msg.contains("(429)") || msg.contains("(5") {
tokio::time::sleep(Duration::from_secs(5)).await; // exponential backoff in production
return mgr.synthesize(text).await;
}
Err(err)
}
} Prevention
- Keep [providers.tts.openai.<alias>].api_key (or the ZEROCLAW_providers__tts__openai__<alias>__api_key env slot) in a secrets manager; the V0.8.0 removal of the OPENAI_API_KEY fallback means the env var alone silently stops working.
- Pin voice to one of alloy/echo/fable/onyx/nova/shimmer and model to a current TTS model.
- When overriding uri, smoke-test the backend with curl POST /v1/audio/speech before wiring it in.
- Monitor 429s: OpenAI TTS rate limits are per-org; alert before quota exhaustion.
When it happens
Trigger: POST /v1/audio/speech with Bearer auth fails: 401 for a missing/invalid api_key, 429/403 for quota or billing exhaustion, 400 for a voice outside {alloy, echo, fable, onyx, nova, shimmer} or an unknown model (default tts-1), 400 when response_format (default opus) is not supported by the chosen model, and any non-2xx from a custom uri backend that is not fully OpenAI-TTS compatible.
Common situations: The OPENAI_API_KEY env fallback was removed in V0.8.0, so old deployments that relied on the env var now send a stale or empty key from [providers.tts.openai.<alias>].api_key. Pointing uri at a proxy or Groq often breaks on the opus response_format or on the voice list. Expired billing or a rotated key also lands here.
Related errors
- ElevenLabs TTS API error ({}): {}
- Google TTS API error ({}): {}
- WeChat QR code fetch failed ({$status}): {$body}
- Failed to initiate call: {}
- Edge TTS binary_path must be a bare command name without pat
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/ac8cc2e6d84b023d.
Report an issue: GitHub.