Thread Anthropic errors into LanguageModelKnownError (#33261)

This PR is in preparation for doing automatic retries for certain
errors, e.g. Overloaded. It doesn't change behavior yet (aside from some
granularity of error messages shown to the user), but rather mostly
changes some error handling to be exhaustive enum matches instead of
`anyhow` downcasts, and leaves some comments for where the behavior
change will be in a future PR.

Release Notes:

- N/A
This commit is contained in:
Richard Feldman 2025-06-23 14:48:26 -04:00 committed by GitHub
parent aabfea4c10
commit c610ebfb03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 283 additions and 123 deletions

View file

@ -1,10 +1,11 @@
use std::io;
use std::str::FromStr;
use std::time::Duration;
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use http_client::http::{HeaderMap, HeaderValue};
use http_client::http::{self, HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
@ -336,7 +337,7 @@ pub async fn complete(
let uri = format!("{api_url}/v1/messages");
let beta_headers = Model::from_id(&request.model)
.map(|model| model.beta_headers())
.unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
@ -346,39 +347,30 @@ pub async fn complete(
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.context("failed to construct request body")?;
.map_err(AnthropicError::BuildRequestBody)?;
let mut response = client
.send(request)
.await
.context("failed to send request to Anthropic")?;
if response.status().is_success() {
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let response_message: Response =
serde_json::from_slice(&body).context("failed to deserialize response body")?;
Ok(response_message)
.map_err(AnthropicError::HttpSend)?;
let status = response.status();
let mut body = String::new();
response
.body_mut()
.read_to_string(&mut body)
.await
.map_err(AnthropicError::ReadResponse)?;
if status.is_success() {
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
} else {
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("failed to read response body")?;
let body_str =
std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
Err(AnthropicError::Other(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str
)))
Err(AnthropicError::HttpResponseError {
status: status.as_u16(),
body,
})
}
}
@ -491,7 +483,7 @@ pub async fn stream_completion_with_rate_limit_info(
let uri = format!("{api_url}/v1/messages");
let beta_headers = Model::from_id(&request.base.model)
.map(|model| model.beta_headers())
.unwrap_or_else(|_err| Model::DEFAULT_BETA_HEADERS.join(","));
.unwrap_or_else(|_| Model::DEFAULT_BETA_HEADERS.join(","));
let request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(uri)
@ -500,15 +492,15 @@ pub async fn stream_completion_with_rate_limit_info(
.header("X-Api-Key", api_key)
.header("Content-Type", "application/json");
let serialized_request =
serde_json::to_string(&request).context("failed to serialize request")?;
serde_json::to_string(&request).map_err(AnthropicError::SerializeRequest)?;
let request = request_builder
.body(AsyncBody::from(serialized_request))
.context("failed to construct request body")?;
.map_err(AnthropicError::BuildRequestBody)?;
let mut response = client
.send(request)
.await
.context("failed to send request to Anthropic")?;
.map_err(AnthropicError::HttpSend)?;
let rate_limits = RateLimitInfo::from_headers(response.headers());
if response.status().is_success() {
let reader = BufReader::new(response.into_body());
@ -520,37 +512,31 @@ pub async fn stream_completion_with_rate_limit_info(
let line = line.strip_prefix("data: ")?;
match serde_json::from_str(line) {
Ok(response) => Some(Ok(response)),
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
Err(error) => Some(Err(AnthropicError::DeserializeResponse(error))),
}
}
Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))),
Err(error) => Some(Err(AnthropicError::ReadResponse(error))),
}
})
.boxed();
Ok((stream, Some(rate_limits)))
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit(retry_after))
Err(AnthropicError::RateLimit { retry_after })
} else {
let mut body = Vec::new();
let mut body = String::new();
response
.body_mut()
.read_to_end(&mut body)
.read_to_string(&mut body)
.await
.context("failed to read response body")?;
.map_err(AnthropicError::ReadResponse)?;
let body_str =
std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?;
match serde_json::from_str::<Event>(body_str) {
match serde_json::from_str::<Event>(&body) {
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
Ok(_) => Err(AnthropicError::Other(anyhow!(
"Unexpected success response while expecting an error: '{body_str}'",
))),
Err(_) => Err(AnthropicError::Other(anyhow!(
"Failed to connect to API: {} {}",
response.status(),
body_str,
))),
Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
Err(_) => Err(AnthropicError::HttpResponseError {
status: response.status().as_u16(),
body: body,
}),
}
}
}
@ -797,17 +783,38 @@ pub struct MessageDelta {
pub stop_sequence: Option<String>,
}
#[derive(Error, Debug)]
#[derive(Debug)]
pub enum AnthropicError {
#[error("rate limit exceeded, retry after {0:?}")]
RateLimit(Duration),
#[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
/// Failed to serialize the HTTP request body to JSON
SerializeRequest(serde_json::Error),
/// Failed to construct the HTTP request body
BuildRequestBody(http::Error),
/// Failed to send the HTTP request
HttpSend(anyhow::Error),
/// Failed to deserialize the response from JSON
DeserializeResponse(serde_json::Error),
/// Failed to read from response stream
ReadResponse(io::Error),
/// HTTP error response from the API
HttpResponseError { status: u16, body: String },
/// Rate limit exceeded
RateLimit { retry_after: Duration },
/// API returned an error response
ApiError(ApiError),
#[error("{0}")]
Other(#[from] anyhow::Error),
/// Unexpected response format
UnexpectedResponseFormat(String),
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Error)]
#[error("Anthropic API Error: {error_type}: {message}")]
pub struct ApiError {
#[serde(rename = "type")]
pub error_type: String,