Skip to content

Commit 244eeca

Browse files
committed
feat: string-error feature allows deserialization into custom error format
the error format of different providers sometimes diverges from OpenAI. the string-error feature does not try to parse errors into the OpenAI format, but leaves it up to the user. when enabled, the is a wrapper around the original error message. This also enables cases where the error message is not even valid json
1 parent 61ab980 commit 244eeca

File tree

8 files changed

+122
-4
lines changed

8 files changed

+122
-4
lines changed

async-openai/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ native-tls-vendored = ["reqwest/native-tls-vendored"]
2525
realtime = ["dep:tokio-tungstenite"]
2626
# Bring your own types
2727
byot = []
28+
# Deserialize error responses yourself
29+
string-errors = []
2830

2931
[dependencies]
3032
async-openai-macros = { path = "../async-openai-macros", version = "0.1.0" }
@@ -59,6 +61,10 @@ serde_json = "1.0"
5961
name = "bring-your-own-type"
6062
required-features = ["byot"]
6163

64+
[[test]]
65+
name = "string-errors"
66+
required-features = ["string-errors", "byot"]
67+
6268
[package.metadata.docs.rs]
6369
all-features = true
6470
rustdoc-args = ["--cfg", "docsrs"]

async-openai/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ This can be useful in many scenarios:
145145
Visit [examples/bring-your-own-type](https://github.com/64bit/async-openai/tree/main/examples/bring-your-own-type)
146146
directory to learn more.
147147

148+
## String Errors
149+
150+
Enable the `string-errors` feature to receive API errors as raw strings instead of parsed structs. This can be useful
151+
in scenarios where providers expose errors in different formats.
152+
153+
See [examples/string-errors](https://github.com/64bit/async-openai/tree/main/examples/string-errors) for usage.
154+
148155
## Dynamic Dispatch for Different Providers
149156

150157
For any struct that implements `Config` trait, you can wrap it in a smart pointer and cast the pointer to `dyn Config`

async-openai/src/client.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ use reqwest::{multipart::Form, Response};
66
use reqwest_eventsource::{Error as EventSourceError, Event, EventSource, RequestBuilderExt};
77
use serde::{de::DeserializeOwned, Serialize};
88

9+
#[cfg(not(feature = "string-errors"))]
10+
use crate::error::{ApiError, WrappedError};
11+
912
use crate::{
1013
config::{Config, OpenAIConfig},
11-
error::{map_deserialization_error, ApiError, OpenAIError, StreamError, WrappedError},
14+
error::{map_deserialization_error, OpenAIError, StreamError},
1215
file::Files,
1316
image::Images,
1417
moderation::Moderations,
@@ -366,6 +369,7 @@ impl<C: Config> Client<C> {
366369
Ok(bytes) => Ok(bytes),
367370
Err(e) => {
368371
match e {
372+
#[cfg(not(feature = "string-errors"))]
369373
OpenAIError::ApiError(api_error) => {
370374
if status.is_server_error() {
371375
Err(backoff::Error::Transient {
@@ -385,6 +389,17 @@ impl<C: Config> Client<C> {
385389
Err(backoff::Error::Permanent(OpenAIError::ApiError(api_error)))
386390
}
387391
}
392+
#[cfg(feature = "string-errors")]
393+
OpenAIError::ApiError(api_error) => {
394+
if status.is_server_error() {
395+
Err(backoff::Error::Transient {
396+
err: OpenAIError::ApiError(api_error),
397+
retry_after: None,
398+
})
399+
} else {
400+
Err(backoff::Error::Permanent(OpenAIError::ApiError(api_error)))
401+
}
402+
}
388403
_ => Err(backoff::Error::Permanent(e)),
389404
}
390405
}
@@ -483,6 +498,7 @@ async fn read_response(response: Response) -> Result<Bytes, OpenAIError> {
483498
let status = response.status();
484499
let bytes = response.bytes().await.map_err(OpenAIError::Reqwest)?;
485500

501+
#[cfg(not(feature = "string-errors"))]
486502
if status.is_server_error() {
487503
// OpenAI does not guarantee server errors are returned as JSON so we cannot deserialize them.
488504
let message: String = String::from_utf8_lossy(&bytes).into_owned();
@@ -497,10 +513,18 @@ async fn read_response(response: Response) -> Result<Bytes, OpenAIError> {
497513

498514
// Deserialize response body from either error object or actual response object
499515
if !status.is_success() {
500-
let wrapped_error: WrappedError = serde_json::from_slice(bytes.as_ref())
501-
.map_err(|e| map_deserialization_error(e, bytes.as_ref()))?;
516+
#[cfg(not(feature = "string-errors"))]
517+
{
518+
let wrapped_error: WrappedError = serde_json::from_slice(bytes.as_ref())
519+
.map_err(|e| map_deserialization_error(e, bytes.as_ref()))?;
520+
return Err(OpenAIError::ApiError(wrapped_error.error));
521+
}
502522

503-
return Err(OpenAIError::ApiError(wrapped_error.error));
523+
#[cfg(feature = "string-errors")]
524+
{
525+
let message: String = String::from_utf8_lossy(&bytes).into_owned();
526+
return Err(OpenAIError::ApiError(message));
527+
}
504528
}
505529

506530
Ok(bytes)

async-openai/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ pub enum OpenAIError {
88
#[error("http error: {0}")]
99
Reqwest(#[from] reqwest::Error),
1010
/// OpenAI returns error object with details of API call failure
11+
#[cfg(not(feature = "string-errors"))]
1112
#[error("{0}")]
1213
ApiError(ApiError),
14+
/// Some OpenAI compatible services return error messages in diverge in error formats.
15+
/// This feature leaves deserialization to the user, not even assuming json.
16+
#[cfg(feature = "string-errors")]
17+
#[error("{0}")]
18+
ApiError(String),
1319
/// Error when a response cannot be deserialized into a Rust type
1420
#[error("failed to deserialize api response: error:{0} content:{1}")]
1521
JSONDeserialize(serde_json::Error, String),
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#![allow(dead_code)]
2+
//! The purpose of this test to make sure that with the string-errors feature enabled, the error is returned as a string.
3+
//! Enabling the byot feature allows for a simpler test, as the body can be written as an empty json value.
4+
5+
use async_openai::{error::OpenAIError, Client};
6+
use serde_json::{json, Value};
7+
8+
#[tokio::test]
9+
async fn test_byot_errors() {
10+
let client = Client::new();
11+
12+
let _r: Result<Value, OpenAIError> = client.chat().create_byot(json!({})).await;
13+
14+
match _r.unwrap_err() {
15+
OpenAIError::ApiError(value) => {
16+
let _value = serde_json::from_str::<Value>(&value).unwrap();
17+
}
18+
_ => {}
19+
};
20+
}

examples/string-errors/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "string-errors"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
async-openai = { path = "../../async-openai", features = ["string-errors", "byot"] }
8+
tokio = { version = "1.43.0", features = ["full"] }
9+
serde = { version = "1.0", features = ["derive"] }
10+
serde_json = "1.0"

examples/string-errors/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# String Errors Example
2+
3+
This example demonstrates how to use the `string-errors` feature to handle API errors from providers that use different error formats than OpenAI.

examples/string-errors/src/main.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! This example demonstrates how errors from OpenRouter can be parsed by the library consumer.
2+
//! It uses the `string-errors` feature to receive API errors as raw strings instead of parsed structs.
3+
4+
use async_openai::{config::OpenAIConfig, error::OpenAIError, Client};
5+
use serde::{Deserialize, Serialize};
6+
use serde_json::json;
7+
8+
#[derive(Debug, Deserialize, Serialize)]
9+
struct OpenRouterError {
10+
code: i32,
11+
message: String,
12+
}
13+
14+
#[derive(Debug, Deserialize)]
15+
struct ErrorWrapper {
16+
error: OpenRouterError,
17+
}
18+
19+
#[tokio::main]
20+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
21+
let config = OpenAIConfig::new().with_api_base("https://openrouter.ai/api/v1");
22+
let client = Client::with_config(config);
23+
24+
let result: Result<serde_json::Value, OpenAIError> = client
25+
.chat()
26+
.create_byot(json!({
27+
"model": "invalid-model",
28+
"messages": [{"role": "user", "content": "Hello"}]
29+
}))
30+
.await;
31+
32+
match result.unwrap_err() {
33+
OpenAIError::ApiError(error_string) => {
34+
let error = serde_json::from_str::<ErrorWrapper>(&error_string).unwrap();
35+
println!("Code: {}", error.error.code);
36+
println!("Message: {}", error.error.message);
37+
}
38+
_ => panic!("Expected OpenAIError::ApiError"),
39+
}
40+
41+
Ok(())
42+
}

0 commit comments

Comments
 (0)