conductor-oss/conductor · error · IOException

Anthropic Messages API failed with status %d: %s

Error message

Anthropic Messages API failed with status %d: %s

What it means

AnthropicMessagesApi.createMessage() throws this IOException when the HTTP response status is not 2xx, after exhausting the automatic temperature-removal retry (which fires only on HTTP 400 responses whose body contains 'temperature'). The message includes the raw HTTP status code and the full response body, so the Anthropic API's own error JSON is visible. This is the single choke-point for all API-level rejections from the Anthropic Messages API.

Source

Thrown at ai/src/main/java/org/conductoross/conductor/ai/providers/anthropic/api/AnthropicMessagesApi.java:97

                        .header("anthropic-version", anthropicVersion)
                        .header("Content-Type", "application/json")
                        .post(RequestBody.create(jsonBody, JSON));

        // Add beta header if request has beta features
        if (request.betaFeatures() != null && !request.betaFeatures().isEmpty()) {
            httpBuilder.header("anthropic-beta", String.join(",", request.betaFeatures()));
        }

        try (Response response = httpClient.newCall(httpBuilder.build()).execute()) {
            String responseBody = readBody(response);
            if (!response.isSuccessful()) {
                // Newer Anthropic models deprecate temperature — retry once without it.
                if (response.code() == 400
                        && responseBody.contains("temperature")
                        && request.temperature() != null) {
                    return createMessage(request.withoutTemperature());
                }
                throw new IOException(
                        "Anthropic Messages API failed with status %d: %s"
                                .formatted(response.code(), responseBody));
            }
            log.debug("Anthropic Messages API response: {}", responseBody);
            return objectMapper.readValue(responseBody, MessagesResponse.class);
        }
    }

    private String readBody(Response response) throws IOException {
        ResponseBody body = response.body();
        return body != null ? body.string() : "";
    }

    // -- Request DTOs --

    @JsonInclude(JsonInclude.Include.NON_NULL)
    public record MessagesRequest(
            String model,

View on GitHub (pinned to cf7c3e4a8a)

Solutions

  1. Read the status code (first %d) and response body (second %s) from the exception message to identify the specific API rejection.
  2. 401/403: verify the API key is valid and the account is active in the Anthropic console.
  3. 429: implement exponential backoff retry at the caller level, or reduce request frequency.
  4. 400 with model error: verify the model name exists and is accessible to your API key tier.
  5. 400 with thinking error: for Opus 4.7+ use adaptive thinking (AnthropicChatModel handles this via requiresAdaptiveThinking), for older models use enabled thinking.
  6. Ensure max_tokens is always set — the code defaults to 8192 but a caller can override with null/0.

Example fix

// before — no retry on rate limit
try {
    return chatModel.call(prompt);
} catch (RuntimeException e) {
    throw e;
}

// after — retry on 429 with backoff
for (int attempt = 0; attempt < 3; attempt++) {
    try {
        return chatModel.call(prompt);
    } catch (RuntimeException e) {
        if (e.getCause() instanceof IOException io
                && io.getMessage().contains("status 429")
                && attempt < 2) {
            Thread.sleep((long) Math.pow(2, attempt) * 1000);
            continue;
        }
        throw e;
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate required fields before building the Anthropic request
void validateRequest(MessagesRequest.Builder builder) {
    if (builder.maxTokens == null || builder.maxTokens <= 0) {
        builder.maxTokens(8192); // Anthropic requires max_tokens
    }
    if (builder.model == null || builder.model.isBlank()) {
        throw new IllegalArgumentException("Model name is required");
    }
}

Try / catch

try {
    return messagesApi.createMessage(request);
} catch (IOException e) {
    String msg = e.getMessage();
    if (msg.contains("status 401")) {
        throw new SecurityException("Invalid Anthropic API key", e);
    } else if (msg.contains("status 429")) {
        throw new RateLimitException("Anthropic rate limit exceeded", e);
    } else if (msg.contains("status 5")) {
        throw new ServiceUnavailableException("Anthropic server error: " + msg, e);
    }
    throw e;
}

Prevention

When it happens

Trigger: HTTP 401 (invalid or missing x-api-key), 403 (forbidden — region blocked or key suspended), 429 (rate limit / quota exceeded), 500 or 529 (Anthropic server-side overload), 400 with non-temperature validation errors such as an unknown model name, missing required max_tokens field, or a thinking config shape incompatible with the target model (e.g. legacy 'thinking.type=enabled' sent to Opus 4.7+).

Common situations: Using a model name that doesn't exist or has been deprecated. Sending a thinking configuration that the model line rejects (Opus 4.7 requires adaptive thinking; Sonnet/Haiku require enabled). API key expired or revoked. Burst traffic hitting the per-minute token rate limit. max_tokens omitted or set to 0 (Anthropic requires it).

Related errors


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