zeroclaw-labs/zeroclaw · error

Transcription API error ({}): {}

Error message

Transcription API error ({}): {}

What it means

parse_whisper_response() is the shared parser for all Whisper-compatible endpoints — Groq, OpenAI Whisper, and local_whisper (whisper.cpp server / LocalAI). It bails when the HTTP status is non-2xx, embedding the status and the trimmed response body. Which provider produced it is identified by the 'Transcription ...' context in the surrounding error chain, not the message itself.

Source

Thrown at crates/zeroclaw-channels/src/transcription.rs:891

        let resp = req
            .multipart(Form::new().part("file", file_part))
            .timeout(std::time::Duration::from_secs(self.timeout_secs))
            .send()
            .await
            .context("Failed to send audio to local Whisper endpoint")?;

        parse_whisper_response(resp).await
    }
}

// ── Shared response parsing ─────────────────────────────────────

async fn parse_whisper_response(resp: reqwest::Response) -> Result<String> {
    let status = resp.status();
    if !status.is_success() {
        let body = resp.text().await.unwrap_or_default();
        bail!("Transcription API error ({}): {}", status, body.trim());
    }

    let body: serde_json::Value = resp
        .json()
        .await
        .context("Failed to parse transcription response")?;

    let text = body["text"]
        .as_str()
        .context("Transcription response missing 'text' field")?
        .to_string();

    Ok(text)
}

// ── TranscriptionManager ────────────────────────────────────────

/// Manages multiple transcription / STT providers and routes transcription

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read {status} and the body text: 401 -> fix the api_key for the selected provider ([transcription] for Groq, [transcription.openai], [transcription.local_whisper])
  2. 404 on local_whisper -> set api_url to the complete endpoint, e.g. http://localhost:9000/v1/audio/transcriptions
  3. 429 (usually Groq) -> back off and retry; stagger concurrent transcriptions
  4. 400 on a local server -> confirm the model is loaded and the audio format is one the server build accepts

Example fix

# before: base URL only -> 404 on local whisper.cpp server
[transcription.local_whisper]
api_url = "http://localhost:9000"

# after: full endpoint path
[transcription.local_whisper]
api_url = "http://localhost:9000/v1/audio/transcriptions"
Defensive patterns

Strategy: try-catch

Try / catch

match provider.transcribe(&audio, name).await {
    Ok(text) => Ok(text),
    Err(e) => {
        let msg = e.to_string();
        if msg.contains("429") {
            retry_with_backoff(|| provider.transcribe(&audio, name)).await // Groq throttling
        } else if msg.contains("404") {
            Err(anyhow!("local_whisper api_url must be the full /v1/audio/transcriptions path"))
        } else {
            Err(e) // 401: fix api_key; 400: model/format issue on the server
        }
    }
}

Prevention

When it happens

Trigger: POST <api_url>/audio/transcriptions returns non-success: Groq 429 rate limit (very common on the free tier) or 401 invalid key; OpenAI 401 invalid api_key or 429; local_whisper 404 when api_url doesn't point at the full endpoint path (must include /v1/audio/transcriptions), 400 from a model not loaded, or connection-level errors surfacing as 502 from a proxy.

Common situations: Groq free-tier bursts of voice notes hitting RPM/TPD limits; whisper.cpp server started without --convert or missing the model so it 400s; LocalAI model not preloaded; api_url configured as a base URL (http://localhost:9000) instead of the full path; OpenAI key rotated.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/c2c1817e42c7a8cd. Report an issue: GitHub.