kunobi_decision/client/
mod.rs1mod builder;
4mod call;
5mod env;
6mod models;
7mod provider;
8pub use provider::Provider;
9mod transport;
10
11use std::fmt;
12use std::sync::Arc;
13use std::sync::atomic::AtomicU64;
14use std::time::Duration;
15
16use reqwest::Method;
17use reqwest::header::HeaderMap;
18
19pub use builder::ClientBuilder;
20pub use call::{Call, RawResponse, WithResponse};
21pub use env::{ENV_API_KEY, ENV_BASE_URL, ENV_DEFAULT_MODEL};
22pub use models::Models;
23
24use crate::answers::SystemOneResult;
25use crate::credentials::Credentials;
26use crate::request::SystemOneRequest;
27use crate::retry::RetryPolicy;
28
29pub const DEFAULT_BASE_URL: &str = "https://api.typesafe.ai";
31
32pub const DEFAULT_MODEL: &str = "jev-latest";
34
35#[derive(Clone)]
39pub struct Client {
40 inner: Arc<Inner>,
41}
42
43struct Inner {
44 provider: Provider,
45 credentials: Credentials,
46 base_url: String,
47 log_bodies: bool,
48 default_model: String,
49 retry: RetryPolicy,
50 timeout: Duration,
51 total_timeout: Option<Duration>,
52 limiter: Option<(tokio::sync::Semaphore, usize)>,
54 default_headers: HeaderMap,
55 http: reqwest::Client,
56 request_count: AtomicU64,
58}
59
60impl Client {
61 pub fn new() -> crate::Result<Self> {
67 Self::builder().build()
68 }
69
70 pub fn builder() -> ClientBuilder {
72 ClientBuilder::default()
73 }
74
75 pub fn provider(&self) -> &Provider {
77 &self.inner.provider
78 }
79
80 pub fn base_url(&self) -> &str {
82 &self.inner.base_url
83 }
84
85 pub fn default_model(&self) -> &str {
87 &self.inner.default_model
88 }
89
90 pub fn retry(&self) -> &RetryPolicy {
92 &self.inner.retry
93 }
94
95 pub fn timeout(&self) -> Duration {
97 self.inner.timeout
98 }
99
100 pub fn total_timeout(&self) -> Option<Duration> {
102 self.inner.total_timeout
103 }
104
105 pub fn max_concurrent_requests(&self) -> Option<usize> {
107 self.inner.limiter.as_ref().map(|(_, max)| *max)
108 }
109
110 pub fn default_headers(&self) -> &HeaderMap {
112 &self.inner.default_headers
113 }
114
115 pub fn system_one(&self, request: SystemOneRequest) -> Call<SystemOneResult> {
134 let prepared = self.provider().prepare(request, self.default_model());
135 let (path, body) = match prepared {
136 Ok((path, body)) => (path, Ok(Some(body))),
137 Err(err) => (String::new(), Err(err)),
138 };
139 let parse = if matches!(self.provider(), Provider::Cloudflare { .. }) {
140 provider::unwrap_cloudflare::<SystemOneResult>
141 } else {
142 call::parse_json
143 };
144 Call::new(self.clone(), Method::POST, path, body, parse)
145 }
146
147 pub async fn check(&self) -> crate::Result<Vec<crate::ModelCard>> {
153 let check = async {
154 if matches!(self.provider(), Provider::OpenRouter) {
155 Call::new(
156 self.clone(),
157 Method::GET,
158 "/v1/key",
159 Ok(None),
160 parse_openrouter_key,
161 )
162 .await?;
163 }
164 self.models().list().await
165 };
166 match self.total_timeout() {
167 Some(timeout) => tokio::time::timeout(timeout, check)
168 .await
169 .unwrap_or(Err(crate::Error::Timeout { timeout })),
170 None => check.await,
171 }
172 }
173
174 pub fn models(&self) -> Models {
176 Models::new(self.clone())
177 }
178}
179
180impl fmt::Debug for Client {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 f.debug_struct("Client")
183 .field("provider", &self.inner.provider)
184 .field("base_url", &self.inner.base_url)
185 .field("default_model", &self.inner.default_model)
186 .field("retry", &self.inner.retry)
187 .field("timeout", &self.inner.timeout)
188 .field("total_timeout", &self.inner.total_timeout)
189 .field("max_concurrent_requests", &self.max_concurrent_requests())
190 .field("credentials", &self.inner.credentials)
191 .field("log_bodies", &self.inner.log_bodies)
192 .finish_non_exhaustive()
193 }
194}
195
196fn parse_openrouter_key(body: &[u8]) -> Result<(), call::DecodeFailure> {
197 #[derive(serde::Deserialize)]
198 struct Key {
199 data: serde_json::Map<String, serde_json::Value>,
200 }
201 let key: Key = call::parse_json(body)?;
202 drop(key.data);
203 Ok(())
204}