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}