| @@ -1,12 +1,14 @@ |
| 1 | 1 | mod supported_countries; |
| 2 | 2 | |
| 3 | | −use anyhow::{anyhow, Result}; |
| 3 | +use anyhow::{anyhow, Context, Result}; |
| 4 | 4 | use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; |
| 5 | 5 | use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; |
| 6 | 6 | use isahc::config::Configurable; |
| 7 | 7 | use serde::{Deserialize, Serialize}; |
| 8 | +use std::str::FromStr; |
| 8 | 9 | use std::time::Duration; |
| 9 | | −use strum::EnumIter; |
| 10 | +use strum::{EnumIter, EnumString}; |
| 11 | +use thiserror::Error; |
| 10 | 12 | |
| 11 | 13 | pub use supported_countries::*; |
| 12 | 14 | |
| @@ -96,7 +98,7 @@ pub async fn complete( · 83 unmodified lines |
| 96 | 98 | api_url: &str, |
| 97 | 99 | api_key: &str, |
| 98 | 100 | request: Request, |
| 99 | | −) -> Result<Response> { |
| 101 | +) -> Result<Response, AnthropicError> { |
| 100 | 102 | let uri = format!("{api_url}/v1/messages"); |
| 101 | 103 | let request_builder = HttpRequest::builder() |
| 102 | 104 | .method(Method::POST) |
| @@ -106,24 +108,40 @@ pub async fn complete( · 3 unmodified lines |
| 106 | 108 | .header("X-Api-Key", api_key) |
| 107 | 109 | .header("Content-Type", "application/json"); |
| 108 | 110 | |
| 109 | | − let serialized_request = serde_json::to_string(&request)?; |
| 110 | | − let request = request_builder.body(AsyncBody::from(serialized_request))?; |
| 111 | + let serialized_request = |
| 112 | + serde_json::to_string(&request).context("failed to serialize request")?; |
| 113 | + let request = request_builder |
| 114 | + .body(AsyncBody::from(serialized_request)) |
| 115 | + .context("failed to construct request body")?; |
| 111 | 116 | |
| 112 | | − let mut response = client.send(request).await?; |
| 117 | + let mut response = client |
| 118 | + .send(request) |
| 119 | + .await |
| 120 | + .context("failed to send request to Anthropic")?; |
| 113 | 121 | if response.status().is_success() { |
| 114 | 122 | let mut body = Vec::new(); |
| 115 | | − response.body_mut().read_to_end(&mut body).await?; |
| 116 | | − let response_message: Response = serde_json::from_slice(&body)?; |
| 123 | + response |
| 124 | + .body_mut() |
| 125 | + .read_to_end(&mut body) |
| 126 | + .await |
| 127 | + .context("failed to read response body")?; |
| 128 | + let response_message: Response = |
| 129 | + serde_json::from_slice(&body).context("failed to deserialize response body")?; |
| 117 | 130 | Ok(response_message) |
| 118 | 131 | } else { |
| 119 | 132 | let mut body = Vec::new(); |
| 120 | | − response.body_mut().read_to_end(&mut body).await?; |
| 121 | | − let body_str = std::str::from_utf8(&body)?; |
| 122 | | − Err(anyhow!( |
| 133 | + response |
| 134 | + .body_mut() |
| 135 | + .read_to_end(&mut body) |
| 136 | + .await |
| 137 | + .context("failed to read response body")?; |
| 138 | + let body_str = |
| 139 | + std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?; |
| 140 | + Err(AnthropicError::Other(anyhow!( |
| 123 | 141 | "Failed to connect to API: {} {}", |
| 124 | 142 | response.status(), |
| 125 | 143 | body_str |
| 126 | | − )) |
| 144 | + ))) |
| 127 | 145 | } |
| 128 | 146 | } |
| 129 | 147 | |
| @@ -133,7 +151,7 @@ pub async fn stream_completion( · 3 unmodified lines |
| 133 | 151 | api_key: &str, |
| 134 | 152 | request: Request, |
| 135 | 153 | low_speed_timeout: Option<Duration>, |
| 136 | | −) -> Result<BoxStream<'static, Result<Event>>> { |
| 154 | +) -> Result<BoxStream<'static, Result<Event, AnthropicError>>, AnthropicError> { |
| 137 | 155 | let request = StreamingRequest { |
| 138 | 156 | base: request, |
| 139 | 157 | stream: true, |
| @@ -149,10 +167,16 @@ pub async fn stream_completion( · 9 unmodified lines |
| 149 | 167 | if let Some(low_speed_timeout) = low_speed_timeout { |
| 150 | 168 | request_builder = request_builder.low_speed_timeout(100, low_speed_timeout); |
| 151 | 169 | } |
| 152 | | − let serialized_request = serde_json::to_string(&request)?; |
| 153 | | − let request = request_builder.body(AsyncBody::from(serialized_request))?; |
| 154 | | − |
| 155 | | − let mut response = client.send(request).await?; |
| 170 | + let serialized_request = |
| 171 | + serde_json::to_string(&request).context("failed to serialize request")?; |
| 172 | + let request = request_builder |
| 173 | + .body(AsyncBody::from(serialized_request)) |
| 174 | + .context("failed to construct request body")?; |
| 175 | + |
| 176 | + let mut response = client |
| 177 | + .send(request) |
| 178 | + .await |
| 179 | + .context("failed to send request to Anthropic")?; |
| 156 | 180 | if response.status().is_success() { |
| 157 | 181 | let reader = BufReader::new(response.into_body()); |
| 158 | 182 | Ok(reader |
| @@ -163,36 +187,41 @@ pub async fn stream_completion( · 4 unmodified lines |
| 163 | 187 | let line = line.strip_prefix("data: ")?; |
| 164 | 188 | match serde_json::from_str(line) { |
| 165 | 189 | Ok(response) => Some(Ok(response)), |
| 166 | | − Err(error) => Some(Err(anyhow!(error))), |
| 190 | + Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))), |
| 167 | 191 | } |
| 168 | 192 | } |
| 169 | | − Err(error) => Some(Err(anyhow!(error))), |
| 193 | + Err(error) => Some(Err(AnthropicError::Other(anyhow!(error)))), |
| 170 | 194 | } |
| 171 | 195 | }) |
| 172 | 196 | .boxed()) |
| 173 | 197 | } else { |
| 174 | 198 | let mut body = Vec::new(); |
| 175 | | − response.body_mut().read_to_end(&mut body).await?; |
| 199 | + response |
| 200 | + .body_mut() |
| 201 | + .read_to_end(&mut body) |
| 202 | + .await |
| 203 | + .context("failed to read response body")?; |
| 176 | 204 | |
| 177 | | − let body_str = std::str::from_utf8(&body)?; |
| 205 | + let body_str = |
| 206 | + std::str::from_utf8(&body).context("failed to parse response body as UTF-8")?; |
| 178 | 207 | |
| 179 | 208 | match serde_json::from_str::<Event>(body_str) { |
| 180 | | − Ok(Event::Error { error }) => Err(api_error_to_err(error)), |
| 181 | | − Ok(_) => Err(anyhow!( |
| 209 | + Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)), |
| 210 | + Ok(_) => Err(AnthropicError::Other(anyhow!( |
| 182 | 211 | "Unexpected success response while expecting an error: '{body_str}'", |
| 183 | | − )), |
| 184 | | − Err(_) => Err(anyhow!( |
| 212 | + ))), |
| 213 | + Err(_) => Err(AnthropicError::Other(anyhow!( |
| 185 | 214 | "Failed to connect to API: {} {}", |
| 186 | 215 | response.status(), |
| 187 | 216 | body_str, |
| 188 | | − )), |
| 217 | + ))), |
| 189 | 218 | } |
| 190 | 219 | } |
| 191 | 220 | } |
| 192 | 221 | |
| 193 | 222 | pub fn extract_text_from_events( |
| 194 | | − response: impl Stream<Item = Result<Event>>, |
| 195 | | −) -> impl Stream<Item = Result<String>> { |
| 223 | + response: impl Stream<Item = Result<Event, AnthropicError>>, |
| 224 | +) -> impl Stream<Item = Result<String, AnthropicError>> { |
| 196 | 225 | response.filter_map(|response| async move { |
| 197 | 226 | match response { |
| 198 | 227 | Ok(response) => match response { |
| @@ -204,7 +233,7 @@ pub fn extract_text_from_events( · 5 unmodified lines |
| 204 | 233 | ContentDelta::TextDelta { text } => Some(Ok(text)), |
| 205 | 234 | _ => None, |
| 206 | 235 | }, |
| 207 | | − Event::Error { error } => Some(Err(api_error_to_err(error))), |
| 236 | + Event::Error { error } => Some(Err(AnthropicError::ApiError(error))), |
| 208 | 237 | _ => None, |
| 209 | 238 | }, |
| 210 | 239 | Err(error) => Some(Err(error)), |
| @@ -212,15 +241,6 @@ pub fn extract_text_from_events( · 1 unmodified line |
| 212 | 241 | }) |
| 213 | 242 | } |
| 214 | 243 | |
| 215 | | −fn api_error_to_err( |
| 216 | | − ApiError { |
| 217 | | − error_type, |
| 218 | | − message, |
| 219 | | − }: ApiError, |
| 220 | | −) -> anyhow::Error { |
| 221 | | − anyhow!("API error. Type: '{error_type}', message: '{message}'",) |
| 222 | | −} |
| 223 | | − |
| 224 | 244 | #[derive(Debug, Serialize, Deserialize)] |
| 225 | 245 | pub struct Message { |
| 226 | 246 | pub role: Role, |
| @@ -374,9 +394,53 @@ pub struct MessageDelta { · 147 unmodified lines |
| 374 | 394 | pub stop_sequence: Option<String>, |
| 375 | 395 | } |
| 376 | 396 | |
| 397 | +#[derive(Error, Debug)] |
| 398 | +pub enum AnthropicError { |
| 399 | + #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)] |
| 400 | + ApiError(ApiError), |
| 401 | + #[error("{0}")] |
| 402 | + Other(#[from] anyhow::Error), |
| 403 | +} |
| 404 | + |
| 377 | 405 | #[derive(Debug, Serialize, Deserialize)] |
| 378 | 406 | pub struct ApiError { |
| 379 | 407 | #[serde(rename = "type")] |
| 380 | 408 | pub error_type: String, |
| 381 | 409 | pub message: String, |
| 382 | 410 | } |
| 411 | + |
| 412 | +/// An Anthropic API error code. |
| 413 | +/// https://docs.anthropic.com/en/api/errors#http-errors |
| 414 | +#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString)] |
| 415 | +#[strum(serialize_all = "snake_case")] |
| 416 | +pub enum ApiErrorCode { |
| 417 | + /// 400 - `invalid_request_error`: There was an issue with the format or content of your request. |
| 418 | + InvalidRequestError, |
| 419 | + /// 401 - `authentication_error`: There's an issue with your API key. |
| 420 | + AuthenticationError, |
| 421 | + /// 403 - `permission_error`: Your API key does not have permission to use the specified resource. |
| 422 | + PermissionError, |
| 423 | + /// 404 - `not_found_error`: The requested resource was not found. |
| 424 | + NotFoundError, |
| 425 | + /// 413 - `request_too_large`: Request exceeds the maximum allowed number of bytes. |
| 426 | + RequestTooLarge, |
| 427 | + /// 429 - `rate_limit_error`: Your account has hit a rate limit. |
| 428 | + RateLimitError, |
| 429 | + /// 500 - `api_error`: An unexpected error has occurred internal to Anthropic's systems. |
| 430 | + ApiError, |
| 431 | + /// 529 - `overloaded_error`: Anthropic's API is temporarily overloaded. |
| 432 | + OverloadedError, |
| 433 | +} |
| 434 | + |
| 435 | +impl ApiError { |
| 436 | + pub fn code(&self) -> Option<ApiErrorCode> { |
| 437 | + ApiErrorCode::from_str(&self.error_type).ok() |
| 438 | + } |
| 439 | + |
| 440 | + pub fn is_rate_limit_error(&self) -> bool { |
| 441 | + match self.error_type.as_str() { |
| 442 | + "rate_limit_error" => true, |
| 443 | + _ => false, |
| 444 | + } |
| 445 | + } |
| 446 | +} |