openai-interface 0.7.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).
//! - **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.
//! - **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.
//! - **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 request parameters
//!   (`enable_thinking`, `thinking_budget`, `top_k`) as direct fields of the
//!   chat request body. See
//!   [the Qwen OpenAI-compatible Chat API docs](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-chat-completions).
//!
//! ## Implemented APIs
//!
//! - Chat Completions (create / retrieve / update / 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)
//!
//! # 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>> {
//!     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. Longer compile times
//! may be required as OpenSSL needs to be built from source.
//!
//! 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 chat;
pub mod completions;
pub mod embeddings;
pub mod errors;
pub mod files;
pub mod images;
pub mod models;
pub mod moderations;
pub mod rest;

#[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(())
    }
}