Skip to main content

openai_interface/
lib.rs

1//! A low-level Rust interface for interacting with OpenAI's API.
2//!
3//! This crate provides a simple, efficient, and low-level way to interact with OpenAI's API,
4//! supporting both streaming and non-streaming responses. It leverages Rust's powerful type
5//! system for safety and performance, while exposing the full flexibility of the API.
6//!
7//! # Features
8//!
9//! - **Chat Completions**: Full support for OpenAI's chat completion and completion API,
10//!   including both streaming and non-streaming responses, and multimodal user
11//!   messages (text / image / audio / file content parts).
12//! - **Models**: List, retrieve and delete models.
13//! - **Embeddings**: Create embedding vectors from text input.
14//! - **Moderations**: Classify whether text and/or image input is potentially
15//!   harmful (untested).
16//! - **Images**: Generate, edit, and create variations of images (untested).
17//! - **Audio**: Text-to-speech, transcription, and translation endpoints (untested).
18//! - **Files**: Support for the OpenAI file API (upload, list, retrieve, delete,
19//!   download content).
20//! - **Streaming and Non-streaming**: Support for both streaming and non-streaming responses.
21//! - **Strong Typing**: Complete type definitions for all API requests and responses,
22//!   utilizing Rust's powerful type system.
23//! - **Configurable HTTP Client**: Every request method takes a [`reqwest::Client`], so
24//!   proxies, timeouts and connection pooling are under your control. See
25//!   [`rest::default_client`] for a sensible default.
26//! - **Error Handling**: Comprehensive error handling with detailed error types defined in
27//!   the [`errors`] module. Failed requests carry the API's error message, type and code.
28//! - **Async/Await**: Built with async/await support.
29//! - **Musl Support**: Designed to work with musl libc out-of-the-box.
30//! - **Multiple Provider Support**: Expected to work with OpenAI, DeepSeek, Qwen, and other
31//!   compatible API providers.
32//!
33//! ## Cargo Features
34//!
35//! To keep the request and response types strictly OpenAI-compatible, fields
36//! that are proprietary to other providers are opt-in via cargo features.
37//! OpenAI-compatible parameters such as `reasoning_effort` are always
38//! available on the request types, regardless of features:
39//!
40//! - **`deepseek`**: Enables DeepSeek's proprietary fields — the Beta chat
41//!   prefix completion fields (`prefix` / `reasoning_content` on assistant
42//!   messages), the `thinking` and `user_id` request parameters,
43//!   `reasoning_content` in responses and logprobs,
44//!   `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` usage statistics,
45//!   and the `insufficient_system_resource` finish reason. See
46//!   [api-docs.deepseek.com](https://api-docs.deepseek.com/).
47//!
48//! - **`qwen`**: Enables Qwen's proprietary request parameters
49//!   (`enable_thinking`, `thinking_budget`, `top_k`) as direct fields of the
50//!   chat request body. See
51//!   [the Qwen OpenAI-compatible Chat API docs](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-chat-completions).
52//!
53//! ## Implemented APIs
54//!
55//! - Chat Completions (create / retrieve / update / delete)
56//! - Completions
57//! - Models (list / retrieve / delete)
58//! - Embeddings
59//! - Moderations (untested)
60//! - Images (generate / edit / variation, untested)
61//! - Audio (speech / transcriptions / translations, untested)
62//! - Files (create / list / retrieve / delete / download content)
63//!
64//! # Examples
65//!
66//! ## Non-streaming Chat Completion
67//!
68//! This example demonstrates how to make a non-streaming request to the chat completion API.
69//!
70//! ```rust,no_run
71//! use openai_interface::chat::create::request::{Message, RequestBody};
72//! use openai_interface::chat::create::response::no_streaming::ChatCompletion;
73//! use openai_interface::rest::{default_client, post::PostNoStream};
74//!
75//! const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
76//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
77//!
78//! #[tokio::main]
79//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
80//!     let request = RequestBody {
81//!         messages: vec![
82//!             Message::System {
83//!                 content: "You are a helpful assistant.".to_string(),
84//!                 name: None,
85//!             },
86//!             Message::User {
87//!                 content: "Hello, how are you?".into(),
88//!                 name: None,
89//!             },
90//!         ],
91//!         model: DEEPSEEK_MODEL.to_string(),
92//!         stream: Some(false),
93//!         ..Default::default()
94//!     };
95//!
96//!     // Send the request
97//!     let chat_completion: ChatCompletion = request
98//!         .get_response(&default_client(), DEEPSEEK_CHAT_URL, "YOUR_API_KEY")
99//!         .await?;
100//!     let text = chat_completion.choices[0]
101//!         .message
102//!         .content
103//!         .as_deref()
104//!         .unwrap();
105//!     println!("{:?}", text);
106//!     Ok(())
107//! }
108//! ```
109//!
110//! ## Streaming Chat Completion
111//!
112//! This example demonstrates how to handle streaming responses from the API. As with the non-streaming
113//! example, all API parameters can be adjusted directly through the request struct.
114//!
115//! ```rust,no_run
116//! use openai_interface::chat::create::request::{Message, RequestBody};
117//! use openai_interface::chat::create::response::streaming::ChatCompletionChunk;
118//! use openai_interface::rest::{default_client, post::PostStream};
119//! use futures_util::StreamExt;
120//!
121//! const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
122//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
123//!
124//! #[tokio::main]
125//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
126//!     let request = RequestBody {
127//!         messages: vec![
128//!             Message::System {
129//!                 content: "You are a helpful assistant.".to_string(),
130//!                 name: None,
131//!             },
132//!             Message::User {
133//!                 content: "Who are you?".into(),
134//!                 name: None,
135//!             },
136//!         ],
137//!         model: DEEPSEEK_MODEL.to_string(),
138//!         stream: Some(true),
139//!         ..Default::default()
140//!     };
141//!
142//!     // Send the request
143//!     let mut response_stream = request
144//!         .get_stream_response(&default_client(), DEEPSEEK_CHAT_URL, "YOUR_API_KEY")
145//!         .await?;
146//!
147//!     let mut message = String::new();
148//!
149//!     while let Some(chunk_result) = response_stream.next().await {
150//!         let chunk: ChatCompletionChunk = chunk_result?;
151//!         if let Some(content) = chunk.choices[0].delta.content.as_deref() {
152//!             println!("content chunk: {}", content);
153//!             message.push_str(content);
154//!         }
155//!     }
156//!
157//!     println!("complete message: {}", message);
158//!     Ok(())
159//! }
160//! ```
161//!
162//! # Musl Build
163//!
164//! This crate is designed to work with musl libc, making it suitable for
165//! lightweight deployments in containerized environments. Longer compile times
166//! may be required as OpenSSL needs to be built from source.
167//!
168//! To build for musl:
169//! ```bash
170//! rustup target add x86_64-unknown-linux-musl
171//! cargo build --target x86_64-unknown-linux-musl
172//! ```
173
174/// Implements `FromStr` for JSON response types by deserializing them with
175/// `serde_json`, mapping any parse failure to
176/// [`OapiError::DeserializationError`](crate::errors::OapiError::DeserializationError).
177macro_rules! impl_from_str {
178    ($($target:ty),* $(,)?) => {
179        $(
180            impl std::str::FromStr for $target {
181                type Err = crate::errors::OapiError;
182
183                fn from_str(content: &str) -> Result<Self, Self::Err> {
184                    serde_json::from_str(content).map_err(|e| {
185                        crate::errors::OapiError::DeserializationError(e.to_string())
186                    })
187                }
188            }
189        )*
190    };
191}
192
193pub(crate) use impl_from_str;
194
195pub mod audio;
196pub mod chat;
197pub mod completions;
198pub mod embeddings;
199pub mod errors;
200pub mod files;
201pub mod images;
202pub mod models;
203pub mod moderations;
204pub mod rest;
205
206#[cfg(test)]
207mod tests {
208    use crate::chat::create::request::{Message, RequestBody};
209    use crate::chat::create::response::streaming::ChatCompletionChunk;
210    use crate::rest::{
211        default_client,
212        post::{PostNoStream, PostStream},
213    };
214    use futures_util::StreamExt;
215
216    const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
217    const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
218
219    fn deepseek_api_key() -> Option<String> {
220        std::env::var("DEEPSEEK_API_KEY")
221            .ok()
222            .map(|key| key.trim().to_string())
223            .filter(|key| !key.is_empty())
224    }
225
226    #[tokio::test]
227    async fn test_no_streaming() -> Result<(), Box<dyn std::error::Error>> {
228        let Some(api_key) = deepseek_api_key() else {
229            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
230            return Ok(());
231        };
232
233        let request = RequestBody {
234            messages: vec![
235                Message::System {
236                    content: "You are a helpful assistant.".to_string(),
237                    name: None,
238                },
239                Message::User {
240                    content: "Hello, how are you?".into(),
241                    name: None,
242                },
243            ],
244            model: DEEPSEEK_MODEL.to_string(),
245            stream: Some(false),
246            ..Default::default()
247        };
248
249        // Send the request
250        let chat_completion: crate::chat::create::response::no_streaming::ChatCompletion = request
251            .get_response(&default_client(), DEEPSEEK_CHAT_URL, &api_key)
252            .await?;
253        let text = chat_completion.choices[0]
254            .message
255            .content
256            .as_deref()
257            .unwrap();
258        println!("lib::test_no_streaming message: {}", text);
259        Ok(())
260    }
261
262    #[tokio::test]
263    async fn test_streaming() -> Result<(), Box<dyn std::error::Error>> {
264        let Some(api_key) = deepseek_api_key() else {
265            println!("Skipping: set DEEPSEEK_API_KEY to run this test");
266            return Ok(());
267        };
268
269        let request = RequestBody {
270            messages: vec![
271                Message::System {
272                    content: "You are a helpful assistant.".to_string(),
273                    name: None,
274                },
275                Message::User {
276                    content: "Who are you?".into(),
277                    name: None,
278                },
279            ],
280            model: DEEPSEEK_MODEL.to_string(),
281            stream: Some(true),
282            ..Default::default()
283        };
284
285        // Send the request
286        let mut response_stream = request
287            .get_stream_response(&default_client(), DEEPSEEK_CHAT_URL, &api_key)
288            .await?;
289
290        let mut message = String::new();
291
292        while let Some(chunk_result) = response_stream.next().await {
293            let chunk: ChatCompletionChunk = chunk_result?;
294            if let Some(content) = chunk.choices[0].delta.content.as_deref() {
295                println!("lib::test_streaming message: {}", content);
296                message.push_str(content);
297            }
298        }
299
300        println!("lib::test_streaming message: {}", message);
301        Ok(())
302    }
303}