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 // The official error shape nests the fields under an `error` key. It
154 // must be tried FIRST: the flat shape would also deserialize (every
155 // field has a default), silently losing the real message.
156 let mut api_error = serde_json::from_str::<crate::errors::NestedApiError>(&text)
157 .map(|nested| nested.error)
158 .or_else(|_| serde_json::from_str::<ApiError>(&text))
159 .unwrap_or(ApiError {
160 status,
161 message: text,
162 error_type: None,
163 code: None,
164 });
165
166 if api_error.message.is_empty() {
167 api_error.message = format!("HTTP {status}");
168 }
169 api_error.status = status;
170
171 Err(OapiError::ApiError(api_error))
172}
173
174/// Parses a response body as text, applying [`check_status`] first.
175pub(crate) async fn response_text_checked(
176 response: reqwest::Response,
177) -> Result<String, OapiError> {
178 let response = check_status(response).await?;
179 response
180 .text()
181 .await
182 .map_err(|e| OapiError::ResponseError(format!("Failed to get response text: {e}")))
183}
184
185/// Parses a response body as raw bytes, applying [`check_status`] first.
186pub(crate) async fn response_bytes_checked(
187 response: reqwest::Response,
188) -> Result<Vec<u8>, OapiError> {
189 let response = check_status(response).await?;
190 response
191 .bytes()
192 .await
193 .map(|bytes| bytes.to_vec())
194 .map_err(|e| OapiError::ResponseError(format!("Failed to get response bytes: {e}")))
195}