1use std::collections::BTreeMap;
2use std::fmt;
3use std::sync::Arc;
4use std::time::Duration;
5
6use serde::Serialize;
7use serde_json::Value;
8
9use crate::answer::{ModelInfo, ModelsResponse, SystemOneResponse};
10use crate::error::{Error, Result};
11use crate::question::Question;
12use crate::request::SystemOneRequest;
13use crate::retry::RetryPolicy;
14
15pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai";
17pub const DEFAULT_MODEL: &str = "jev-latest";
19const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
20const MAX_ERROR_BODY: usize = 2048;
21
22const ENV_API_KEY: &str = "TYPESAFE_API_KEY";
23const ENV_MODEL: &str = "TYPESAFE_DEFAULT_MODEL";
24const ENV_BASE_URL: &str = "TYPESAFE_BASE_URL";
25
26struct Inner {
27 http: reqwest::Client,
28 api_key: String,
29 base_url: String,
30 model: String,
31 retry: RetryPolicy,
32 timeout: Duration,
33}
34
35#[derive(Clone)]
37pub struct Client {
38 inner: Arc<Inner>,
39}
40
41impl fmt::Debug for Client {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 f.debug_struct("Client")
44 .field("base_url", &self.inner.base_url)
45 .field("model", &self.inner.model)
46 .field("retry", &self.inner.retry)
47 .field("timeout", &self.inner.timeout)
48 .finish_non_exhaustive()
49 }
50}
51
52#[derive(Default)]
54#[must_use = "call `.build()` to get a Client"]
55pub struct ClientBuilder {
56 api_key: Option<String>,
57 base_url: Option<String>,
58 model: Option<String>,
59 retry: Option<RetryPolicy>,
60 timeout: Option<Duration>,
61 http: Option<reqwest::Client>,
62}
63
64impl fmt::Debug for ClientBuilder {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 f.debug_struct("ClientBuilder")
67 .field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
68 .field("base_url", &self.base_url)
69 .field("model", &self.model)
70 .field("retry", &self.retry)
71 .field("timeout", &self.timeout)
72 .finish_non_exhaustive()
73 }
74}
75
76impl ClientBuilder {
77 pub fn api_key(mut self, key: impl Into<String>) -> Self {
79 self.api_key = Some(key.into());
80 self
81 }
82
83 pub fn base_url(mut self, url: impl Into<String>) -> Self {
86 self.base_url = Some(url.into());
87 self
88 }
89
90 pub fn model(mut self, model: impl Into<String>) -> Self {
93 self.model = Some(model.into());
94 self
95 }
96
97 pub fn retry(mut self, retry: RetryPolicy) -> Self {
99 self.retry = Some(retry);
100 self
101 }
102
103 pub fn timeout(mut self, timeout: Duration) -> Self {
105 self.timeout = Some(timeout);
106 self
107 }
108
109 pub fn http_client(mut self, http: reqwest::Client) -> Self {
112 self.http = Some(http);
113 self
114 }
115
116 pub fn build(self) -> Result<Client> {
125 let api_key = self
126 .api_key
127 .filter(|k| !k.trim().is_empty())
128 .or_else(|| env_non_empty(ENV_API_KEY))
129 .ok_or_else(|| Error::Config(format!("no API key: pass ClientBuilder::api_key or set {ENV_API_KEY}")))?;
130 let base_url = self
131 .base_url
132 .or_else(|| env_non_empty(ENV_BASE_URL))
133 .unwrap_or_else(|| DEFAULT_BASE_URL.to_owned())
134 .trim_end_matches('/')
135 .to_owned();
136 let model = self
137 .model
138 .or_else(|| env_non_empty(ENV_MODEL))
139 .unwrap_or_else(|| DEFAULT_MODEL.to_owned());
140 let http = match self.http {
141 Some(h) => h,
142 None if cfg!(not(any(feature = "rustls", feature = "native-tls"))) => {
143 return Err(Error::Config(
144 "no TLS backend: enable the `rustls` or `native-tls` feature, or pass a reqwest::Client via \
145 ClientBuilder::http_client"
146 .to_string(),
147 ));
148 }
149 None => reqwest::Client::builder()
150 .build()
151 .map_err(|e| Error::Config(format!("failed to build HTTP client: {e}")))?,
152 };
153 Ok(Client {
154 inner: Arc::new(Inner {
155 http,
156 api_key,
157 base_url,
158 model,
159 retry: self.retry.unwrap_or_default(),
160 timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
161 }),
162 })
163 }
164}
165
166fn env_non_empty(name: &str) -> Option<String> {
167 std::env::var(name)
168 .ok()
169 .map(|v| v.trim().to_owned())
170 .filter(|v| !v.is_empty())
171}
172
173#[derive(Serialize)]
174struct SystemOneBody<'a> {
175 state: &'a Value,
176 model: &'a str,
177 questions: &'a BTreeMap<String, Question>,
178}
179
180impl Client {
181 pub fn builder() -> ClientBuilder {
183 ClientBuilder::default()
184 }
185
186 pub fn from_env() -> Result<Self> {
193 Self::builder().build()
194 }
195
196 #[must_use]
198 pub fn default_model(&self) -> &str {
199 &self.inner.model
200 }
201
202 pub fn system_one(&self) -> SystemOneRequest<'_> {
204 SystemOneRequest::new(self)
205 }
206
207 pub async fn evaluate<K>(
221 &self,
222 state: impl Serialize,
223 questions: impl IntoIterator<Item = (K, Question)>,
224 ) -> Result<SystemOneResponse>
225 where
226 K: Into<String>,
227 {
228 self.evaluate_with_model(&self.inner.model, state, questions).await
229 }
230
231 pub async fn evaluate_with_model<K>(
237 &self,
238 model: &str,
239 state: impl Serialize,
240 questions: impl IntoIterator<Item = (K, Question)>,
241 ) -> Result<SystemOneResponse>
242 where
243 K: Into<String>,
244 {
245 let state = serde_json::to_value(state).map_err(Error::RequestSerialization)?;
246 let questions: BTreeMap<String, Question> = questions.into_iter().map(|(k, q)| (k.into(), q)).collect();
247 let body = serde_json::to_vec(&SystemOneBody {
248 state: &state,
249 model,
250 questions: &questions,
251 })
252 .map_err(Error::RequestSerialization)?;
253 let bytes = self.send(reqwest::Method::POST, "/v1/systemone", Some(body)).await?;
254 serde_json::from_slice(&bytes).map_err(Error::ResponseValidation)
255 }
256
257 pub async fn models(&self) -> Result<Vec<ModelInfo>> {
265 let bytes = self.send(reqwest::Method::GET, "/v1/models", None).await?;
266 let parsed: ModelsResponse = serde_json::from_slice(&bytes).map_err(Error::ResponseValidation)?;
267 Ok(parsed.models)
268 }
269
270 async fn send(&self, method: reqwest::Method, path: &str, body: Option<Vec<u8>>) -> Result<Vec<u8>> {
271 let url = format!("{}{}", self.inner.base_url, path);
272 let mut retry = 0u32;
273 loop {
274 let mut req = self
275 .inner
276 .http
277 .request(method.clone(), &url)
278 .bearer_auth(&self.inner.api_key)
279 .timeout(self.inner.timeout);
280 if let Some(b) = &body {
281 req = req
282 .header(reqwest::header::CONTENT_TYPE, "application/json")
283 .body(b.clone());
284 }
285
286 let outcome = match req.send().await {
287 Ok(resp) => {
288 let status = resp.status().as_u16();
289 if (200..300).contains(&status) {
290 return resp.bytes().await.map(|b| b.to_vec()).map_err(Error::from_transport);
291 }
292 let retry_after = parse_retry_after(resp.headers());
293 let message = read_error_body(resp).await;
294 let err = Error::from_status(status, retry_after, message);
295 if RetryPolicy::is_retryable_status(status) {
296 Err((err, retry_after))
297 } else {
298 return Err(err);
299 }
300 }
301 Err(e) => Err((Error::from_transport(e), None)),
302 };
303
304 let (err, retry_after) = match outcome {
305 Ok(never) => never,
306 Err(pair) => pair,
307 };
308 if retry >= self.inner.retry.max_retries {
309 return Err(err);
310 }
311 retry += 1;
312 tokio::time::sleep(self.inner.retry.delay(retry, retry_after)).await;
313 }
314 }
315}
316
317fn parse_retry_after(headers: &reqwest::header::HeaderMap) -> Option<Duration> {
318 headers
319 .get(reqwest::header::RETRY_AFTER)?
320 .to_str()
321 .ok()?
322 .trim()
323 .parse::<f64>()
324 .ok()
325 .filter(|s| s.is_finite() && *s >= 0.0)
326 .map(Duration::from_secs_f64)
327}
328
329async fn read_error_body(resp: reqwest::Response) -> String {
330 let text = resp.text().await.unwrap_or_default();
331 let message = serde_json::from_str::<Value>(&text)
332 .ok()
333 .and_then(|v| {
334 ["detail", "message", "error"].into_iter().find_map(|k| {
335 v.get(k)
336 .map(|m| m.as_str().map_or_else(|| m.to_string(), str::to_owned))
337 })
338 })
339 .unwrap_or(text);
340 if message.len() > MAX_ERROR_BODY {
341 let mut end = MAX_ERROR_BODY;
342 while !message.is_char_boundary(end) {
343 end -= 1;
344 }
345 format!("{}…", &message[..end])
346 } else {
347 message
348 }
349}