1use std::future::{Future, IntoFuture};
4use std::pin::Pin;
5use std::sync::Arc;
6use std::time::{Duration, Instant};
7
8use http::header::{
9 ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderName, HeaderValue, USER_AGENT,
10};
11use http::{Method, StatusCode};
12use serde::Serialize;
13use serde_json::{Map, Value};
14
15use crate::constants::*;
16use crate::error::{ApiError, Error, ResponseValidationError, Result, lenient_body};
17use crate::question::Questions;
18use crate::response::{
19 DecodeFailure, DecodedSystemOne, ListModelsResponse, ResponseMeta, SystemOneResponse,
20 decode_models, decode_system_one,
21};
22use crate::retry::RetryPolicy;
23
24type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
25
26#[derive(Debug, Default)]
29pub struct ClientBuilder {
30 api_key: Option<String>,
31 base_url: Option<String>,
32 model: Option<String>,
33 timeout: Option<Duration>,
34 retry: Option<RetryPolicy>,
35 headers: HeaderMap,
36 http: Option<reqwest::Client>,
37}
38
39impl ClientBuilder {
40 pub fn api_key(mut self, key: impl Into<String>) -> Self {
42 self.api_key = Some(key.into());
43 self
44 }
45
46 pub fn base_url(mut self, url: impl Into<String>) -> Self {
48 self.base_url = Some(url.into());
49 self
50 }
51
52 pub fn model(mut self, model: impl Into<String>) -> Self {
54 self.model = Some(model.into());
55 self
56 }
57
58 pub fn timeout(mut self, timeout: Duration) -> Self {
60 self.timeout = Some(timeout);
61 self
62 }
63
64 pub fn retry(mut self, policy: RetryPolicy) -> Self {
66 self.retry = Some(policy);
67 self
68 }
69
70 pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
73 self.headers.insert(name, value);
74 self
75 }
76
77 #[cfg(feature = "reqwest-client")]
80 pub fn http_client(mut self, client: reqwest::Client) -> Self {
81 self.http = Some(client);
82 self
83 }
84
85 pub fn build(self) -> Result<Client> {
87 let api_key = resolve(self.api_key, API_KEY_ENV, None).ok_or_else(|| {
88 Error::Config(format!(
89 "No API key was provided. Pass api_key or set the {API_KEY_ENV} environment variable."
90 ))
91 })?;
92 let base_url = resolve(self.base_url, BASE_URL_ENV, Some(DEFAULT_BASE_URL))
93 .unwrap_or_default()
94 .trim_end_matches('/')
95 .to_owned();
96 check_base_url(&base_url)?;
97 let model = resolve(self.model, DEFAULT_MODEL_ENV, Some(DEFAULT_MODEL)).unwrap_or_default();
98 let timeout = check_timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT))?;
99 let retry = self.retry.unwrap_or_default();
100 retry.validate()?;
101
102 let mut auth = HeaderValue::from_str(&format!("Bearer {api_key}")).map_err(|_| {
103 Error::Config("The API key contains characters not allowed in a header.".into())
104 })?;
105 auth.set_sensitive(true);
106
107 let mut protected = HeaderMap::new();
108 protected.insert(AUTHORIZATION, auth);
109 protected.insert(ACCEPT, HeaderValue::from_static("application/json"));
110 let ident = HeaderValue::from_str(&format!("{SDK_NAME}/{VERSION}")).expect("ascii");
111 protected.insert(USER_AGENT, ident.clone());
112 protected.insert(HeaderName::from_static(SDK_HEADER), ident);
113 protected.insert(
114 HeaderName::from_static(RUNTIME_HEADER),
115 HeaderValue::from_str(&format!(
116 "rust ({}; {})",
117 std::env::consts::OS,
118 std::env::consts::ARCH
119 ))
120 .expect("ascii"),
121 );
122
123 Ok(Client {
124 inner: Arc::new(Inner {
125 http: self.http.unwrap_or_default(),
126 base_url,
127 model,
128 timeout,
129 retry,
130 default_headers: self.headers,
131 protected,
132 }),
133 })
134 }
135}
136
137fn resolve(explicit: Option<String>, env: &str, default: Option<&str>) -> Option<String> {
138 explicit
139 .or_else(|| {
140 std::env::var(env)
141 .ok()
142 .map(|v| v.trim().to_owned())
143 .filter(|v| !v.is_empty())
144 })
145 .or_else(|| default.map(str::to_owned))
146}
147
148fn check_base_url(url: &str) -> Result<()> {
149 match reqwest::Url::parse(url) {
150 Ok(u) if matches!(u.scheme(), "http" | "https") && u.has_host() => Ok(()),
151 Ok(_) => Err(Error::Config(format!(
152 "base_url must be an http(s) URL with a host, got {url:?}."
153 ))),
154 Err(e) => Err(Error::Config(format!(
155 "base_url {url:?} is not a valid URL: {e}."
156 ))),
157 }
158}
159
160fn check_timeout(t: Duration) -> Result<Duration> {
161 if t.is_zero() {
162 Err(Error::Config("timeout must be a positive duration.".into()))
163 } else {
164 Ok(t)
165 }
166}
167
168#[derive(Debug)]
169struct Inner {
170 http: reqwest::Client,
171 base_url: String,
172 model: String,
173 timeout: Duration,
174 retry: RetryPolicy,
175 default_headers: HeaderMap,
176 protected: HeaderMap,
177}
178
179#[derive(Debug, Clone)]
201pub struct Client {
202 inner: Arc<Inner>,
203}
204
205impl Client {
206 pub fn builder() -> ClientBuilder {
208 ClientBuilder::default()
209 }
210
211 pub fn from_env() -> Result<Self> {
213 Self::builder().build()
214 }
215
216 pub fn default_model(&self) -> &str {
218 &self.inner.model
219 }
220
221 pub fn system_one<S: Serialize>(
224 &self,
225 state: S,
226 questions: impl Into<Questions>,
227 ) -> SystemOneRequest {
228 SystemOneRequest {
229 client: self.clone(),
230 state: serde_json::to_value(state)
231 .map_err(|e| format!("The state could not be encoded as JSON: {e}")),
232 questions: questions.into(),
233 model: None,
234 extra_body: Map::new(),
235 opts: CallOptions::default(),
236 }
237 }
238
239 pub fn models(&self) -> Models {
241 Models {
242 client: self.clone(),
243 }
244 }
245
246 async fn execute(
247 &self,
248 method: Method,
249 path: &str,
250 body: Option<Vec<u8>>,
251 opts: CallOptions,
252 ) -> Result<(Vec<u8>, ResponseMeta, String)> {
253 let inner = &self.inner;
254 let retry = opts.retry.as_ref().unwrap_or(&inner.retry);
255 retry.validate()?;
256 let timeout = check_timeout(opts.timeout.unwrap_or(inner.timeout))?;
257 let url = format!("{}{}", inner.base_url, path);
258 let endpoint = format!("{method} {}", redact_url(&url));
259
260 let mut headers = inner.default_headers.clone();
261 headers.extend(opts.headers);
262 headers.remove(RETRY_COUNT_HEADER);
263 headers.extend(inner.protected.clone());
264 if body.is_some() {
265 headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
266 }
267
268 let started = Instant::now();
269 let mut attempts: u32 = 0;
270 loop {
271 let mut h = headers.clone();
272 if attempts > 0 {
273 h.insert(
274 HeaderName::from_static(RETRY_COUNT_HEADER),
275 HeaderValue::from(attempts),
276 );
277 tracing::info!(%endpoint, retry = attempts, "retrying");
278 }
279 attempts += 1;
280 let result = self
281 .attempt(&method, &url, &endpoint, h, body.clone(), timeout)
282 .await;
283 match result {
284 Ok((bytes, status, resp_headers)) => {
285 let meta = ResponseMeta {
286 status,
287 headers: resp_headers,
288 attempts,
289 };
290 return Ok((bytes, meta, endpoint));
291 }
292 Err(err) => {
293 if !retry.is_retryable(&err) {
294 return Err(err);
295 }
296 let delay = retry.delay(attempts, &err);
297 if retry.should_stop(attempts, started.elapsed(), delay) {
298 return Err(err);
299 }
300 if !delay.is_zero() {
301 tokio::time::sleep(delay).await;
302 }
303 }
304 }
305 }
306 }
307
308 async fn attempt(
309 &self,
310 method: &Method,
311 url: &str,
312 endpoint: &str,
313 headers: HeaderMap,
314 body: Option<Vec<u8>>,
315 timeout: Duration,
316 ) -> Result<(Vec<u8>, u16, HeaderMap)> {
317 let t0 = Instant::now();
318 tracing::debug!(%endpoint, "->");
319 if tracing::enabled!(tracing::Level::TRACE) {
320 tracing::trace!(%endpoint, headers = ?redacted(&headers),
321 body = %body.as_deref().map(String::from_utf8_lossy).unwrap_or_default(), "->");
322 }
323 let mut req = self
324 .inner
325 .http
326 .request(method.clone(), url)
327 .headers(headers)
328 .timeout(timeout);
329 if let Some(b) = body {
330 req = req.body(b);
331 }
332 let map_err = |e: reqwest::Error| {
333 tracing::debug!(%endpoint, error = %e, "<- transport error");
334 if e.is_timeout() {
335 Error::Timeout(timeout)
336 } else {
337 Error::Connection(Box::new(e))
338 }
339 };
340 let resp = req.send().await.map_err(map_err)?;
341 let status = resp.status();
342 let resp_headers = resp.headers().clone();
343 let bytes = resp.bytes().await.map_err(map_err)?;
344
345 tracing::debug!(
346 %endpoint,
347 status = status.as_u16(),
348 elapsed_ms = t0.elapsed().as_millis() as u64,
349 request_id = resp_headers.get(REQUEST_ID_HEADER).and_then(|v| v.to_str().ok()).unwrap_or("-"),
350 "<-"
351 );
352 if tracing::enabled!(tracing::Level::TRACE) {
353 tracing::trace!(%endpoint, headers = ?redacted(&resp_headers),
354 body = %String::from_utf8_lossy(&bytes), "<-");
355 }
356
357 if !status.is_success() {
358 return Err(Error::Api(Box::new(ApiError::new(
359 status.as_u16(),
360 lenient_body(&bytes),
361 resp_headers,
362 Some(endpoint.to_owned()),
363 ))));
364 }
365 Ok((bytes.to_vec(), status.as_u16(), resp_headers))
366 }
367}
368
369fn validation_error(
370 status: StatusCode,
371 body: Option<Value>,
372 headers: HeaderMap,
373 endpoint: &str,
374 f: DecodeFailure,
375) -> Error {
376 Error::ResponseValidation(Box::new(ResponseValidationError {
377 status: status.as_u16(),
378 field_path: f.path,
379 detail: f.detail,
380 body,
381 headers,
382 endpoint: Some(endpoint.to_owned()),
383 }))
384}
385
386fn redact_url(url: &str) -> String {
387 match reqwest::Url::parse(url) {
388 Ok(mut u) => {
389 let _ = u.set_username("");
390 let _ = u.set_password(None);
391 u.set_query(None);
392 u.set_fragment(None);
393 u.to_string()
394 }
395 Err(_) => url.to_owned(),
396 }
397}
398
399fn redacted(headers: &HeaderMap) -> Vec<(String, String)> {
400 headers
401 .iter()
402 .map(|(k, v)| {
403 let value = if SECRET_HEADERS.contains(&k.as_str()) {
404 "[REDACTED]".to_owned()
405 } else {
406 v.to_str().unwrap_or("<binary>").to_owned()
407 };
408 (k.as_str().to_owned(), value)
409 })
410 .collect()
411}
412
413#[derive(Debug, Default, Clone)]
414struct CallOptions {
415 retry: Option<RetryPolicy>,
416 timeout: Option<Duration>,
417 headers: HeaderMap,
418}
419
420macro_rules! call_option_methods {
421 () => {
422 pub fn retry(mut self, policy: RetryPolicy) -> Self {
424 self.opts.retry = Some(policy);
425 self
426 }
427
428 pub fn timeout(mut self, timeout: Duration) -> Self {
430 self.opts.timeout = Some(timeout);
431 self
432 }
433
434 pub fn header(mut self, name: HeaderName, value: HeaderValue) -> Self {
436 self.opts.headers.insert(name, value);
437 self
438 }
439 };
440}
441
442#[must_use = "requests do nothing until awaited"]
444#[derive(Debug)]
445pub struct SystemOneRequest {
446 client: Client,
447 state: std::result::Result<Value, String>,
448 questions: Questions,
449 model: Option<String>,
450 extra_body: Map<String, Value>,
451 opts: CallOptions,
452}
453
454impl SystemOneRequest {
455 call_option_methods!();
456
457 pub fn model(mut self, model: impl Into<String>) -> Self {
459 self.model = Some(model.into());
460 self
461 }
462
463 pub fn extra_body(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
466 self.extra_body.insert(key.into(), value.into());
467 self
468 }
469
470 pub async fn send(self) -> Result<SystemOneResponse> {
472 let state = self.state.map_err(Error::InvalidRequest)?;
473 self.questions.validate()?;
474 let model = self
475 .model
476 .unwrap_or_else(|| self.client.inner.model.clone());
477 let extra = &self.extra_body;
480 let body = SystemOneBody {
481 state: (!extra.contains_key("state")).then_some(&state),
482 model: (!extra.contains_key("model")).then_some(&model),
483 questions: (!extra.contains_key("questions")).then_some(&self.questions),
484 extra,
485 };
486 let bytes = serde_json::to_vec(&body).map_err(|e| {
487 Error::InvalidRequest(format!(
488 "The request body could not be encoded as JSON: {e}"
489 ))
490 })?;
491
492 let (bytes, meta, endpoint) = self
493 .client
494 .execute(Method::POST, SYSTEM_ONE_PATH, Some(bytes), self.opts)
495 .await?;
496 match decode_system_one(&bytes) {
497 Ok(DecodedSystemOne {
498 model,
499 usage,
500 answers,
501 raw,
502 }) => Ok(SystemOneResponse {
503 model,
504 usage,
505 answers,
506 raw,
507 meta,
508 }),
509 Err(f) => Err(validation_error(
510 StatusCode::from_u16(meta.status).unwrap_or(StatusCode::OK),
511 lenient_body(&bytes),
512 meta.headers,
513 &endpoint,
514 f,
515 )),
516 }
517 }
518}
519
520#[derive(Serialize)]
521struct SystemOneBody<'a> {
522 #[serde(skip_serializing_if = "Option::is_none")]
523 state: Option<&'a Value>,
524 #[serde(skip_serializing_if = "Option::is_none")]
525 model: Option<&'a str>,
526 #[serde(skip_serializing_if = "Option::is_none")]
527 questions: Option<&'a Questions>,
528 #[serde(flatten)]
529 extra: &'a Map<String, Value>,
530}
531
532impl IntoFuture for SystemOneRequest {
533 type Output = Result<SystemOneResponse>;
534 type IntoFuture = BoxFuture<'static, Self::Output>;
535
536 fn into_future(self) -> Self::IntoFuture {
537 Box::pin(self.send())
538 }
539}
540
541#[derive(Debug, Clone)]
543pub struct Models {
544 client: Client,
545}
546
547impl Models {
548 pub fn list(&self) -> ListModelsRequest {
550 ListModelsRequest {
551 client: self.client.clone(),
552 opts: CallOptions::default(),
553 }
554 }
555}
556
557#[must_use = "requests do nothing until awaited"]
559#[derive(Debug)]
560pub struct ListModelsRequest {
561 client: Client,
562 opts: CallOptions,
563}
564
565impl ListModelsRequest {
566 call_option_methods!();
567
568 pub async fn send(self) -> Result<ListModelsResponse> {
570 let (bytes, meta, endpoint) = self
571 .client
572 .execute(Method::GET, MODELS_PATH, None, self.opts)
573 .await?;
574 match decode_models(&bytes) {
575 Ok((models, raw)) => Ok(ListModelsResponse { models, raw, meta }),
576 Err(f) => Err(validation_error(
577 StatusCode::from_u16(meta.status).unwrap_or(StatusCode::OK),
578 lenient_body(&bytes),
579 meta.headers,
580 &endpoint,
581 f,
582 )),
583 }
584 }
585}
586
587impl IntoFuture for ListModelsRequest {
588 type Output = Result<ListModelsResponse>;
589 type IntoFuture = BoxFuture<'static, Self::Output>;
590
591 fn into_future(self) -> Self::IntoFuture {
592 Box::pin(self.send())
593 }
594}