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, and
29//! [`rest::install_crypto_provider`] to pick the TLS backend.
30//! - **Error Handling**: Comprehensive error handling with detailed error types defined in
31//! the [`errors`] module. Failed requests carry the API's error message, type and code.
32//! - **Async/Await**: Built with async/await support.
33//! - **Musl Support**: Designed to work with musl libc out-of-the-box; TLS is
34//! pure Rust, so no OpenSSL or C toolchain is needed.
35//! - **Multiple Provider Support**: Expected to work with OpenAI, DeepSeek, Qwen, and other
36//! compatible API providers.
37//!
38//! ## Cargo Features
39//!
40//! To keep the request and response types strictly OpenAI-compatible, fields
41//! that are proprietary to other providers are opt-in via cargo features.
42//! OpenAI-compatible parameters such as `reasoning_effort` are always
43//! available on the request types, regardless of features:
44//!
45//! - **`deepseek`**: Enables DeepSeek's proprietary fields — the Beta chat
46//! prefix completion fields (`prefix` / `reasoning_content` on assistant
47//! messages), the `thinking` and `user_id` request parameters,
48//! `reasoning_content` in responses and logprobs,
49//! `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens` usage statistics,
50//! and the `insufficient_system_resource` finish reason. See
51//! [api-docs.deepseek.com](https://api-docs.deepseek.com/).
52//!
53//! - **`qwen`**: Enables Qwen's proprietary fields — the chat request
54//! parameters `enable_thinking`, `thinking_budget` and `top_k`, the
55//! Responses API input part `input_file`, the built-in tools
56//! (`web_extractor`, `code_interpreter`, `web_search_image`,
57//! `image_search`, `file_search`, `mcp`), the corresponding output items
58//! and streaming events, and the `x_details` / `x_tools` usage
59//! statistics. See
60//! [the Qwen OpenAI-compatible Chat API docs](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-chat-completions)
61//! and
62//! [the Qwen Responses API docs](https://www.alibabacloud.com/help/zh/model-studio/qwen-api-via-openai-responses).
63//!
64//! There is one feature unrelated to request fields:
65//!
66//! - **`ferritls`**: Adds the pure-Rust `ferritls-rustls` TLS crypto backend
67//! and the [`rest::install_crypto_provider`] helper that installs it. Off by
68//! default, so the crate never dictates your crypto backend; when you leave
69//! it off, install a [`rustls::crypto::CryptoProvider`] yourself before
70//! building any client. See ["TLS Crypto Provider"](#tls-crypto-provider).
71//!
72//! ## Implemented APIs
73//!
74//! - Chat Completions (create / retrieve / update / delete)
75//! - Responses (create / retrieve / delete)
76//! - Completions
77//! - Models (list / retrieve / delete)
78//! - Embeddings
79//! - Moderations (untested)
80//! - Images (generate / edit / variation, untested)
81//! - Audio (speech / transcriptions / translations, untested)
82//! - Files (create / list / retrieve / delete / download content)
83//!
84//! # TLS Crypto Provider
85//!
86//! HTTP is done by `reqwest`, depended on with its `rustls-no-provider`
87//! feature: the rustls stack is compiled **without** a crypto backend, which
88//! keeps the pure-Rust build (no C or asm toolchain needed) and leaves the
89//! backend choice to the application. Consequently, exactly one
90//! [`rustls::crypto::CryptoProvider`] must be installed as the process default
91//! before any [`reqwest::Client`] is built — including the one returned by
92//! [`rest::default_client`]. If none is installed, reqwest panics at client
93//! construction time.
94//!
95//! This crate never installs a provider on your behalf. The optional
96//! **`ferritls`** cargo feature adds the pure-Rust `ferritls-rustls` backend
97//! together with [`rest::install_crypto_provider`], so you can delegate that
98//! one decision to the crate:
99//!
100//! ```toml
101//! [dependencies]
102//! openai-interface = { version = "0.10", features = ["ferritls"] }
103//! ```
104//!
105//! ```rust,no_run
106//! # #[cfg(feature = "ferritls")] {
107//! // Choose the backend once, before building any client:
108//! openai_interface::rest::install_crypto_provider()
109//! .expect("a rustls crypto provider was already installed");
110//! # }
111//! ```
112//!
113//! To use a different backend (`ring`, `aws-lc-rs`, or a hand-picked
114//! [`rustls::crypto::CryptoProvider`]), leave the feature off and install it
115//! yourself — first install wins, so whichever provider is in place when the
116//! first client is built is the one everything in the process uses:
117//!
118//! ```rust,ignore
119//! // In the application crate, with `rustls = "0.23"` (feature `ring` or
120//! // `aws-lc-rs`) as one of its own dependencies:
121//! rustls::crypto::ring::default_provider()
122//! .install_default()
123//! .expect("a rustls crypto provider was already installed");
124//! ```
125//!
126//! ## When nothing needs to be installed
127//!
128//! Cargo features are additive across the dependency tree, so if your project
129//! depends on `reqwest` itself with a crypto backend compiled in — its default
130//! `default-tls`, or `rustls` explicitly — reqwest falls back to the
131//! `aws-lc-rs` provider it ships with, and no install step is needed at all.
132//! Enabling `native-tls` instead routes TLS through the system stack, so the
133//! rustls path is never taken.
134//!
135//! ```toml
136//! [dependencies]
137//! reqwest = "0.13" # default features: `default-tls` -> `rustls`
138//! openai-interface = "0.10" # no provider of its own
139//! ```
140//!
141//! The catch is that the backend is then decided by feature unification rather
142//! than by you, and an unrelated dependency change can move it. To pin the
143//! choice, enable the `ferritls` feature or install a provider yourself.
144//!
145//! # Examples
146//!
147//! ## Non-streaming Chat Completion
148//!
149//! This example demonstrates how to make a non-streaming request to the chat completion API.
150//!
151//! ```rust,no_run
152//! use openai_interface::chat::create::request::{Message, RequestBody};
153//! use openai_interface::chat::create::response::no_streaming::ChatCompletion;
154//! use openai_interface::rest::{RequestOptions, default_client, post::PostNoStream};
155//!
156//! const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
157//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
158//!
159//! #[tokio::main]
160//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
161//! // Needs the `ferritls` cargo feature; leave it out if you install
162//! // your own rustls crypto provider. See the "TLS Crypto Provider"
163//! // section above.
164//! # #[cfg(feature = "ferritls")]
165//! openai_interface::rest::install_crypto_provider().ok();
166//!
167//! let request = RequestBody {
168//! messages: vec![
169//! Message::System {
170//! content: "You are a helpful assistant.".into(),
171//! name: None,
172//! },
173//! Message::User {
174//! content: "Hello, how are you?".into(),
175//! name: None,
176//! },
177//! ],
178//! model: DEEPSEEK_MODEL.to_string(),
179//! stream: Some(false),
180//! ..Default::default()
181//! };
182//!
183//! // Send the request
184//! let chat_completion: ChatCompletion = request
185//! .get_response(&default_client(), DEEPSEEK_CHAT_URL, &RequestOptions::bearer("YOUR_API_KEY"))
186//! .await?;
187//! let text = chat_completion.choices[0]
188//! .message
189//! .content
190//! .as_deref()
191//! .unwrap();
192//! println!("{:?}", text);
193//! Ok(())
194//! }
195//! ```
196//!
197//! ## Streaming Chat Completion
198//!
199//! This example demonstrates how to handle streaming responses from the API. As with the non-streaming
200//! example, all API parameters can be adjusted directly through the request struct.
201//!
202//! ```rust,no_run
203//! use openai_interface::chat::create::request::{Message, RequestBody};
204//! use openai_interface::chat::create::response::streaming::ChatCompletionChunk;
205//! use openai_interface::rest::{RequestOptions, default_client, post::PostStream};
206//! use futures_util::StreamExt;
207//!
208//! const DEEPSEEK_CHAT_URL: &'static str = "https://api.deepseek.com";
209//! const DEEPSEEK_MODEL: &'static str = "deepseek-v4-flash";
210//!
211//! #[tokio::main]
212//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
213//! let request = RequestBody {
214//! messages: vec![
215//! Message::System {
216//! content: "You are a helpful assistant.".into(),
217//! name: None,
218//! },
219//! Message::User {
220//! content: "Who are you?".into(),
221//! name: None,
222//! },
223//! ],
224//! model: DEEPSEEK_MODEL.to_string(),
225//! stream: Some(true),
226//! ..Default::default()
227//! };
228//!
229//! // Send the request
230//! let mut response_stream = request
231//! .get_stream_response(&default_client(), DEEPSEEK_CHAT_URL, &RequestOptions::bearer("YOUR_API_KEY"))
232//! .await?;
233//!
234//! let mut message = String::new();
235//!
236//! while let Some(chunk_result) = response_stream.next().await {
237//! let chunk: ChatCompletionChunk = chunk_result?;
238//! if let Some(content) = chunk.choices[0].delta.content.as_deref() {
239//! println!("content chunk: {}", content);
240//! message.push_str(content);
241//! }
242//! }
243//!
244//! println!("complete message: {}", message);
245//! Ok(())
246//! }
247//! ```
248//!
249//! # Musl Build
250//!
251//! This crate is designed to work with musl libc, making it suitable for
252//! lightweight deployments in containerized environments. TLS is provided by
253//! rustls with a pure-Rust crypto backend, so OpenSSL does not need to be
254//! built from source. See [`rest::install_crypto_provider`] for how the
255//! backend is selected at runtime.
256//!
257//! To build for musl:
258//! ```bash
259//! rustup target add x86_64-unknown-linux-musl
260//! cargo build --target x86_64-unknown-linux-musl
261//! ```
262
263/// Implements `FromStr` for JSON response types by deserializing them with
264/// `serde_json`, mapping any parse failure to
265/// [`OapiError::DeserializationError`](crate::errors::OapiError::DeserializationError).
266macro_rules! impl_from_str {
267 ($($target:ty),* $(,)?) => {
268 $(
269 impl std::str::FromStr for $target {
270 type Err = crate::errors::OapiError;
271
272 fn from_str(content: &str) -> Result<Self, Self::Err> {
273 serde_json::from_str(content).map_err(|e| {
274 crate::errors::OapiError::DeserializationError(e.to_string())
275 })
276 }
277 }
278 )*
279 };
280}
281
282pub(crate) use impl_from_str;
283
284pub mod audio;
285pub mod batches;
286pub mod chat;
287pub mod completions;
288pub mod containers;
289pub mod conversations;
290pub mod embeddings;
291pub mod errors;
292pub mod evals;
293pub mod files;
294pub mod fine_tuning;
295pub mod images;
296pub mod models;
297pub mod moderations;
298pub mod pagination;
299pub mod realtime;
300pub mod responses;
301pub mod rest;
302pub mod uploads;
303pub mod vector_stores;
304
305#[cfg(test)]
306mod tests {
307 use crate::chat::create::request::{Message, RequestBody};
308 use crate::chat::create::response::streaming::ChatCompletionChunk;
309 use crate::rest::{
310 RequestOptions, default_client,
311 post::{PostNoStream, PostStream},
312 };
313 use futures_util::StreamExt;
314
315 const DEEPSEEK_CHAT_URL: &str = "https://api.deepseek.com";
316 const DEEPSEEK_MODEL: &str = "deepseek-v4-flash";
317
318 fn deepseek_api_key() -> Option<String> {
319 std::env::var("DEEPSEEK_API_KEY")
320 .ok()
321 .map(|key| key.trim().to_string())
322 .filter(|key| !key.is_empty())
323 }
324
325 #[tokio::test]
326 async fn test_no_streaming() -> Result<(), Box<dyn std::error::Error>> {
327 let Some(api_key) = deepseek_api_key() else {
328 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
329 return Ok(());
330 };
331
332 let request = RequestBody {
333 messages: vec![
334 Message::System {
335 content: "You are a helpful assistant.".into(),
336 name: None,
337 },
338 Message::User {
339 content: "Hello, how are you?".into(),
340 name: None,
341 },
342 ],
343 model: DEEPSEEK_MODEL.to_string(),
344 stream: Some(false),
345 ..Default::default()
346 };
347
348 // Send the request
349 let chat_completion: crate::chat::create::response::no_streaming::ChatCompletion = request
350 .get_response(
351 &default_client(),
352 DEEPSEEK_CHAT_URL,
353 &RequestOptions::bearer(&api_key),
354 )
355 .await?;
356 let text = chat_completion.choices[0]
357 .message
358 .content
359 .as_deref()
360 .unwrap();
361 println!("lib::test_no_streaming message: {}", text);
362 Ok(())
363 }
364
365 #[tokio::test]
366 async fn test_streaming() -> Result<(), Box<dyn std::error::Error>> {
367 let Some(api_key) = deepseek_api_key() else {
368 println!("Skipping: set DEEPSEEK_API_KEY to run this test");
369 return Ok(());
370 };
371
372 let request = RequestBody {
373 messages: vec![
374 Message::System {
375 content: "You are a helpful assistant.".into(),
376 name: None,
377 },
378 Message::User {
379 content: "Who are you?".into(),
380 name: None,
381 },
382 ],
383 model: DEEPSEEK_MODEL.to_string(),
384 stream: Some(true),
385 ..Default::default()
386 };
387
388 // Send the request
389 let mut response_stream = request
390 .get_stream_response(
391 &default_client(),
392 DEEPSEEK_CHAT_URL,
393 &RequestOptions::bearer(&api_key),
394 )
395 .await?;
396
397 let mut message = String::new();
398
399 while let Some(chunk_result) = response_stream.next().await {
400 let chunk: ChatCompletionChunk = chunk_result?;
401 if let Some(content) = chunk.choices[0].delta.content.as_deref() {
402 println!("lib::test_streaming message: {}", content);
403 message.push_str(content);
404 }
405 }
406
407 println!("lib::test_streaming message: {}", message);
408 Ok(())
409 }
410}