Skip to main content

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}