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//! let client = reqwest::Client::builder()
103//!     .proxy(reqwest::Proxy::http("http://127.0.0.1:10808")?)
104//!     .timeout(std::time::Duration::from_secs(60))
105//!     .build()?;
106//! # Ok::<(), reqwest::Error>(())
107//! ```
108
109pub mod delete;
110pub mod get;
111pub mod post;
112
113use crate::errors::{ApiError, OapiError};
114
115/// Builds a [`reqwest::Client`] with library defaults.
116///
117/// The client has a 300-second total timeout and a 60-second connect
118/// timeout. Pass your own client to any request method if you need a
119/// different configuration (proxy, timeout, pooling, ...).
120///
121/// # Panics
122///
123/// Panics if the underlying TLS backend fails to initialize. This mirrors
124/// the panic behavior of [`reqwest::Client::new`].
125#[must_use]
126pub fn default_client() -> reqwest::Client {
127    reqwest::Client::builder()
128        .timeout(std::time::Duration::from_secs(300))
129        .connect_timeout(std::time::Duration::from_secs(60))
130        .build()
131        .expect("Failed to build the default HTTP client")
132}
133
134/// Checks a response status, turning a non-2xx response into an
135/// [`OapiError::ApiError`] that carries the parsed error body.
136///
137/// For a non-2xx status, this consumes the response and attempts to parse
138/// the body as an [`ApiError`]. If the body cannot be parsed, the raw text
139/// is kept as the error message. The response is returned unchanged when
140/// the status is a success.
141pub async fn check_status(response: reqwest::Response) -> Result<reqwest::Response, OapiError> {
142    if response.status().is_success() {
143        return Ok(response);
144    }
145
146    let status = response.status().as_u16();
147    let text = response.text().await.map_err(|e| {
148        OapiError::ResponseError(format!(
149            "HTTP {status} failed, and the error body could not be read: {e}"
150        ))
151    })?;
152
153    let mut api_error = serde_json::from_str::<ApiError>(&text)
154        .map(|mut parsed: ApiError| {
155            parsed.status = status;
156            parsed
157        })
158        // The official error shape nests the fields under an `error` key.
159        .or_else(|_| {
160            serde_json::from_str::<crate::errors::NestedApiError>(&text).map(|nested| nested.error)
161        })
162        .unwrap_or(ApiError {
163            status,
164            message: text,
165            error_type: None,
166            code: None,
167        });
168
169    if api_error.message.is_empty() {
170        api_error.message = format!("HTTP {status}");
171    }
172    api_error.status = status;
173
174    Err(OapiError::ApiError(api_error))
175}
176
177/// Parses a response body as text, applying [`check_status`] first.
178pub(crate) async fn response_text_checked(
179    response: reqwest::Response,
180) -> Result<String, OapiError> {
181    let response = check_status(response).await?;
182    response
183        .text()
184        .await
185        .map_err(|e| OapiError::ResponseError(format!("Failed to get response text: {e}")))
186}
187
188/// Parses a response body as raw bytes, applying [`check_status`] first.
189pub(crate) async fn response_bytes_checked(
190    response: reqwest::Response,
191) -> Result<Vec<u8>, OapiError> {
192    let response = check_status(response).await?;
193    response
194        .bytes()
195        .await
196        .map(|bytes| bytes.to_vec())
197        .map_err(|e| OapiError::ResponseError(format!("Failed to get response bytes: {e}")))
198}