Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions async-openai/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use crate::error::StreamError;
use crate::executor::TowerExecutor;
use crate::{
config::{Config, OpenAIConfig},
error::{map_deserialization_error, ApiError, OpenAIError, WrappedError},
error::{map_deserialization_error, ApiError, ApiErrorResponse, OpenAIError, WrappedError},
executor::{HttpRequestFactory, ReqwestExecutor, SharedExecutor},
traits::AsyncTryFrom,
RequestOptions,
Expand Down Expand Up @@ -732,11 +732,14 @@ async fn read_response(response: Response) -> Result<(Bytes, HeaderMap), OpenAIE
// OpenAI does not guarantee server errors are returned as JSON so we cannot deserialize them.
let message: String = String::from_utf8_lossy(&bytes).into_owned();
tracing::warn!("Server error: {status} - {message}");
return Err(OpenAIError::ApiError(ApiError {
message,
r#type: None,
param: None,
code: None,
return Err(OpenAIError::ApiError(ApiErrorResponse {
status_code: status,
api_error: ApiError {
message,
r#type: None,
param: None,
code: None,
},
}));
}

Expand All @@ -745,7 +748,10 @@ async fn read_response(response: Response) -> Result<(Bytes, HeaderMap), OpenAIE
let wrapped_error: WrappedError = serde_json::from_slice(bytes.as_ref())
.map_err(|e| map_deserialization_error(e, bytes.as_ref()))?;

return Err(OpenAIError::ApiError(wrapped_error.error));
return Err(OpenAIError::ApiError(ApiErrorResponse {
status_code: status,
api_error: wrapped_error.error,
}));
}

Ok((bytes, headers))
Expand Down
25 changes: 23 additions & 2 deletions async-openai/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ pub enum OpenAIError {
/// Underlying error from reqwest library after an API call was made
#[error("http error: {0}")]
Reqwest(#[from] reqwest::Error),
/// OpenAI returns error object with details of API call failure
/// OpenAI returns error object with details of API call failure, along
/// with the HTTP status code from the response.
#[error("{0}")]
ApiError(ApiError),
ApiError(ApiErrorResponse),
/// Error when a response cannot be deserialized into a Rust type
#[error("failed to deserialize api response: error:{0} content:{1}")]
JSONDeserialize(serde_json::Error, String),
Expand Down Expand Up @@ -109,6 +110,26 @@ impl std::fmt::Display for ApiError {

impl std::error::Error for ApiError {}

/// `ApiError` paired with the HTTP status code from the response.
#[cfg(feature = "_api")]
#[derive(Debug, Clone)]
pub struct ApiErrorResponse {
/// HTTP status code
pub status_code: reqwest::StatusCode,
/// Parsed error from response
pub api_error: ApiError,
Comment thread
64bit marked this conversation as resolved.
}
Comment thread
64bit marked this conversation as resolved.

#[cfg(feature = "_api")]
impl std::fmt::Display for ApiErrorResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}", self.status_code, self.api_error)
}
}

#[cfg(feature = "_api")]
impl std::error::Error for ApiErrorResponse {}

/// Wrapper to deserialize the error object nested in "error" JSON key
#[derive(Debug, Deserialize, Serialize)]
pub struct WrappedError {
Expand Down
13 changes: 10 additions & 3 deletions async-openai/src/middleware/retry/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{future::Future, pin::Pin, time::Duration};
use reqwest::{header::HeaderMap, Response};

use crate::{
error::{OpenAIError, WrappedError},
error::{ApiErrorResponse, OpenAIError, WrappedError},
executor::HttpRequestFactory,
};

Expand Down Expand Up @@ -165,6 +165,7 @@ where
let (final_result, headers, retry_after) = match result {
Ok(response) if response.status().is_success() => return Ok(response),
Ok(response) if response.status().as_u16() == 429 => {
let status_code = response.status();
let headers = response.headers().clone();
let retry_after = retry_after(&headers);
let bytes = match response.bytes().await {
Expand All @@ -177,10 +178,16 @@ where
// 429 and insufficient_quota are treated as permanent error.
// https://developers.openai.com/api/docs/guides/error-codes
if wrapped_error.error.r#type.as_deref() == Some(INSUFFICIENT_QUOTA) {
return Err(OpenAIError::ApiError(wrapped_error.error));
return Err(OpenAIError::ApiError(ApiErrorResponse {
status_code,
api_error: wrapped_error.error,
}));
}

OpenAIError::ApiError(wrapped_error.error)
OpenAIError::ApiError(ApiErrorResponse {
status_code,
api_error: wrapped_error.error,
})
}
Err(error) => {
return Err(OpenAIError::JSONDeserialize(
Expand Down
Loading