conductor-oss/conductor · error · IOException

Speech API failed with status %d: %s

Error message

Speech API failed with status %d: %s

What it means

Thrown by OpenAISpeechApi.createSpeech when the OpenAI Text-to-Speech endpoint (POST /v1/audio/speech) returns a non-2xx HTTP status. It is an IOException whose message embeds the raw response code and the server's error body so the caller can see exactly what OpenAI rejected. The provider supports both public OpenAI and Azure OpenAI auth (api-key vs Authorization: Bearer).

Source

Thrown at ai/src/main/java/org/conductoross/conductor/ai/providers/openai/api/OpenAISpeechApi.java:80

     * @param request Speech request parameters
     * @return Raw audio bytes in the requested format
     */
    public byte[] createSpeech(SpeechRequest request) throws IOException {
        String jsonBody = objectMapper.writeValueAsString(request);

        Request httpRequest =
                new Request.Builder()
                        .url(baseUrl + "/audio/speech")
                        .header(authHeaderName, authHeaderValue)
                        .header("Content-Type", "application/json")
                        .post(RequestBody.create(jsonBody, JSON))
                        .build();

        try (Response response = httpClient.newCall(httpRequest).execute()) {
            if (!response.isSuccessful()) {
                ResponseBody body = response.body();
                String errorBody = body != null ? body.string() : "";
                throw new IOException(
                        "Speech API failed with status %d: %s"
                                .formatted(response.code(), errorBody));
            }
            ResponseBody body = response.body();
            if (body == null) {
                throw new IOException("Speech API returned empty body");
            }
            return body.bytes();
        }
    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    public record SpeechRequest(
            String model,
            String input,
            String voice,
            @JsonProperty("response_format") String responseFormat,
            Double speed) {}

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Read the full message: the %s error body usually contains OpenAI's JSON with an error.message field naming the exact problem.
  2. Verify the API key is valid and has TTS access; for Azure confirm azureAuth=true and the deployment baseUrl.
  3. Confirm the model is a valid TTS model (tts-1, tts-1-hd, gpt-4o-mini-tts) and the voice is one OpenAI exposes.
  4. For 429 responses, add exponential backoff / retry around createSpeech.
  5. Check that baseUrl ends with /v1 so the appended /audio/speech resolves correctly.

Example fix

// before
byte[] audio = speechApi.createSpeech(req); // throws bare IOException

// after
try {
    byte[] audio = speechApi.createSpeech(req);
} catch (IOException e) {
    if (e.getMessage().contains("status 429")) {
        // back off and retry
    } else {
        throw e;
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (speechReq.model() == null || speechReq.model().isBlank()) {
    throw new IllegalArgumentException("SpeechRequest.model must be set to a valid TTS model");
}
if (speechReq.input() == null || speechReq.input().length() > 4096) {
    throw new IllegalArgumentException("input must be 1-4096 characters");
}

Try / catch

try {
    byte[] audio = speechApi.createSpeech(req);
} catch (IOException e) {
    String msg = e.getMessage();
    if (msg.contains("status 429")) {
        // back off and retry
    } else if (msg.contains("status 401")) {
        // credentials issue - do not retry
        throw e;
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling createSpeech with an invalid or expired API key (401), exhausted quota / rate-limited (429), an unsupported model name like a non-tts model (400), a blank/oversized input text exceeding the 4096-character limit (400), or a baseUrl pointing at a path that does not serve /audio/speech (404).

Common situations: Pointing baseUrl at https://api.openai.com instead of .../v1, using azureAuth=true with a plain OpenAI key, typos in the model/voice fields, hitting OpenAI's per-minute token limits under load, or a corporate proxy returning its own 4xx HTML.

Related errors


AI-assisted analysis of conductor-oss/conductor@cf7c3e4a8a (2026-08-14). Data as JSON: /api/errors/abb6f1552434496a. Report an issue: GitHub.