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