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