openai-interface 0.9.0

A low-level Rust interface for the OpenAI API
Documentation
use std::path::PathBuf;

use serde::Deserialize;
use thiserror::Error;

/// The error body returned by an API on a failed request.
///
/// This mirrors the standard OpenAI error shape
/// `{"error": {"message": ..., "type": ..., "code": ...}}`. Compatible
/// providers that place these fields at the top level of the response body
/// are handled as well.
#[derive(Debug, Deserialize, Clone)]
pub struct ApiError {
    /// The HTTP status code of the failed response.
    #[serde(default)]
    pub status: u16,
    /// A human-readable description of the error.
    #[serde(default)]
    pub message: String,
    /// The category of the error, e.g. `invalid_request_error`.
    #[serde(rename = "type", alias = "error_type", default)]
    pub error_type: Option<String>,
    /// The machine-readable error code, e.g. `context_length_exceeded`.
    #[serde(default, deserialize_with = "deserialize_code")]
    pub code: Option<String>,
}

/// The error `code` is a string in the official API, but some compatible
/// providers return it as a number.
fn deserialize_code<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
    Ok(match value {
        None | Some(serde_json::Value::Null) => None,
        Some(serde_json::Value::String(s)) => Some(s),
        Some(other) => Some(other.to_string()),
    })
}

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "HTTP {}: {}", self.status, self.message)?;
        if let Some(error_type) = &self.error_type {
            write!(f, " (type: {error_type})")?;
        }
        if let Some(code) = &self.code {
            write!(f, " (code: {code})")?;
        }
        Ok(())
    }
}

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

/// Helper type for the official error envelope `{"error": {...}}`.
#[derive(Debug, Deserialize, Clone)]
pub(crate) struct NestedApiError {
    pub error: ApiError,
}

#[derive(Debug, Error)]
pub enum OapiError {
    #[error("Failed to send request: {0}")]
    SendError(#[from] reqwest::Error),
    #[error("Response error: {0}")]
    ResponseError(String),
    #[error("API request failed: {0}")]
    ApiError(#[from] ApiError),
    #[error("Failed to parse SSE stream: {0}")]
    SseParseError(String),
    /// If the request is a streaming request, but the context is not streaming.
    #[error("You cannot post a streaming request in a non-streaming context")]
    NonStreamingViolation,
    /// If the request is a non-streaming request, but the context is streaming.
    #[error("You cannot post a non-streaming request in a streaming context")]
    StreamingViolation,
    #[error("Deserialization error:\n{0}\n\nPlease report this error in the project issue.")]
    DeserializationError(String),
    #[error("File not found at: {0}")]
    FileNotFoundError(PathBuf),
    #[error("Failed to read file: {0}")]
    FileReadError(#[from] std::io::Error),
    #[error("Failed to operate on url: {0}")]
    UrlError(#[from] url::ParseError),
    #[error("Cannot be base: {0}")]
    UrlCannotBeBase(String),
}