Skip to main content

openai_interface/rest/
mod.rs

1//! REST API client module for OpenAI interface
2//!
3//! This module provides the core HTTP functionality for making requests to OpenAI-compatible APIs.
4//! It includes traits and implementations for both streaming and non-streaming API calls.
5//!
6//! # Overview
7//!
8//! The `rest` module contains:
9//! - [`post`]: HTTP POST request functionality with streaming and non-streaming support
10//! - [`get`]: HTTP GET request functionality with various parameter handling options
11//! - [`delete`]: HTTP DELETE request functionality
12//! - [`default_client`]: A `reqwest::Client` constructor shared by all request traits
13//! - [`check_status`]: Shared non-2xx response handling which parses the error body
14//!
15//! # Usage
16//!
17//! The module is designed to be used through the higher-level API modules (`chat`, `completions`,
18//! etc.). However, you can use the traits directly if needed:
19//!
20//! ## POST Requests
21//!
22//! ```rust
23//! use openai_interface::rest::post::{Post, PostNoStream};
24//! use openai_interface::errors::OapiError;
25//! use serde::{Serialize, Deserialize};
26//!
27//! use std::str::FromStr;
28//!
29//! #[derive(Serialize)]
30//! struct MyRequest {
31//!     prompt: String,
32//!     stream: bool,
33//! }
34//!
35//! #[derive(Deserialize)]
36//! struct MyResponse {
37//!     // Define the fields of your response here
38//!     id: String,
39//! }
40//!
41//! impl FromStr for MyResponse {
42//!     type Err = OapiError;
43//!
44//!     fn from_str(content: &str) -> Result<Self, Self::Err> {
45//!         let parse_result: Result<Self, _> = serde_json::from_str(content)
46//!             .map_err(|e| OapiError::DeserializationError(e.to_string()));
47//!         parse_result
48//!     }
49//! }
50//!
51//! impl Post for MyRequest {
52//!     fn is_streaming(&self) -> bool {
53//!         self.stream
54//!     }
55//!     fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
56//!         Ok(format!("{}/service", base_url))
57//!     }
58//! }
59//!
60//! impl PostNoStream for MyRequest {
61//!     type Response = MyResponse;
62//! }
63//!
64//! // Send it with a client:
65//! // let client = openai_interface::rest::default_client();
66//! // let response: MyResponse = request
67//! //     .get_response(&client, "https://api.openai.com/v1/chat/completions", "API_KEY")
68//! //     .await?;
69//! ```
70//!
71//! ## GET Requests
72//!
73//! ```rust
74//! use openai_interface::rest::get::Get;
75//! use openai_interface::errors::OapiError;
76//!
77//! // GET request with URL building
78//! struct ComplexRequest {
79//!     resource_id: String,
80//!     limit: Option<u32>,
81//! }
82//!
83//! impl Get for ComplexRequest {
84//!     fn build_url(&self, base_url: &str) -> Result<String, OapiError> {
85//!         let mut url = format!("{}/{}", base_url, self.resource_id);
86//!         if let Some(limit) = self.limit {
87//!             url.push_str(&format!("?limit={}", limit));
88//!         }
89//!         Ok(url)
90//!     }
91//! }
92//! ```
93//!
94//! # Client configuration
95//!
96//! Every request method takes the client as its first argument, so callers
97//! control proxies, timeouts and connection pooling. Use
98//! [`default_client`] for a sensible default, or build your own, e.g. with a
99//! proxy:
100//!
101//! ```rust
102//! # #[cfg(feature = "ferritls")] {
103//! // Building a client needs an installed provider: this one comes from the
104//! // `ferritls` feature (see "TLS crypto provider" below).
105//! openai_interface::rest::install_crypto_provider().ok();
106//! let client = reqwest::Client::builder()
107//!     .proxy(reqwest::Proxy::http("http://127.0.0.1:10808")?)
108//!     .timeout(std::time::Duration::from_secs(60))
109//!     .build()?;
110//! # }
111//! # Ok::<(), reqwest::Error>(())
112//! ```
113//!
114//! # TLS crypto provider
115//!
116//! This crate depends on reqwest with its `rustls-no-provider` feature, so
117//! the rustls stack is compiled **without** a crypto backend. That leaves the
118//! choice of backend to the application: exactly one
119//! [`rustls::crypto::CryptoProvider`] must be installed as the process
120//! default before any `reqwest::Client` is built, otherwise reqwest panics at
121//! construction time.
122//!
123//! Nothing in this crate installs a provider for you — neither
124//! [`default_client`] nor any request method touches the global state, so an
125//! application that picked a provider first keeps it, and an application that
126//! never builds a client through this crate is free to install its own
127//! whenever it likes.
128//!
129//! This example is the regression lock for that promise. It runs as
130//! `should_panic`, deliberately with no provider installed and no hidden
131//! `ferritls` setup: if `default_client()` ever learns to install one on the
132//! side, it stops panicking and `cargo test` fails.
133//!
134//! ```rust,should_panic
135//! # // Silence reqwest's expected panic message so `--nocapture` stays clean;
136//! # // the payload still propagates, so should_panic matches as usual.
137//! # std::panic::set_hook(Box::new(|_| {}));
138//! let _client = openai_interface::rest::default_client();
139//! ```
140//!
141//! ## With the `ferritls` feature
142//!
143//! The optional `ferritls` feature (off by default) adds the pure-Rust
144//! `ferritls-rustls` backend and the [`install_crypto_provider`] helper.
145//! Enable it when you are happy to let this crate pick a provider for you:
146//!
147//! ```toml
148//! [dependencies]
149//! openai-interface = { version = "0.10", features = ["ferritls"] }
150//! ```
151//!
152//! Then call it once at startup, before the first client:
153//!
154//! ```rust
155//! # #[cfg(feature = "ferritls")] {
156//! openai_interface::rest::install_crypto_provider()
157//!     .expect("a rustls crypto provider was already installed");
158//! let client = openai_interface::rest::default_client();
159//! # }
160//! ```
161//!
162//! ## Without it
163//!
164//! With the feature off, `ferritls-rustls` is not in the dependency tree at
165//! all and [`install_crypto_provider`] does not exist. Install a provider
166//! yourself instead — first install wins, so do it before any client is
167//! built:
168//!
169//! ```rust,ignore
170//! // In the application crate, with `rustls = "0.23"` (feature `ring` or
171//! // `aws-lc-rs`) as one of its own dependencies:
172//! rustls::crypto::ring::default_provider()
173//!     .install_default()
174//!     .expect("a rustls crypto provider was already installed");
175//! ```
176//!
177//! ## When reqwest already has a backend
178//!
179//! Because Cargo features are additive, a project that depends on `reqwest`
180//! itself with `default-tls` / `rustls` (its defaults, which fall back to the
181//! bundled `aws-lc-rs` provider) or with `native-tls` (which skips the rustls
182//! path entirely) needs no provider installed here at all. That backend is
183//! then picked by feature unification instead of by you; see the
184//! ["TLS Crypto Provider"][crate#when-nothing-needs-to-be-installed] section
185//! of the crate docs for the trade-off.
186
187pub mod delete;
188pub mod get;
189pub mod post;
190
191use crate::errors::{ApiError, OapiError};
192
193/// Installs the pure-Rust [`ferritls-rustls`](https://crates.io/crates/ferritls-rustls)
194/// crypto provider as the process-wide default for rustls.
195///
196/// Only compiled with the `ferritls` cargo feature, which is what puts
197/// `ferritls-rustls` in the dependency tree at all; without the feature this
198/// crate ships no crypto backend, so you either install a
199/// [`rustls::crypto::CryptoProvider`] of your own or depend on `reqwest`
200/// yourself with a backend compiled in — see
201/// [When reqwest already has a backend](self#when-reqwest-already-has-a-backend).
202/// (`doc` builds include this function anyway so its entry and the links to it
203/// exist regardless of features.)
204///
205/// This crate depends on reqwest with the `rustls-no-provider` feature, so
206/// no crypto backend is compiled in by default and building a
207/// [`reqwest::Client`] without an installed provider panics. This helper lets
208/// an application delegate that choice to the crate; it is **never called
209/// implicitly** — no function here mutates the global provider on its own.
210///
211/// First install wins: if any crate (including the application itself) got
212/// there first, that provider is kept and the `Err` variant carries it. Call
213/// this once at startup, before the first client is built.
214///
215/// # Errors
216///
217/// Never fails; the `Err` variant carries the existing provider when one is
218/// already installed.
219#[cfg(any(doc, test, feature = "ferritls"))]
220pub fn install_crypto_provider() -> Result<(), std::sync::Arc<rustls::crypto::CryptoProvider>> {
221    ferritls_rustls::default_provider().install_default()
222}
223
224/// Builds a [`reqwest::Client`] with library defaults.
225///
226/// The client has a 300-second total timeout and a 60-second connect
227/// timeout. Pass your own client to any request method if you need a
228/// different configuration (proxy, timeout, pooling, ...).
229///
230/// This function does not select a TLS backend: see the
231/// [module docs][self#tls-crypto-provider] for why, and install a provider
232/// (with [`install_crypto_provider`] under the `ferritls` feature, or one of
233/// your own) before the first client is built.
234///
235/// # Panics
236///
237/// Panics if no rustls crypto provider has been installed as the process
238/// default yet — reqwest's `rustls-no-provider` build requires one at
239/// client construction time. This mirrors the panic behavior of
240/// [`reqwest::Client::new`].
241#[must_use]
242pub fn default_client() -> reqwest::Client {
243    // Inline unit tests are compiled into the test harness (`cfg(test)`), so
244    // they get a provider without per-test boilerplate. Library users, and
245    // the doctests (which link the non-test build), must install one
246    // explicitly — see the module docs.
247    #[cfg(test)]
248    let _ = install_crypto_provider();
249
250    reqwest::Client::builder()
251        .timeout(std::time::Duration::from_secs(300))
252        .connect_timeout(std::time::Duration::from_secs(60))
253        .build()
254        .expect("Failed to build the default HTTP client")
255}
256
257/// Checks a response status, turning a non-2xx response into an
258/// [`OapiError::ApiError`] that carries the parsed error body.
259///
260/// For a non-2xx status, this consumes the response and attempts to parse
261/// the body as an [`ApiError`]. If the body cannot be parsed, the raw text
262/// is kept as the error message. The response is returned unchanged when
263/// the status is a success.
264pub async fn check_status(response: reqwest::Response) -> Result<reqwest::Response, OapiError> {
265    if response.status().is_success() {
266        return Ok(response);
267    }
268
269    let status = response.status().as_u16();
270    let text = response.text().await.map_err(|e| {
271        OapiError::ResponseError(format!(
272            "HTTP {status} failed, and the error body could not be read: {e}"
273        ))
274    })?;
275
276    // The official error shape nests the fields under an `error` key. It
277    // must be tried FIRST: the flat shape would also deserialize (every
278    // field has a default), silently losing the real message.
279    let mut api_error = serde_json::from_str::<crate::errors::NestedApiError>(&text)
280        .map(|nested| nested.error)
281        .or_else(|_| serde_json::from_str::<ApiError>(&text))
282        .unwrap_or(ApiError {
283            status,
284            message: text,
285            error_type: None,
286            code: None,
287        });
288
289    if api_error.message.is_empty() {
290        api_error.message = format!("HTTP {status}");
291    }
292    api_error.status = status;
293
294    Err(OapiError::ApiError(api_error))
295}
296
297/// Parses a response body as text, applying [`check_status`] first.
298pub(crate) async fn response_text_checked(
299    response: reqwest::Response,
300) -> Result<String, OapiError> {
301    let response = check_status(response).await?;
302    response
303        .text()
304        .await
305        .map_err(|e| OapiError::ResponseError(format!("Failed to get response text: {e}")))
306}
307
308/// Parses a response body as raw bytes, applying [`check_status`] first.
309pub(crate) async fn response_bytes_checked(
310    response: reqwest::Response,
311) -> Result<Vec<u8>, OapiError> {
312    let response = check_status(response).await?;
313    response
314        .bytes()
315        .await
316        .map(|bytes| bytes.to_vec())
317        .map_err(|e| OapiError::ResponseError(format!("Failed to get response bytes: {e}")))
318}