openai-interface 0.6.0

A low-level Rust interface for the OpenAI API
Documentation
//! REST API client module for OpenAI interface
//!
//! This module provides the core HTTP functionality for making requests to OpenAI-compatible APIs.
//! It includes traits and implementations for both streaming and non-streaming API calls.
//!
//! # Overview
//!
//! The `rest` module contains:
//! - [`post`]: HTTP POST request functionality with streaming and non-streaming support
//! - [`get`]: HTTP GET request functionality with various parameter handling options
//! - [`delete`]: HTTP DELETE request functionality
//! - [`default_client`]: A `reqwest::Client` constructor shared by all request traits
//! - [`check_status`]: Shared non-2xx response handling which parses the error body
//!
//! # Usage
//!
//! The module is designed to be used through the higher-level API modules (`chat`, `completions`,
//! etc.). However, you can use the traits directly if needed:
//!
//! ## POST Requests
//!
//! ```rust
//! use openai_interface::rest::post::{Post, PostNoStream};
//! use openai_interface::errors::OapiError;
//! use serde::{Serialize, Deserialize};
//!
//! use std::str::FromStr;
//!
//! #[derive(Serialize)]
//! struct MyRequest {
//!     prompt: String,
//!     stream: bool,
//! }
//!
//! #[derive(Deserialize)]
//! struct MyResponse {
//!     // Define the fields of your response here
//!     id: String,
//! }
//!
//! impl FromStr for MyResponse {
//!     type Err = OapiError;
//!
//!     fn from_str(content: &str) -> Result<Self, Self::Err> {
//!         let parse_result: Result<Self, _> = serde_json::from_str(content)
//!             .map_err(|e| OapiError::DeserializationError(e.to_string()));
//!         parse_result
//!     }
//! }
//!
//! impl Post for MyRequest {
//!     fn is_streaming(&self) -> bool {
//!         self.stream
//!     }
//!     fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
//!         Ok(format!("{}/service", base_url))
//!     }
//! }
//!
//! impl PostNoStream for MyRequest {
//!     type Response = MyResponse;
//! }
//!
//! // Send it with a client:
//! // let client = openai_interface::rest::default_client();
//! // let response: MyResponse = request
//! //     .get_response(&client, "https://api.openai.com/v1/chat/completions", "API_KEY")
//! //     .await?;
//! ```
//!
//! ## GET Requests
//!
//! ```rust
//! use openai_interface::rest::get::Get;
//! use openai_interface::errors::OapiError;
//!
//! // GET request with URL building
//! struct ComplexRequest {
//!     resource_id: String,
//!     limit: Option<u32>,
//! }
//!
//! impl Get for ComplexRequest {
//!     fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
//!         let mut url = format!("{}/{}", base_url, self.resource_id);
//!         if let Some(limit) = self.limit {
//!             url.push_str(&format!("?limit={}", limit));
//!         }
//!         Ok(url)
//!     }
//! }
//! ```
//!
//! # Client configuration
//!
//! Every request method takes the client as its first argument, so callers
//! control proxies, timeouts and connection pooling. Use
//! [`default_client`] for a sensible default, or build your own, e.g. with a
//! proxy:
//!
//! ```rust
//! let client = reqwest::Client::builder()
//!     .proxy(reqwest::Proxy::http("http://127.0.0.1:10808")?)
//!     .timeout(std::time::Duration::from_secs(60))
//!     .build()?;
//! # Ok::<(), reqwest::Error>(())
//! ```

pub mod delete;
pub mod get;
pub mod post;

use crate::errors::{ApiError, OapiError};

/// Builds a [`reqwest::Client`] with library defaults.
///
/// The client has a 300-second total timeout and a 60-second connect
/// timeout. Pass your own client to any request method if you need a
/// different configuration (proxy, timeout, pooling, ...).
///
/// # Panics
///
/// Panics if the underlying TLS backend fails to initialize. This mirrors
/// the panic behavior of [`reqwest::Client::new`].
#[must_use]
pub fn default_client() -> reqwest::Client {
    reqwest::Client::builder()
        .timeout(std::time::Duration::from_secs(300))
        .connect_timeout(std::time::Duration::from_secs(60))
        .build()
        .expect("Failed to build the default HTTP client")
}

/// Checks a response status, turning a non-2xx response into an
/// [`OapiError::ApiError`] that carries the parsed error body.
///
/// For a non-2xx status, this consumes the response and attempts to parse
/// the body as an [`ApiError`]. If the body cannot be parsed, the raw text
/// is kept as the error message. The response is returned unchanged when
/// the status is a success.
pub async fn check_status(response: reqwest::Response) -> Result<reqwest::Response, OapiError> {
    if response.status().is_success() {
        return Ok(response);
    }

    let status = response.status().as_u16();
    let text = response.text().await.map_err(|e| {
        OapiError::ResponseError(format!(
            "HTTP {status} failed, and the error body could not be read: {e}"
        ))
    })?;

    let mut api_error = serde_json::from_str::<ApiError>(&text)
        .map(|mut parsed: ApiError| {
            parsed.status = status;
            parsed
        })
        // The official error shape nests the fields under an `error` key.
        .or_else(|_| {
            serde_json::from_str::<crate::errors::NestedApiError>(&text).map(|nested| nested.error)
        })
        .unwrap_or(ApiError {
            status,
            message: text,
            error_type: None,
            code: None,
        });

    if api_error.message.is_empty() {
        api_error.message = format!("HTTP {status}");
    }
    api_error.status = status;

    Err(OapiError::ApiError(api_error))
}

/// Parses a response body as text, applying [`check_status`] first.
pub(crate) async fn response_text_checked(
    response: reqwest::Response,
) -> Result<String, OapiError> {
    let response = check_status(response).await?;
    response
        .text()
        .await
        .map_err(|e| OapiError::ResponseError(format!("Failed to get response text: {e}")))
}

/// Parses a response body as raw bytes, applying [`check_status`] first.
pub(crate) async fn response_bytes_checked(
    response: reqwest::Response,
) -> Result<Vec<u8>, OapiError> {
    let response = check_status(response).await?;
    response
        .bytes()
        .await
        .map(|bytes| bytes.to_vec())
        .map_err(|e| OapiError::ResponseError(format!("Failed to get response bytes: {e}")))
}