1use std::sync::Arc;
2use std::time::Duration;
3
4use bytes::Bytes;
5use http::{HeaderMap, HeaderValue, Method, header};
6use serde::Serialize;
7use url::Url;
8
9use crate::config::{
10 CallOptions, ClientConfig, DEFAULT_BASE_URL, DEFAULT_MODEL, strip_trailing_slashes,
11};
12use crate::error::{ApiError, Error, parse_error_body};
13use crate::headers::{
14 merge_user_headers, request_id, retry_count_value, runtime_header, sdk_header, user_agent,
15};
16use crate::retry::RetryPolicy;
17use crate::transport::{HttpTransport, RawResponse, join_endpoint};
18use crate::types::{
19 ListModelsResponse, ModelCard, Questions, ResponseMeta, SystemOneRequest, SystemOneResponse,
20 validate_questions,
21};
22
23#[derive(Debug)]
24struct Inner {
25 http: HttpTransport,
26 api_key: crate::config::SecretString,
27 base_url: Url,
28 default_model: String,
29 timeout: Duration,
30 retry: RetryPolicy,
31 default_headers: HeaderMap,
32}
33
34#[derive(Clone, Debug)]
56pub struct Client {
57 inner: Arc<Inner>,
58}
59
60impl Client {
61 pub fn new(config: ClientConfig) -> Result<Self, Error> {
68 Self::new_with_env(config, |key| std::env::var(key).ok())
69 }
70
71 pub fn from_env() -> Result<Self, Error> {
73 Self::new(ClientConfig::default())
74 }
75
76 pub fn new_with_env(
80 mut config: ClientConfig,
81 lookup: impl FnMut(&str) -> Option<String>,
82 ) -> Result<Self, Error> {
83 config.overlay_env(lookup)?;
84 if config.timeout.is_zero() {
85 return Err(Error::InvalidRequest(
86 "`timeout` must be a positive duration".to_owned(),
87 ));
88 }
89 config.retry.validate()?;
90 let api_key = config.api_key.ok_or(Error::MissingApiKey)?;
91 let base_url =
92 strip_trailing_slashes(config.base_url.unwrap_or_else(|| {
93 Url::parse(DEFAULT_BASE_URL).expect("default base URL is valid")
94 }));
95 let default_model = config
96 .default_model
97 .filter(|m| !m.trim().is_empty())
98 .unwrap_or_else(|| DEFAULT_MODEL.to_owned());
99 Ok(Self {
100 inner: Arc::new(Inner {
101 http: HttpTransport::new()?,
102 api_key,
103 base_url,
104 default_model,
105 timeout: config.timeout,
106 retry: config.retry,
107 default_headers: config.default_headers,
108 }),
109 })
110 }
111
112 #[must_use]
114 pub fn default_model(&self) -> &str {
115 &self.inner.default_model
116 }
117
118 #[must_use]
120 pub fn base_url(&self) -> &Url {
121 &self.inner.base_url
122 }
123
124 pub async fn system_one(
135 &self,
136 state: impl Serialize,
137 questions: Questions,
138 ) -> Result<SystemOneResponse, Error> {
139 let state = serde_json::to_value(state).map_err(|err| {
140 Error::InvalidRequest(format!("state is not JSON-serializable: {err}"))
141 })?;
142 let req = SystemOneRequest::new(state, questions);
143 self.system_one_with(&req, CallOptions::default()).await
144 }
145
146 pub async fn system_one_with(
148 &self,
149 req: &SystemOneRequest,
150 opts: CallOptions,
151 ) -> Result<SystemOneResponse, Error> {
152 validate_questions(&req.questions)?;
153 let model = opts
154 .model
155 .as_deref()
156 .filter(|m| !m.is_empty())
157 .or_else(|| {
158 if req.model.trim().is_empty() {
159 None
160 } else {
161 Some(req.model.as_str())
162 }
163 })
164 .unwrap_or(self.inner.default_model.as_str())
165 .to_owned();
166 let payload = serde_json::json!({
167 "state": req.state,
168 "model": model,
169 "questions": req.questions,
170 });
171 let body =
172 Bytes::from(serde_json::to_vec(&payload).map_err(|err| {
173 Error::InvalidRequest(format!("failed to serialize request: {err}"))
174 })?);
175
176 let raw = {
177 let fut = self.execute(Method::POST, "/v1/systemone", Some(body), &opts);
178 #[cfg(feature = "tracing")]
179 {
180 use tracing::Instrument;
181 let span = tracing::info_span!(
182 "typesafe.request",
183 http.request.method = "POST",
184 url.path = "/v1/systemone",
185 typesafe.model = model.as_str(),
186 typesafe.questions.count = req.questions.len(),
187 );
188 fut.instrument(span).await?
189 }
190 #[cfg(not(feature = "tracing"))]
191 {
192 fut.await?
193 }
194 };
195 let mut parsed: SystemOneResponse = decode_json(&raw, "/v1/systemone")?;
196 parsed.meta = meta_from_raw(&raw);
197 Ok(parsed)
198 }
199
200 #[must_use]
202 pub fn models(&self) -> Models<'_> {
203 Models { client: self }
204 }
205
206 pub async fn warm_up(&self) -> Result<(), Error> {
208 let raw = self
209 .execute(Method::GET, "/v1/models", None, &CallOptions::default())
210 .await?;
211 let _ = raw;
212 Ok(())
213 }
214
215 async fn execute(
216 &self,
217 method: Method,
218 path: &'static str,
219 body: Option<Bytes>,
220 opts: &CallOptions,
221 ) -> Result<RawResponse, Error> {
222 let timeout = opts.timeout.unwrap_or(self.inner.timeout);
223 if timeout.is_zero() {
224 return Err(Error::InvalidRequest(
225 "`timeout` must be a positive duration".to_owned(),
226 ));
227 }
228 let retry = opts
229 .retry
230 .clone()
231 .unwrap_or_else(|| self.inner.retry.clone());
232 retry.validate()?;
233
234 let url = join_endpoint(&self.inner.base_url, path)?;
235 let mut attempt = 0_u32;
236 loop {
237 let retries_left = retry.max_retries.saturating_sub(attempt);
238 let headers = self.attempt_headers(opts, body.is_some(), attempt);
239 match self
240 .inner
241 .http
242 .send(method.clone(), url.clone(), headers, body.clone(), timeout)
243 .await
244 {
245 Ok(mut raw) if raw.status.is_success() => {
246 raw.attempts = attempt + 1;
247 return Ok(raw);
248 }
249 Ok(raw) => {
250 let retryable = retry.http_statuses.contains_status(raw.status);
251 if retries_left > 0 && retryable {
252 let delay = retry.delay_after_failure(attempt, Some(&raw.headers));
253 emit_retry(attempt, delay, &raw.status.to_string());
254 tokio::time::sleep(delay).await;
255 attempt += 1;
256 continue;
257 }
258 let body = parse_error_body(&raw.body);
259 return Err(Error::Api(Box::new(ApiError::from_response(
260 raw.status,
261 body,
262 raw.headers,
263 path,
264 attempt + 1,
265 ))));
266 }
267 Err(err) => {
268 let retryable = match &err {
269 Error::Timeout { .. } => retry.retry_timeouts,
270 Error::Connection(te) => retry.retries_connection(te.is_pre_send()),
271 _ => false,
272 };
273 if retries_left > 0 && retryable {
274 let delay = retry.delay_after_failure(attempt, None);
275 emit_retry(attempt, delay, &err.to_string());
276 tokio::time::sleep(delay).await;
277 attempt += 1;
278 continue;
279 }
280 return Err(err);
281 }
282 }
283 }
284 }
285
286 fn attempt_headers(&self, opts: &CallOptions, has_body: bool, attempt: u32) -> HeaderMap {
287 let mut headers = HeaderMap::new();
288 merge_user_headers(&mut headers, &self.inner.default_headers);
289 merge_user_headers(&mut headers, &opts.headers);
290 headers.insert(
291 header::AUTHORIZATION,
292 HeaderValue::from_str(&format!("Bearer {}", self.inner.api_key.expose()))
293 .unwrap_or_else(|_| HeaderValue::from_static("Bearer")),
294 );
295 headers.insert(header::ACCEPT, HeaderValue::from_static("application/json"));
296 headers.insert(header::USER_AGENT, user_agent());
297 headers.insert("x-typesafe-sdk", sdk_header());
298 headers.insert("x-typesafe-runtime", runtime_header());
299 if has_body {
300 headers.insert(
301 header::CONTENT_TYPE,
302 HeaderValue::from_static("application/json"),
303 );
304 }
305 if attempt > 0 {
306 headers.insert("x-typesafe-retry-count", retry_count_value(attempt));
307 } else {
308 headers.remove("x-typesafe-retry-count");
309 }
310 headers
311 }
312}
313
314#[derive(Clone, Copy, Debug)]
316pub struct Models<'a> {
317 client: &'a Client,
318}
319
320impl Models<'_> {
321 pub async fn list(&self) -> Result<Vec<ModelCard>, Error> {
325 self.list_with(&CallOptions::default()).await
326 }
327
328 pub async fn list_with(&self, opts: &CallOptions) -> Result<Vec<ModelCard>, Error> {
330 let raw = self
331 .client
332 .execute(Method::GET, "/v1/models", None, opts)
333 .await?;
334 let value: serde_json::Value = decode_json(&raw, "/v1/models")?;
335 match value.get("models") {
336 Some(serde_json::Value::Array(_)) => {
337 let parsed: ListModelsResponse =
338 serde_json::from_value(value).map_err(|source| Error::Decode {
339 source,
340 body: raw.body.clone(),
341 meta: Box::new(meta_from_raw(&raw)),
342 })?;
343 Ok(parsed.models)
344 }
345 _ => Err(Error::UnexpectedShape {
346 endpoint: "GET /v1/models",
347 meta: Box::new(meta_from_raw(&raw)),
348 }),
349 }
350 }
351}
352
353fn decode_json<T: serde::de::DeserializeOwned>(
354 raw: &RawResponse,
355 endpoint: &'static str,
356) -> Result<T, Error> {
357 serde_json::from_slice(&raw.body).map_err(|source| {
358 let _ = endpoint;
359 Error::Decode {
360 source,
361 body: raw.body.clone(),
362 meta: Box::new(meta_from_raw(raw)),
363 }
364 })
365}
366
367fn meta_from_raw(raw: &RawResponse) -> ResponseMeta {
368 ResponseMeta {
369 request_id: request_id(&raw.headers),
370 status: Some(raw.status),
371 headers: raw.headers.clone(),
372 attempts: raw.attempts,
373 }
374}
375
376fn emit_retry(attempt: u32, delay: Duration, reason: &str) {
377 let _ = (attempt, delay, reason);
378 #[cfg(feature = "tracing")]
379 {
380 tracing::info!(
381 attempt,
382 delay_ms = delay.as_millis() as u64,
383 reason,
384 "retry_scheduled"
385 );
386 }
387}