1use std::sync::Arc;
2use std::time::Duration;
3
4use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7
8use crate::error::{ApiError, ApiErrorBody, Error, Result};
9
10const MAX_RETRIES: u32 = 3;
12const INITIAL_BACKOFF_MS: u64 = 500;
14
15fn is_retryable(status: reqwest::StatusCode) -> bool {
17 matches!(status.as_u16(), 429 | 502 | 503 | 504)
18}
19
20pub const DEFAULT_BASE_URL: &str = "https://api.quantumencoding.ai";
22
23pub const TICKS_PER_USD: i64 = 10_000_000_000;
25
26#[derive(Debug, Clone, Default)]
28pub struct ResponseMeta {
29 pub cost_ticks: i64,
31 pub request_id: String,
33 pub model: String,
35}
36
37pub struct ClientBuilder {
39 api_key: String,
40 base_url: String,
41 timeout: Duration,
42}
43
44impl ClientBuilder {
45 pub fn new(api_key: impl Into<String>) -> Self {
47 Self {
48 api_key: api_key.into(),
49 base_url: DEFAULT_BASE_URL.to_string(),
50 timeout: Duration::from_secs(60),
51 }
52 }
53
54 pub fn base_url(mut self, url: impl Into<String>) -> Self {
56 self.base_url = url.into();
57 self
58 }
59
60 pub fn timeout(mut self, timeout: Duration) -> Self {
62 self.timeout = timeout;
63 self
64 }
65
66 pub fn build(self) -> Result<Client> {
68 let auth_value = format!("Bearer {}", self.api_key);
69 let auth_header = HeaderValue::from_str(&auth_value).map_err(|_| {
70 Error::Api(ApiError {
71 status_code: 0,
72 code: "invalid_api_key".to_string(),
73 message: "API key contains invalid header characters".to_string(),
74 request_id: String::new(),
75 })
76 })?;
77
78 let mut headers = HeaderMap::new();
79 headers.insert(AUTHORIZATION, auth_header.clone());
80 if let Ok(v) = HeaderValue::from_str(&self.api_key) {
82 headers.insert("X-API-Key", v);
83 }
84
85 let http = reqwest::Client::builder()
86 .default_headers(headers)
87 .timeout(self.timeout)
88 .build()?;
89
90 Ok(Client {
91 inner: Arc::new(ClientInner {
92 base_url: self.base_url,
93 http,
94 auth_header,
95 }),
96 })
97 }
98}
99
100struct ClientInner {
101 base_url: String,
102 http: reqwest::Client,
103 auth_header: HeaderValue,
104}
105
106#[derive(Clone)]
116pub struct Client {
117 inner: Arc<ClientInner>,
118}
119
120impl Client {
121 pub fn new(api_key: impl Into<String>) -> Self {
123 ClientBuilder::new(api_key)
124 .build()
125 .expect("default client configuration is valid")
126 }
127
128 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
130 ClientBuilder::new(api_key)
131 }
132
133 pub(crate) fn base_url(&self) -> &str {
135 &self.inner.base_url
136 }
137
138 pub(crate) fn auth_header(&self) -> &HeaderValue {
140 &self.inner.auth_header
141 }
142
143 pub async fn post_json<Req: Serialize, Resp: DeserializeOwned>(
145 &self,
146 path: &str,
147 body: &Req,
148 ) -> Result<(Resp, ResponseMeta)> {
149 let url = format!("{}{}", self.inner.base_url, path);
150 let body_bytes = serde_json::to_vec(body)?;
151
152 let mut last_err = None;
153 for attempt in 0..=MAX_RETRIES {
154 if attempt > 0 {
155 let delay = INITIAL_BACKOFF_MS * 2u64.pow(attempt - 1);
156 eprintln!("[sdk] Retry {attempt}/{MAX_RETRIES} for POST {path} in {delay}ms");
157 tokio::time::sleep(Duration::from_millis(delay)).await;
158 }
159
160 let resp = self
161 .inner
162 .http
163 .post(&url)
164 .header(CONTENT_TYPE, "application/json")
165 .body(body_bytes.clone())
166 .send()
167 .await?;
168
169 let status = resp.status();
170 let meta = parse_response_meta(&resp);
171
172 if status.is_success() {
173 let body_text = resp.text().await?;
174 let result: Resp = serde_json::from_str(&body_text).map_err(|e| {
175 let preview = if body_text.len() > 300 { &body_text[..300] } else { &body_text };
176 eprintln!("[sdk] JSON decode error on {path}: {e}\n body preview: {preview}");
177 e
178 })?;
179 return Ok((result, meta));
180 }
181
182 if is_retryable(status) && attempt < MAX_RETRIES {
183 eprintln!("[sdk] POST {path} returned {status}, will retry");
184 last_err = Some(parse_api_error(resp, &meta.request_id).await);
185 continue;
186 }
187
188 return Err(parse_api_error(resp, &meta.request_id).await);
189 }
190
191 Err(last_err.unwrap_or_else(|| Error::Api(ApiError {
192 status_code: 502,
193 code: "retry_exhausted".into(),
194 message: format!("max retries ({MAX_RETRIES}) exceeded"),
195 request_id: String::new(),
196 })))
197 }
198
199 pub async fn get_json<Resp: DeserializeOwned>(
201 &self,
202 path: &str,
203 ) -> Result<(Resp, ResponseMeta)> {
204 let url = format!("{}{}", self.inner.base_url, path);
205
206 let mut last_err = None;
207 for attempt in 0..=MAX_RETRIES {
208 if attempt > 0 {
209 let delay = INITIAL_BACKOFF_MS * 2u64.pow(attempt - 1);
210 eprintln!("[sdk] Retry {attempt}/{MAX_RETRIES} for GET {path} in {delay}ms");
211 tokio::time::sleep(Duration::from_millis(delay)).await;
212 }
213
214 let resp = self.inner.http.get(&url).send().await?;
215 let status = resp.status();
216 let meta = parse_response_meta(&resp);
217
218 if status.is_success() {
219 let body_text = resp.text().await?;
220 let result: Resp = serde_json::from_str(&body_text).map_err(|e| {
221 let preview = if body_text.len() > 300 { &body_text[..300] } else { &body_text };
222 eprintln!("[sdk] JSON decode error on {path}: {e}\n body preview: {preview}");
223 e
224 })?;
225 return Ok((result, meta));
226 }
227
228 if is_retryable(status) && attempt < MAX_RETRIES {
229 eprintln!("[sdk] GET {path} returned {status}, will retry");
230 last_err = Some(parse_api_error(resp, &meta.request_id).await);
231 continue;
232 }
233
234 return Err(parse_api_error(resp, &meta.request_id).await);
235 }
236
237 Err(last_err.unwrap_or_else(|| Error::Api(ApiError {
238 status_code: 502,
239 code: "retry_exhausted".into(),
240 message: format!("max retries ({MAX_RETRIES}) exceeded"),
241 request_id: String::new(),
242 })))
243 }
244
245 pub async fn delete_json<Resp: DeserializeOwned>(
247 &self,
248 path: &str,
249 ) -> Result<(Resp, ResponseMeta)> {
250 let url = format!("{}{}", self.inner.base_url, path);
251 let resp = self.inner.http.delete(&url).send().await?;
252
253 let meta = parse_response_meta(&resp);
254
255 if !resp.status().is_success() {
256 return Err(parse_api_error(resp, &meta.request_id).await);
257 }
258
259 let result: Resp = resp.json().await?;
260 Ok((result, meta))
261 }
262
263 pub async fn post_multipart<Resp: DeserializeOwned>(
265 &self,
266 path: &str,
267 form: reqwest::multipart::Form,
268 ) -> Result<(Resp, ResponseMeta)> {
269 let url = format!("{}{}", self.inner.base_url, path);
270 let resp = self.inner.http.post(&url).multipart(form).send().await?;
271
272 let meta = parse_response_meta(&resp);
273
274 if !resp.status().is_success() {
275 return Err(parse_api_error(resp, &meta.request_id).await);
276 }
277
278 let result: Resp = resp.json().await?;
279 Ok((result, meta))
280 }
281
282 pub async fn get_stream_raw(
286 &self,
287 path: &str,
288 ) -> Result<(reqwest::Response, ResponseMeta)> {
289 let url = format!("{}{}", self.inner.base_url, path);
290
291 let stream_client = reqwest::Client::builder().build()?;
292
293 let resp = stream_client
294 .get(&url)
295 .header(AUTHORIZATION, self.inner.auth_header.clone())
296 .header("Accept", "text/event-stream")
297 .send()
298 .await?;
299
300 let meta = parse_response_meta(&resp);
301
302 if !resp.status().is_success() {
303 return Err(parse_api_error(resp, &meta.request_id).await);
304 }
305
306 Ok((resp, meta))
307 }
308
309 pub async fn post_stream_raw(
313 &self,
314 path: &str,
315 body: &impl Serialize,
316 ) -> Result<(reqwest::Response, ResponseMeta)> {
317 let url = format!("{}{}", self.inner.base_url, path);
318
319 let stream_client = reqwest::Client::builder().build()?;
321
322 let resp = stream_client
323 .post(&url)
324 .header(AUTHORIZATION, self.inner.auth_header.clone())
325 .header(CONTENT_TYPE, "application/json")
326 .header("Accept", "text/event-stream")
327 .json(body)
328 .send()
329 .await?;
330
331 let meta = parse_response_meta(&resp);
332
333 if !resp.status().is_success() {
334 return Err(parse_api_error(resp, &meta.request_id).await);
335 }
336
337 Ok((resp, meta))
338 }
339}
340
341fn parse_response_meta(resp: &reqwest::Response) -> ResponseMeta {
343 let headers = resp.headers();
344 let request_id = headers
345 .get("X-QAI-Request-Id")
346 .and_then(|v| v.to_str().ok())
347 .unwrap_or("")
348 .to_string();
349 let model = headers
350 .get("X-QAI-Model")
351 .and_then(|v| v.to_str().ok())
352 .unwrap_or("")
353 .to_string();
354 let cost_ticks = headers
355 .get("X-QAI-Cost-Ticks")
356 .and_then(|v| v.to_str().ok())
357 .and_then(|v| v.parse::<i64>().ok())
358 .unwrap_or(0);
359
360 ResponseMeta {
361 cost_ticks,
362 request_id,
363 model,
364 }
365}
366
367async fn parse_api_error(resp: reqwest::Response, request_id: &str) -> Error {
369 let status_code = resp.status().as_u16();
370 let status_text = resp
371 .status()
372 .canonical_reason()
373 .unwrap_or("Unknown")
374 .to_string();
375
376 let body = resp.text().await.unwrap_or_default();
377
378 let (code, message) = if let Ok(err_body) = serde_json::from_str::<ApiErrorBody>(&body) {
379 let msg = if err_body.error.message.is_empty() {
380 body.clone()
381 } else {
382 err_body.error.message
383 };
384 let c = if !err_body.error.code.is_empty() {
385 err_body.error.code
386 } else if !err_body.error.error_type.is_empty() {
387 err_body.error.error_type
388 } else {
389 status_text
390 };
391 (c, msg)
392 } else {
393 (status_text, body)
394 };
395
396 Error::Api(ApiError {
397 status_code,
398 code,
399 message,
400 request_id: request_id.to_string(),
401 })
402}