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 chat;
207pub mod completions;
208pub mod embeddings;
209pub mod errors;
210pub mod files;
211pub mod images;
212pub mod models;
213pub mod moderations;
214pub mod responses;
215pub mod rest;
216
217#[cfg(test)]
218mod tests {
219 use crate::chat::create::request::{Message, RequestBody};
220 use crate::chat::create::response::streaming::ChatCompletionChunk;
221 use crate::rest::{
222 default_client,
223 post::{PostNoStream, PostStream},
224 };
225 use futures_util::StreamExt;
226
227 const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
228 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
229
230 fn deepseek_api_key() -> Option<String> {
231 std::env::var("DEEPSEEK_API_KEY")
232 .ok()
233 .map(|key| key.trim().to_string())
234 .filter(|key| !key.is_empty())
235 }
236
237 #[tokio::test]
238 async fn test_no_streaming() -> Result<(), Box<dyn std::error::Error>> {
239 let Some(api_key) = deepseek_api_key() else {
240 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
241 return Ok(());
242 };
243
244 let request = RequestBody {
245 messages: vec![
246 Message::System {
247 content: "You are a helpful assistant.".to_string(),
248 name: None,
249 },
250 Message::User {
251 content: "Hello, how are you?".into(),
252 name: None,
253 },
254 ],
255 model: DEEPSEEK_MODEL.to_string(),
256 stream: Some(false),
257 ..Default::default()
258 };
259
260 // Send the request
261 let chat_completion: crate::chat::create::response::no_streaming::ChatCompletion = request
262 .get_response(&default_client(), DEEPSEEK_CHAT_URL, &api_key)
263 .await?;
264 let text = chat_completion.choices[0]
265 .message
266 .content
267 .as_deref()
268 .unwrap();
269 println!("lib::test_no_streaming message: {}", text);
270 Ok(())
271 }
272
273 #[tokio::test]
274 async fn test_streaming() -> Result<(), Box<dyn std::error::Error>> {
275 let Some(api_key) = deepseek_api_key() else {
276 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
277 return Ok(());
278 };
279
280 let request = RequestBody {
281 messages: vec![
282 Message::System {
283 content: "You are a helpful assistant.".to_string(),
284 name: None,
285 },
286 Message::User {
287 content: "Who are you?".into(),
288 name: None,
289 },
290 ],
291 model: DEEPSEEK_MODEL.to_string(),
292 stream: Some(true),
293 ..Default::default()
294 };
295
296 // Send the request
297 let mut response_stream = request
298 .get_stream_response(&default_client(), DEEPSEEK_CHAT_URL, &api_key)
299 .await?;
300
301 let mut message = String::new();
302
303 while let Some(chunk_result) = response_stream.next().await {
304 let chunk: ChatCompletionChunk = chunk_result?;
305 if let Some(content) = chunk.choices[0].delta.content.as_deref() {
306 println!("lib::test_streaming message: {}", content);
307 message.push_str(content);
308 }
309 }
310
311 println!("lib::test_streaming message: {}", message);
312 Ok(())
313 }
314}