{"record":{"id":"c2c1817e42c7a8cd","repo":"zeroclaw-labs/zeroclaw","slug":"transcription-api-error","errorCode":null,"errorMessage":"Transcription API error ({}): {}","messagePattern":"Transcription API error \\((.+?)\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/transcription.rs","lineNumber":891,"sourceCode":"\n        let resp = req\n            .multipart(Form::new().part(\"file\", file_part))\n            .timeout(std::time::Duration::from_secs(self.timeout_secs))\n            .send()\n            .await\n            .context(\"Failed to send audio to local Whisper endpoint\")?;\n\n        parse_whisper_response(resp).await\n    }\n}\n\n// ── Shared response parsing ─────────────────────────────────────\n\nasync fn parse_whisper_response(resp: reqwest::Response) -> Result<String> {\n    let status = resp.status();\n    if !status.is_success() {\n        let body = resp.text().await.unwrap_or_default();\n        bail!(\"Transcription API error ({}): {}\", status, body.trim());\n    }\n\n    let body: serde_json::Value = resp\n        .json()\n        .await\n        .context(\"Failed to parse transcription response\")?;\n\n    let text = body[\"text\"]\n        .as_str()\n        .context(\"Transcription response missing 'text' field\")?\n        .to_string();\n\n    Ok(text)\n}\n\n// ── TranscriptionManager ────────────────────────────────────────\n\n/// Manages multiple transcription / STT providers and routes transcription","sourceCodeStart":873,"sourceCodeEnd":909,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/transcription.rs#L873-L909","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Read {status} and the body text: 401 -> fix the api_key for the selected provider ([transcription] for Groq, [transcription.openai], [transcription.local_whisper])","404 on local_whisper -> set api_url to the complete endpoint, e.g. http://localhost:9000/v1/audio/transcriptions","429 (usually Groq) -> back off and retry; stagger concurrent transcriptions","400 on a local server -> confirm the model is loaded and the audio format is one the server build accepts"],"exampleFix":"# before: base URL only -> 404 on local whisper.cpp server\n[transcription.local_whisper]\napi_url = \"http://localhost:9000\"\n\n# after: full endpoint path\n[transcription.local_whisper]\napi_url = \"http://localhost:9000/v1/audio/transcriptions\"","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match provider.transcribe(&audio, name).await {\n    Ok(text) => Ok(text),\n    Err(e) => {\n        let msg = e.to_string();\n        if msg.contains(\"429\") {\n            retry_with_backoff(|| provider.transcribe(&audio, name)).await // Groq throttling\n        } else if msg.contains(\"404\") {\n            Err(anyhow!(\"local_whisper api_url must be the full /v1/audio/transcriptions path\"))\n        } else {\n            Err(e) // 401: fix api_key; 400: model/format issue on the server\n        }\n    }\n}","preventionTips":["Configure api_url as the complete endpoint path (…/v1/audio/transcriptions), never a bare host","Throttle concurrent Groq transcriptions to stay under rate limits","Smoke-test local whisper servers at startup with a 1-second clip to catch 404/400 before real traffic"],"tags":["groq","openai","whisper","localai","http-status","transcription"],"backgroundTag":"speech-to-text-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}