use std::time::Duration;
use reqwest::header::HeaderMap;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware, RetryableStrategy};
use crate::error::Error;
use crate::value_objects::RateLimit;
pub fn build_http_client() -> ClientWithMiddleware {
build_http_client_with_max_retries(2)
}
pub fn build_http_client_with_max_retries(max_retries: u32) -> ClientWithMiddleware {
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(max_retries);
ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build()
}
pub fn build_http_client_with_retry_strategy(
max_retries: u32,
strategy: impl RetryableStrategy + Send + Sync + 'static,
) -> ClientWithMiddleware {
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(max_retries);
ClientBuilder::new(reqwest::Client::new())
.with(RetryTransientMiddleware::new_with_policy_and_strategy(
retry_policy,
strategy,
))
.build()
}
pub fn merge_provider_options<T: serde::Serialize>(
wire_request: &T,
provider_options: &serde_json::Value,
) -> Result<serde_json::Value, Error> {
let mut body = serde_json::to_value(wire_request)?;
if let (Some(base), Some(overrides)) = (body.as_object_mut(), provider_options.as_object()) {
for (key, value) in overrides {
base.insert(key.clone(), value.clone());
}
}
Ok(body)
}
pub fn merge_provider_options_into_form(
mut form: reqwest::multipart::Form,
provider_options: &serde_json::Value,
) -> reqwest::multipart::Form {
if let Some(overrides) = provider_options.as_object() {
for (key, value) in overrides {
let value = value
.as_str()
.map(str::to_string)
.unwrap_or_else(|| value.to_string());
form = form.text(key.clone(), value);
}
}
form
}
pub struct ErrorMapper<'a> {
pub provider: &'a str,
}
impl<'a> ErrorMapper<'a> {
pub fn map_error_response(
&self,
status: reqwest::StatusCode,
headers: &HeaderMap,
body: &str,
) -> Error {
let (kind, message) = parse_error_body(body);
match status.as_u16() {
429 => Error::RateLimited {
provider: self.provider.to_string(),
retry_after: header_duration(headers, "retry-after"),
limits: parse_rate_limits(headers),
},
413 => Error::RequestTooLarge {
provider: self.provider.to_string(),
details: message,
},
529 => Error::Overloaded {
provider: self.provider.to_string(),
},
status_code => Error::Provider {
provider: self.provider.to_string(),
status: status_code,
kind,
message,
},
}
}
}
fn parse_error_body(body: &str) -> (Option<String>, String) {
match serde_json::from_str::<serde_json::Value>(body) {
Ok(value) => {
let message = value
.pointer("/error/message")
.and_then(|v| v.as_str())
.unwrap_or(body)
.to_string();
let kind = value
.pointer("/error/type")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
(kind, message)
}
Err(_) => (None, body.to_string()),
}
}
fn header_duration(headers: &HeaderMap, name: &str) -> Option<Duration> {
headers
.get(name)?
.to_str()
.ok()?
.parse::<u64>()
.ok()
.map(Duration::from_secs)
}
fn parse_rate_limits(headers: &HeaderMap) -> Vec<RateLimit> {
let limit = header_u64(headers, "x-ratelimit-limit-requests");
let remaining = header_u64(headers, "x-ratelimit-remaining-requests");
match (limit, remaining) {
(Some(limit), Some(remaining)) => vec![RateLimit {
name: "requests".to_string(),
limit,
remaining,
reset_at: headers
.get("x-ratelimit-reset-requests")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string()),
}],
_ => Vec::new(),
}
}
fn header_u64(headers: &HeaderMap, name: &str) -> Option<u64> {
headers.get(name)?.to_str().ok()?.parse::<u64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
use serde_json::json;
#[derive(Serialize)]
struct Example {
model: String,
temperature: Option<f32>,
}
#[test]
fn merge_provider_options_overrides_and_adds_fields() {
let wire_request = Example {
model: "gpt-4o-mini".to_string(),
temperature: Some(0.2),
};
let provider_options = json!({"temperature": 0.9, "seed": 42});
let body = merge_provider_options(&wire_request, &provider_options).unwrap();
assert_eq!(body["model"], "gpt-4o-mini");
assert_eq!(body["temperature"], 0.9);
assert_eq!(body["seed"], 42);
}
#[test]
fn merge_provider_options_is_a_no_op_for_the_default_null_value() {
let wire_request = Example {
model: "gpt-4o-mini".to_string(),
temperature: Some(0.2),
};
let expected = serde_json::to_value(&wire_request).unwrap();
let body = merge_provider_options(&wire_request, &serde_json::Value::Null).unwrap();
assert_eq!(body, expected);
}
}