mod builder;
mod call;
mod env;
mod models;
mod provider;
pub use provider::Provider;
mod transport;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
use reqwest::Method;
use reqwest::header::HeaderMap;
pub use builder::ClientBuilder;
pub use call::{Call, RawResponse, WithResponse};
pub use env::{ENV_API_KEY, ENV_BASE_URL, ENV_DEFAULT_MODEL};
pub use models::Models;
use crate::answers::SystemOneResult;
use crate::credentials::Credentials;
use crate::request::SystemOneRequest;
use crate::retry::RetryPolicy;
pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai";
pub const DEFAULT_MODEL: &str = "jev-latest";
#[derive(Clone)]
pub struct Client {
inner: Arc<Inner>,
}
struct Inner {
provider: Provider,
credentials: Credentials,
base_url: String,
log_bodies: bool,
default_model: String,
retry: RetryPolicy,
timeout: Duration,
total_timeout: Option<Duration>,
limiter: Option<(tokio::sync::Semaphore, usize)>,
default_headers: HeaderMap,
http: reqwest::Client,
request_count: AtomicU64,
}
impl Client {
pub fn new() -> crate::Result<Self> {
Self::builder().build()
}
pub fn builder() -> ClientBuilder {
ClientBuilder::default()
}
pub fn provider(&self) -> &Provider {
&self.inner.provider
}
pub fn base_url(&self) -> &str {
&self.inner.base_url
}
pub fn default_model(&self) -> &str {
&self.inner.default_model
}
pub fn retry(&self) -> &RetryPolicy {
&self.inner.retry
}
pub fn timeout(&self) -> Duration {
self.inner.timeout
}
pub fn total_timeout(&self) -> Option<Duration> {
self.inner.total_timeout
}
pub fn max_concurrent_requests(&self) -> Option<usize> {
self.inner.limiter.as_ref().map(|(_, max)| *max)
}
pub fn default_headers(&self) -> &HeaderMap {
&self.inner.default_headers
}
pub fn system_one(&self, request: SystemOneRequest) -> Call<SystemOneResult> {
let prepared = self.provider().prepare(request, self.default_model());
let (path, body) = match prepared {
Ok((path, body)) => (path, Ok(Some(body))),
Err(err) => (String::new(), Err(err)),
};
let parse = if matches!(self.provider(), Provider::Cloudflare { .. }) {
provider::unwrap_cloudflare::<SystemOneResult>
} else {
call::parse_json
};
Call::new(self.clone(), Method::POST, path, body, parse)
}
pub async fn check(&self) -> crate::Result<Vec<crate::ModelCard>> {
let check = async {
if matches!(self.provider(), Provider::OpenRouter) {
Call::new(
self.clone(),
Method::GET,
"/v1/key",
Ok(None),
parse_openrouter_key,
)
.await?;
}
self.models().list().await
};
match self.total_timeout() {
Some(timeout) => tokio::time::timeout(timeout, check)
.await
.unwrap_or(Err(crate::Error::Timeout { timeout })),
None => check.await,
}
}
pub fn models(&self) -> Models {
Models::new(self.clone())
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Client")
.field("provider", &self.inner.provider)
.field("base_url", &self.inner.base_url)
.field("default_model", &self.inner.default_model)
.field("retry", &self.inner.retry)
.field("timeout", &self.inner.timeout)
.field("total_timeout", &self.inner.total_timeout)
.field("max_concurrent_requests", &self.max_concurrent_requests())
.field("credentials", &self.inner.credentials)
.field("log_bodies", &self.inner.log_bodies)
.finish_non_exhaustive()
}
}
fn parse_openrouter_key(body: &[u8]) -> Result<(), call::DecodeFailure> {
#[derive(serde::Deserialize)]
struct Key {
data: serde_json::Map<String, serde_json::Value>,
}
let key: Key = call::parse_json(body)?;
drop(key.data);
Ok(())
}