openai-interface 0.10.0

A low-level Rust interface for the OpenAI API
Documentation
//! A low-level Rust interface for interacting with OpenAI's API.
//!
//! This crate provides a simple, efficient, and low-level way to interact with OpenAI's API,
//! supporting both streaming and non-streaming responses. It leverages Rust's powerful type
//! system for safety and performance, while exposing the full flexibility of the API.
//!
//! # Features
//!
//! - **Chat Completions**: Full support for OpenAI's chat completion and completion API,
//!   including both streaming and non-streaming responses, and multimodal user
//!   messages (text / image / audio / file content parts).
//! - **Responses**: Create model responses with the Responses API (string or
//!   item-based input, function tools, built-in web search, streaming events),
//!   retrieve and delete stored responses. Tested against DeepSeek and Qwen.
//! - **Models**: List, retrieve and delete models.
//! - **Embeddings**: Create embedding vectors from text input.
//! - **Moderations**: Classify whether text and/or image input is potentially
//!   harmful (untested).
//! - **Images**: Generate, edit, and create variations of images (untested).
//! - **Audio**: Text-to-speech, transcription, and translation endpoints (untested).
//! - **Files**: Support for the OpenAI file API (upload, list, retrieve, delete,
//!   download content).
//! - **Streaming and Non-streaming**: Support for both streaming and non-streaming responses.
//! - **Strong Typing**: Complete type definitions for all API requests and responses,
//!   utilizing Rust's powerful type system.
//! - **Configurable HTTP Client**: Every request method takes a [`reqwest::Client`], so
//!   proxies, timeouts and connection pooling are under your control. See
//!   [`rest::default_client`] for a sensible default, and
//!   [`rest::install_crypto_provider`] to pick the TLS backend.
//! - **Error Handling**: Comprehensive error handling with detailed error types defined in
//!   the [`errors`] module. Failed requests carry the API's error message, type and code.
//! - **Async/Await**: Built with async/await support.
//! - **Musl Support**: Designed to work with musl libc out-of-the-box; TLS is
//!   pure Rust, so no OpenSSL or C toolchain is needed.
//! - **Multiple Provider Support**: Expected to work with OpenAI, DeepSeek, Qwen, and other
//!   compatible API providers.
//!
//! ## Cargo Features
//!
//! To keep the request and response types strictly OpenAI-compatible, fields
//! that are proprietary to other providers are opt-in via cargo features.
//! OpenAI-compatible parameters such as `reasoning_effort` are always
//! available on the request types, regardless of features:
//!
//! - **`deepseek`**: Enables DeepSeek's proprietary fields — the Beta chat
//!   prefix completion fields (`prefix` / `reasoning_content` on assistant
//!   messages), the `thinking` and `user_id` request parameters,
//!   `reasoning_content` in responses and logprobs,
//!   `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` usage statistics,
//!   and the `insufficient_system_resource` finish reason. See
//!   [api-docs.deepseek.com](https://api-docs.deepseek.com/).
//!
//! - **`qwen`**: Enables Qwen's proprietary fields — the chat request
//!   parameters `enable_thinking`, `thinking_budget` and `top_k`, the
//!   Responses API input part `input_file`, the built-in tools
//!   (`web_extractor`, `code_interpreter`, `web_search_image`,
//!   `image_search`, `file_search`, `mcp`), the corresponding output items
//!   and streaming events, and the `x_details` / `x_tools` usage
//!   statistics. See
//!   [the Qwen OpenAI-compatible Chat API docs](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-chat-completions)
//!   and
//!   [the Qwen Responses API docs](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-responses).
//!
//! There is one feature unrelated to request fields:
//!
//! - **`ferritls`**: Adds the pure-Rust `ferritls-rustls` TLS crypto backend
//!   and the [`rest::install_crypto_provider`] helper that installs it. Off by
//!   default, so the crate never dictates your crypto backend; when you leave
//!   it off, install a [`rustls::crypto::CryptoProvider`] yourself before
//!   building any client. See ["TLS Crypto Provider"](#tls-crypto-provider).
//!
//! ## Implemented APIs
//!
//! - Chat Completions (create / retrieve / update / delete)
//! - Responses (create / retrieve / delete)
//! - Completions
//! - Models (list / retrieve / delete)
//! - Embeddings
//! - Moderations (untested)
//! - Images (generate / edit / variation, untested)
//! - Audio (speech / transcriptions / translations, untested)
//! - Files (create / list / retrieve / delete / download content)
//!
//! # TLS Crypto Provider
//!
//! HTTP is done by `reqwest`, depended on with its `rustls-no-provider`
//! feature: the rustls stack is compiled **without** a crypto backend, which
//! keeps the pure-Rust build (no C or asm toolchain needed) and leaves the
//! backend choice to the application. Consequently, exactly one
//! [`rustls::crypto::CryptoProvider`] must be installed as the process default
//! before any [`reqwest::Client`] is built — including the one returned by
//! [`rest::default_client`]. If none is installed, reqwest panics at client
//! construction time.
//!
//! This crate never installs a provider on your behalf. The optional
//! **`ferritls`** cargo feature adds the pure-Rust `ferritls-rustls` backend
//! together with [`rest::install_crypto_provider`], so you can delegate that
//! one decision to the crate:
//!
//! ```toml
//! [dependencies]
//! openai-interface = { version = "0.10", features = ["ferritls"] }
//! ```
//!
//! ```rust,no_run
//! # #[cfg(feature = "ferritls")] {
//! // Choose the backend once, before building any client:
//! openai_interface::rest::install_crypto_provider()
//!     .expect("a rustls crypto provider was already installed");
//! # }
//! ```
//!
//! To use a different backend (`ring`, `aws-lc-rs`, or a hand-picked
//! [`rustls::crypto::CryptoProvider`]), leave the feature off and install it
//! yourself — first install wins, so whichever provider is in place when the
//! first client is built is the one everything in the process uses:
//!
//! ```rust,ignore
//! // In the application crate, with `rustls = "0.23"` (feature `ring` or
//! // `aws-lc-rs`) as one of its own dependencies:
//! rustls::crypto::ring::default_provider()
//!     .install_default()
//!     .expect("a rustls crypto provider was already installed");
//! ```
//!
//! ## When nothing needs to be installed
//!
//! Cargo features are additive across the dependency tree, so if your project
//! depends on `reqwest` itself with a crypto backend compiled in — its default
//! `default-tls`, or `rustls` explicitly — reqwest falls back to the
//! `aws-lc-rs` provider it ships with, and no install step is needed at all.
//! Enabling `native-tls` instead routes TLS through the system stack, so the
//! rustls path is never taken.
//!
//! ```toml
//! [dependencies]
//! reqwest = "0.13"          # default features: `default-tls` -> `rustls`
//! openai-interface = "0.10" # no provider of its own
//! ```
//!
//! The catch is that the backend is then decided by feature unification rather
//! than by you, and an unrelated dependency change can move it. To pin the
//! choice, enable the `ferritls` feature or install a provider yourself.
//!
//! # Examples
//!
//! ## Non-streaming Chat Completion
//!
//! This example demonstrates how to make a non-streaming request to the chat completion API.
//!
//! ```rust,no_run
//! use openai_interface::chat::create::request::{Message, RequestBody};
//! use openai_interface::chat::create::response::no_streaming::ChatCompletion;
//! use openai_interface::rest::{default_client, post::PostNoStream};
//!
//! const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     // Needs the `ferritls` cargo feature; leave it out if you install
//!     // your own rustls crypto provider. See the "TLS Crypto Provider"
//!     // section above.
//!     # #[cfg(feature = "ferritls")]
//!     openai_interface::rest::install_crypto_provider().ok();
//!
//!     let request = RequestBody {
//!         messages: vec![
//!             Message::System {
//!                 content: "You are a helpful assistant.".to_string(),
//!                 name: None,
//!             },
//!             Message::User {
//!                 content: "Hello, how are you?".into(),
//!                 name: None,
//!             },
//!         ],
//!         model: DEEPSEEK_MODEL.to_string(),
//!         stream: Some(false),
//!         ..Default::default()
//!     };
//!
//!     // Send the request
//!     let chat_completion: ChatCompletion = request
//!         .get_response(&default_client(), DEEPSEEK_CHAT_URL, "YOUR_API_KEY")
//!         .await?;
//!     let text = chat_completion.choices[0]
//!         .message
//!         .content
//!         .as_deref()
//!         .unwrap();
//!     println!("{:?}", text);
//!     Ok(())
//! }
//! ```
//!
//! ## Streaming Chat Completion
//!
//! This example demonstrates how to handle streaming responses from the API. As with the non-streaming
//! example, all API parameters can be adjusted directly through the request struct.
//!
//! ```rust,no_run
//! use openai_interface::chat::create::request::{Message, RequestBody};
//! use openai_interface::chat::create::response::streaming::ChatCompletionChunk;
//! use openai_interface::rest::{default_client, post::PostStream};
//! use futures_util::StreamExt;
//!
//! const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!     let request = RequestBody {
//!         messages: vec![
//!             Message::System {
//!                 content: "You are a helpful assistant.".to_string(),
//!                 name: None,
//!             },
//!             Message::User {
//!                 content: "Who are you?".into(),
//!                 name: None,
//!             },
//!         ],
//!         model: DEEPSEEK_MODEL.to_string(),
//!         stream: Some(true),
//!         ..Default::default()
//!     };
//!
//!     // Send the request
//!     let mut response_stream = request
//!         .get_stream_response(&default_client(), DEEPSEEK_CHAT_URL, "YOUR_API_KEY")
//!         .await?;
//!
//!     let mut message = String::new();
//!
//!     while let Some(chunk_result) = response_stream.next().await {
//!         let chunk: ChatCompletionChunk = chunk_result?;
//!         if let Some(content) = chunk.choices[0].delta.content.as_deref() {
//!             println!("content chunk: {}", content);
//!             message.push_str(content);
//!         }
//!     }
//!
//!     println!("complete message: {}", message);
//!     Ok(())
//! }
//! ```
//!
//! # Musl Build
//!
//! This crate is designed to work with musl libc, making it suitable for
//! lightweight deployments in containerized environments. TLS is provided by
//! rustls with a pure-Rust crypto backend, so OpenSSL does not need to be
//! built from source. See [`rest::install_crypto_provider`] for how the
//! backend is selected at runtime.
//!
//! To build for musl:
//! ```bash
//! rustup target add x86_64-unknown-linux-musl
//! cargo build --target x86_64-unknown-linux-musl
//! ```

/// Implements `FromStr` for JSON response types by deserializing them with
/// `serde_json`, mapping any parse failure to
/// [`OapiError::DeserializationError`](crate::errors::OapiError::DeserializationError).
macro_rules! impl_from_str {
    ($($target:ty),* $(,)?) => {
        $(
            impl std::str::FromStr for $target {
                type Err = crate::errors::OapiError;

                fn from_str(content: &str) -> Result<Self, Self::Err> {
                    serde_json::from_str(content).map_err(|e| {
                        crate::errors::OapiError::DeserializationError(e.to_string())
                    })
                }
            }
        )*
    };
}

pub(crate) use impl_from_str;

pub mod audio;
pub mod batches;
pub mod chat;
pub mod completions;
pub mod containers;
pub mod conversations;
pub mod embeddings;
pub mod errors;
pub mod evals;
pub mod files;
pub mod fine_tuning;
pub mod images;
pub mod models;
pub mod moderations;
pub mod pagination;
pub mod realtime;
pub mod responses;
pub mod rest;
pub mod uploads;
pub mod vector_stores;

#[cfg(test)]
mod tests {
    use crate::chat::create::request::{Message, RequestBody};
    use crate::chat::create::response::streaming::ChatCompletionChunk;
    use crate::rest::{
        default_client,
        post::{PostNoStream, PostStream},
    };
    use futures_util::StreamExt;

    const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
    const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";

    fn deepseek_api_key() -> Option<String> {
        std::env::var("DEEPSEEK_API_KEY")
            .ok()
            .map(|key| key.trim().to_string())
            .filter(|key| !key.is_empty())
    }

    #[tokio::test]
    async fn test_no_streaming() -> Result<(), Box<dyn std::error::Error>> {
        let Some(api_key) = deepseek_api_key() else {
            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
            return Ok(());
        };

        let request = RequestBody {
            messages: vec![
                Message::System {
                    content: "You are a helpful assistant.".to_string(),
                    name: None,
                },
                Message::User {
                    content: "Hello, how are you?".into(),
                    name: None,
                },
            ],
            model: DEEPSEEK_MODEL.to_string(),
            stream: Some(false),
            ..Default::default()
        };

        // Send the request
        let chat_completion: crate::chat::create::response::no_streaming::ChatCompletion = request
            .get_response(&default_client(), DEEPSEEK_CHAT_URL, &api_key)
            .await?;
        let text = chat_completion.choices[0]
            .message
            .content
            .as_deref()
            .unwrap();
        println!("lib::test_no_streaming message: {}", text);
        Ok(())
    }

    #[tokio::test]
    async fn test_streaming() -> Result<(), Box<dyn std::error::Error>> {
        let Some(api_key) = deepseek_api_key() else {
            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
            return Ok(());
        };

        let request = RequestBody {
            messages: vec![
                Message::System {
                    content: "You are a helpful assistant.".to_string(),
                    name: None,
                },
                Message::User {
                    content: "Who are you?".into(),
                    name: None,
                },
            ],
            model: DEEPSEEK_MODEL.to_string(),
            stream: Some(true),
            ..Default::default()
        };

        // Send the request
        let mut response_stream = request
            .get_stream_response(&default_client(), DEEPSEEK_CHAT_URL, &api_key)
            .await?;

        let mut message = String::new();

        while let Some(chunk_result) = response_stream.next().await {
            let chunk: ChatCompletionChunk = chunk_result?;
            if let Some(content) = chunk.choices[0].delta.content.as_deref() {
                println!("lib::test_streaming message: {}", content);
                message.push_str(content);
            }
        }

        println!("lib::test_streaming message: {}", message);
        Ok(())
    }
}