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}