Skip to main content

typesafe_ai_rs/
client.rs

1use crate::{
2    config::{config_methods, Config, ConfigBuilder},
3    transport, Error, ListModelsResponse, RawResponse, RequestOptions, SystemOneRequest,
4    SystemOneResponse,
5};
6use reqwest::Method;
7use serde_json::Value;
8use std::time::Instant;
9
10/// Asynchronous TypeSafe client. Clone it to share a pooled HTTP connection manager.
11#[derive(Clone)]
12pub struct Client {
13    config: Config,
14    http: reqwest::Client,
15}
16
17/// Construct an asynchronous client with environment fallbacks.
18#[derive(Default)]
19pub struct ClientBuilder {
20    config: ConfigBuilder,
21    http: Option<reqwest::Client>,
22}
23
24impl std::fmt::Debug for Client {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("Client")
27            .field("config", &self.config)
28            .finish_non_exhaustive()
29    }
30}
31
32impl std::fmt::Debug for ClientBuilder {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        f.debug_struct("ClientBuilder").finish_non_exhaustive()
35    }
36}
37
38impl ClientBuilder {
39    config_methods!();
40    /// Use an existing HTTP client for custom proxy, TLS, and connection settings.
41    /// SDK request timeouts still apply. Configure its redirect policy appropriately.
42    pub fn http_client(mut self, client: reqwest::Client) -> Self {
43        self.http = Some(client);
44        self
45    }
46    /// Resolve configuration and build the client without making a network request.
47    pub fn build(self) -> Result<Client, Error> {
48        let config = self.config.resolve()?;
49        let http = match self.http {
50            Some(http) => http,
51            None => reqwest::Client::builder()
52                .redirect(reqwest::redirect::Policy::none())
53                .build()
54                .map_err(Error::Connection)?,
55        };
56        Ok(Client { config, http })
57    }
58}
59
60impl Client {
61    /// Create a client using `TYPESAFE_*` environment variables.
62    pub fn new() -> Result<Self, Error> {
63        Self::builder().build()
64    }
65    /// Configure a client explicitly.
66    pub fn builder() -> ClientBuilder {
67        ClientBuilder::default()
68    }
69    /// The resolved default model.
70    pub fn default_model(&self) -> &str {
71        &self.config.model
72    }
73    /// The resolved API root.
74    pub fn base_url(&self) -> &str {
75        &self.config.base_url
76    }
77    /// Evaluate named questions about text or structured JSON state.
78    pub async fn system_one(&self, request: SystemOneRequest) -> Result<SystemOneResponse, Error> {
79        self.system_one_with_options(request, RequestOptions::default())
80            .await
81    }
82    /// Evaluate questions with per-call timeout, retry, headers, or cancellation overrides.
83    pub async fn system_one_with_options(
84        &self,
85        request: SystemOneRequest,
86        options: RequestOptions,
87    ) -> Result<SystemOneResponse, Error> {
88        let body = request.prepare(&self.config.model)?;
89        self.send(Method::POST, "/v1/systemone", Some(body), options, |raw| {
90            SystemOneResponse::from_raw_with_log_level(raw, self.config.log_level)
91        })
92        .await
93    }
94    /// Access model discovery.
95    pub fn models(&self) -> Models<'_> {
96        Models(self)
97    }
98
99    async fn send<T>(
100        &self,
101        method: Method,
102        path: &str,
103        body: Option<Value>,
104        options: RequestOptions,
105        decode: impl Fn(RawResponse) -> Result<T, Error>,
106    ) -> Result<T, Error> {
107        let request = transport::prepare(&self.config, method, path, body, &options)?;
108        let started = Instant::now();
109        let mut attempt = 0;
110        loop {
111            let headers = request.headers(attempt);
112            let attempt_started = Instant::now();
113            request.log_request(&self.config, &headers, attempt);
114            let mut builder = self
115                .http
116                .request(request.method.clone(), &request.url)
117                .headers(headers)
118                .timeout(request.timeout);
119            if let Some(body) = &request.body {
120                builder = builder.body(body.clone());
121            }
122            let operation = async {
123                let response = builder.send().await.map_err(|e| request.map_error(e))?;
124                let status = response.status();
125                let headers = response.headers().clone();
126                let body = response.bytes().await.map_err(|e| request.map_error(e))?;
127                let raw = RawResponse {
128                    status,
129                    headers,
130                    body,
131                };
132                request.log_response(&self.config, &raw, attempt_started);
133                request.finish(raw, &decode)
134            };
135            let result = tokio::select! {
136                biased;
137                _ = cancelled(&options) => return Err(Error::Cancelled),
138                result = operation => result,
139            };
140            match result {
141                Ok(response) => return Ok(response),
142                Err(error) => {
143                    let Some(delay) = request.retry_delay(&error, attempt, started) else {
144                        return Err(error);
145                    };
146                    if self.config.log_level >= log::LevelFilter::Info {
147                        log::info!(target: "typesafe_ai_rs", "retry={} delay_ms={}", attempt + 1, delay.as_millis());
148                    }
149                    tokio::select! {
150                        biased;
151                        _ = cancelled(&options) => return Err(Error::Cancelled),
152                        _ = tokio::time::sleep(delay) => {},
153                    }
154                    attempt += 1;
155                }
156            }
157        }
158    }
159}
160
161async fn cancelled(options: &RequestOptions) {
162    match &options.cancellation_token {
163        Some(token) => token.cancelled().await,
164        None => std::future::pending::<()>().await,
165    }
166}
167
168/// Models resource associated with an asynchronous client.
169#[derive(Clone, Copy, Debug)]
170pub struct Models<'a>(&'a Client);
171impl Models<'_> {
172    /// List available model cards with response metadata.
173    pub async fn list(&self) -> Result<ListModelsResponse, Error> {
174        self.list_with_options(RequestOptions::default()).await
175    }
176    /// List models with per-call request overrides.
177    pub async fn list_with_options(
178        &self,
179        options: RequestOptions,
180    ) -> Result<ListModelsResponse, Error> {
181        self.0
182            .send(
183                Method::GET,
184                "/v1/models",
185                None,
186                options,
187                ListModelsResponse::from_raw,
188            )
189            .await
190    }
191}