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