use crate::{
config::{config_methods, Config, ConfigBuilder},
transport, Error, ListModelsResponse, RawResponse, RequestOptions, SystemOneRequest,
SystemOneResponse,
};
use reqwest::Method;
use serde_json::Value;
use std::time::Instant;
#[derive(Clone)]
pub struct Client {
config: Config,
http: reqwest::blocking::Client,
}
#[derive(Default)]
pub struct ClientBuilder {
config: ConfigBuilder,
http: Option<reqwest::blocking::Client>,
}
impl std::fmt::Debug for Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Client")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl std::fmt::Debug for ClientBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientBuilder").finish_non_exhaustive()
}
}
impl ClientBuilder {
config_methods!();
pub fn http_client(mut self, value: reqwest::blocking::Client) -> Self {
self.http = Some(value);
self
}
pub fn build(self) -> Result<Client, Error> {
let config = self.config.resolve()?;
let http = match self.http {
Some(http) => http,
None => reqwest::blocking::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.build()
.map_err(Error::Connection)?,
};
Ok(Client { config, http })
}
}
impl Client {
pub fn new() -> Result<Self, Error> {
Self::builder().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn default_model(&self) -> &str {
&self.config.model
}
pub fn base_url(&self) -> &str {
&self.config.base_url
}
pub fn system_one(&self, request: SystemOneRequest) -> Result<SystemOneResponse, Error> {
self.system_one_with_options(request, RequestOptions::default())
}
pub fn system_one_with_options(
&self,
request: SystemOneRequest,
options: RequestOptions,
) -> Result<SystemOneResponse, Error> {
let body = request.prepare(&self.config.model)?;
self.send(Method::POST, "/v1/systemone", Some(body), options, |raw| {
SystemOneResponse::from_raw_with_log_level(raw, self.config.log_level)
})
}
pub fn models(&self) -> Models<'_> {
Models(self)
}
fn send<T>(
&self,
method: Method,
path: &str,
body: Option<Value>,
options: RequestOptions,
decode: impl Fn(RawResponse) -> Result<T, Error>,
) -> Result<T, Error> {
if options.cancellation_token.is_some() {
return Err(Error::Configuration(
"Cancellation requires the async client".into(),
));
}
let request = transport::prepare(&self.config, method, path, body, &options)?;
let started = Instant::now();
let mut attempt = 0;
loop {
let headers = request.headers(attempt);
let attempt_started = Instant::now();
request.log_request(&self.config, &headers, attempt);
let mut builder = self
.http
.request(request.method.clone(), &request.url)
.headers(headers)
.timeout(request.timeout);
if let Some(body) = &request.body {
builder = builder.body(body.clone());
}
let result = (|| {
let response = builder.send().map_err(|e| request.map_error(e))?;
let status = response.status();
let headers = response.headers().clone();
let body = response.bytes().map_err(|e| request.map_error(e))?;
let raw = RawResponse {
status,
headers,
body,
};
request.log_response(&self.config, &raw, attempt_started);
request.finish(raw, &decode)
})();
match result {
Ok(response) => return Ok(response),
Err(error) => {
let Some(delay) = request.retry_delay(&error, attempt, started) else {
return Err(error);
};
if self.config.log_level >= log::LevelFilter::Info {
log::info!(target: "typesafe_ai_rs", "retry={} delay_ms={}", attempt + 1, delay.as_millis());
}
std::thread::sleep(delay);
attempt += 1;
}
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct Models<'a>(&'a Client);
impl Models<'_> {
pub fn list(&self) -> Result<ListModelsResponse, Error> {
self.list_with_options(RequestOptions::default())
}
pub fn list_with_options(&self, options: RequestOptions) -> Result<ListModelsResponse, Error> {
self.0.send(
Method::GET,
"/v1/models",
None,
options,
ListModelsResponse::from_raw,
)
}
}