use serde::Deserialize;
use super::sse::decode_json;
use crate::types::OpenAIChunk;
use crate::{Error, Result};
const ERROR_FINISH_REASON: &str = "error";
const NO_DETAIL: &str = "the server reported an error without details";
#[derive(Deserialize)]
struct Envelope {
error: Option<ErrorBody>,
}
#[derive(Deserialize)]
struct ErrorBody {
message: Option<String>,
#[serde(rename = "type")]
kind: Option<String>,
code: Option<serde_json::Value>,
}
pub(super) fn decode_chunk(data: &str) -> Result<OpenAIChunk> {
let parsed = decode_json::<OpenAIChunk>(data);
let may_carry_error = match &parsed {
Ok(chunk) => chunk
.choices
.iter()
.any(|choice| choice.finish_reason.as_deref() == Some(ERROR_FINISH_REASON)),
Err(_) => true,
};
match may_carry_error.then(|| reported_error(data)).flatten() {
Some(error) => Err(error),
None => parsed,
}
}
fn reported_error(data: &str) -> Option<Error> {
let body = serde_json::from_str::<Envelope>(data).ok()?.error?;
let message = describe(body.kind, body.message);
Some(match body.code.as_ref().and_then(http_status) {
Some(status) => Error::api_status(status, message),
None => Error::api(message),
})
}
fn http_status(code: &serde_json::Value) -> Option<u16> {
code.as_u64()
.and_then(|code| u16::try_from(code).ok())
.filter(|status| (400..=599).contains(status))
}
fn describe(kind: Option<String>, message: Option<String>) -> String {
match (
kind.filter(|kind| !kind.is_empty()),
message.filter(|message| !message.is_empty()),
) {
(Some(kind), Some(message)) => format!("{kind}: {message}"),
(None, Some(message)) => message,
(Some(kind), None) => kind,
(None, None) => NO_DETAIL.to_string(),
}
}
#[cfg(test)]
mod tests;