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
20fn is_permanent_error(body: &str) -> bool {
23 let lower = body.to_lowercase();
24 lower.contains("content moderation")
25 || lower.contains("content_policy")
26 || lower.contains("safety_block")
27 || lower.contains("invalid argument")
28 || lower.contains("invalid_request")
29 || (lower.contains("status 400") && lower.contains("rejected"))
30}
31
32pub const DEFAULT_BASE_URL: &str = "https://api.quantumencoding.ai";
34
35pub const TICKS_PER_USD: i64 = 10_000_000_000;
37
38#[derive(Debug, Clone, Default)]
40pub struct ResponseMeta {
41 pub cost_ticks: i64,
43 pub request_id: String,
45 pub model: String,
47}
48
49pub struct ClientBuilder {
51 api_key: String,
52 base_url: String,
53 timeout: Duration,
54}
55
56impl ClientBuilder {
57 pub fn new(api_key: impl Into<String>) -> Self {
59 Self {
60 api_key: api_key.into(),
61 base_url: DEFAULT_BASE_URL.to_string(),
62 timeout: Duration::from_secs(60),
63 }
64 }
65
66 pub fn base_url(mut self, url: impl Into<String>) -> Self {
68 self.base_url = url.into();
69 self
70 }
71
72 pub fn timeout(mut self, timeout: Duration) -> Self {
74 self.timeout = timeout;
75 self
76 }
77
78 pub fn build(self) -> Result<Client> {
80 let auth_value = format!("Bearer {}", self.api_key);
81 let auth_header = HeaderValue::from_str(&auth_value).map_err(|_| {
82 Error::Api(ApiError {
83 status_code: 0,
84 code: "invalid_api_key".to_string(),
85 message: "API key contains invalid header characters".to_string(),
86 request_id: String::new(),
87 })
88 })?;
89
90 let mut headers = HeaderMap::new();
91 headers.insert(AUTHORIZATION, auth_header.clone());
92 if let Ok(v) = HeaderValue::from_str(&self.api_key) {
94 headers.insert("X-API-Key", v);
95 }
96
97 let http = reqwest::Client::builder()
98 .default_headers(headers)
99 .timeout(self.timeout)
100 .build()?;
101
102 Ok(Client {
103 inner: Arc::new(ClientInner {
104 base_url: self.base_url,
105 http,
106 auth_header,
107 }),
108 })
109 }
110}
111
112struct ClientInner {
113 base_url: String,
114 http: reqwest::Client,
115 auth_header: HeaderValue,
116}
117
118#[derive(Clone)]
128pub struct Client {
129 inner: Arc<ClientInner>,
130}
131
132impl Client {
133 pub fn new(api_key: impl Into<String>) -> Self {
135 ClientBuilder::new(api_key)
136 .build()
137 .expect("default client configuration is valid")
138 }
139
140 pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
142 ClientBuilder::new(api_key)
143 }
144
145 pub(crate) fn base_url(&self) -> &str {
147 &self.inner.base_url
148 }
149
150 pub(crate) fn auth_header(&self) -> &HeaderValue {
152 &self.inner.auth_header
153 }
154
155 pub async fn post_json<Req: Serialize, Resp: DeserializeOwned>(
157 &self,
158 path: &str,
159 body: &Req,
160 ) -> Result<(Resp, ResponseMeta)> {
161 let url = format!("{}{}", self.inner.base_url, path);
162 let body_bytes = serde_json::to_vec(body)?;
163
164 let mut last_err = None;
165 for attempt in 0..=MAX_RETRIES {
166 if attempt > 0 {
167 let delay = INITIAL_BACKOFF_MS * 2u64.pow(attempt - 1);
168 eprintln!("[sdk] Retry {attempt}/{MAX_RETRIES} for POST {path} in {delay}ms");
169 tokio::time::sleep(Duration::from_millis(delay)).await;
170 }
171
172 let resp = self
173 .inner
174 .http
175 .post(&url)
176 .header(CONTENT_TYPE, "application/json")
177 .body(body_bytes.clone())
178 .send()
179 .await?;
180
181 let status = resp.status();
182 let meta = parse_response_meta(&resp);
183
184 if status.is_success() {
185 let body_text = resp.text().await?;
186 let result: Resp = serde_json::from_str(&body_text).map_err(|e| {
187 let preview = if body_text.len() > 300 { &body_text[..300] } else { &body_text };
188 eprintln!("[sdk] JSON decode error on {path}: {e}\n body preview: {preview}");
189 e
190 })?;
191 return Ok((result, meta));
192 }
193
194 if is_retryable(status) && attempt < MAX_RETRIES {
195 let body_text = resp.text().await.unwrap_or_default();
197 if is_permanent_error(&body_text) {
198 eprintln!("[sdk] POST {path} returned {status} but error is permanent, not retrying");
199 let err = parse_api_error_from_text(status, &body_text, &meta.request_id);
200 return Err(err);
201 }
202 eprintln!("[sdk] POST {path} returned {status}, will retry");
203 let err = parse_api_error_from_text(status, &body_text, &meta.request_id);
204 last_err = Some(err);
205 continue;
206 }
207
208 return Err(parse_api_error(resp, &meta.request_id).await);
209 }
210
211 Err(last_err.unwrap_or_else(|| Error::Api(ApiError {
212 status_code: 502,
213 code: "retry_exhausted".into(),
214 message: format!("max retries ({MAX_RETRIES}) exceeded"),
215 request_id: String::new(),
216 })))
217 }
218
219 pub async fn get_json<Resp: DeserializeOwned>(
221 &self,
222 path: &str,
223 ) -> Result<(Resp, ResponseMeta)> {
224 let url = format!("{}{}", self.inner.base_url, path);
225
226 let mut last_err = None;
227 for attempt in 0..=MAX_RETRIES {
228 if attempt > 0 {
229 let delay = INITIAL_BACKOFF_MS * 2u64.pow(attempt - 1);
230 eprintln!("[sdk] Retry {attempt}/{MAX_RETRIES} for GET {path} in {delay}ms");
231 tokio::time::sleep(Duration::from_millis(delay)).await;
232 }
233
234 let resp = self.inner.http.get(&url).send().await?;
235 let status = resp.status();
236 let meta = parse_response_meta(&resp);
237
238 if status.is_success() {
239 let body_text = resp.text().await?;
240 let result: Resp = serde_json::from_str(&body_text).map_err(|e| {
241 let preview = if body_text.len() > 300 { &body_text[..300] } else { &body_text };
242 eprintln!("[sdk] JSON decode error on {path}: {e}\n body preview: {preview}");
243 e
244 })?;
245 return Ok((result, meta));
246 }
247
248 if is_retryable(status) && attempt < MAX_RETRIES {
249 let body_text = resp.text().await.unwrap_or_default();
250 if is_permanent_error(&body_text) {
251 eprintln!("[sdk] GET {path} returned {status} but error is permanent, not retrying");
252 return Err(parse_api_error_from_text(status, &body_text, &meta.request_id));
253 }
254 eprintln!("[sdk] GET {path} returned {status}, will retry");
255 last_err = Some(parse_api_error_from_text(status, &body_text, &meta.request_id));
256 continue;
257 }
258
259 return Err(parse_api_error(resp, &meta.request_id).await);
260 }
261
262 Err(last_err.unwrap_or_else(|| Error::Api(ApiError {
263 status_code: 502,
264 code: "retry_exhausted".into(),
265 message: format!("max retries ({MAX_RETRIES}) exceeded"),
266 request_id: String::new(),
267 })))
268 }
269
270 pub async fn delete_json<Resp: DeserializeOwned>(
272 &self,
273 path: &str,
274 ) -> Result<(Resp, ResponseMeta)> {
275 let url = format!("{}{}", self.inner.base_url, path);
276 let resp = self.inner.http.delete(&url).send().await?;
277
278 let meta = parse_response_meta(&resp);
279
280 if !resp.status().is_success() {
281 return Err(parse_api_error(resp, &meta.request_id).await);
282 }
283
284 let result: Resp = resp.json().await?;
285 Ok((result, meta))
286 }
287
288 pub async fn post_multipart<Resp: DeserializeOwned>(
290 &self,
291 path: &str,
292 form: reqwest::multipart::Form,
293 ) -> Result<(Resp, ResponseMeta)> {
294 let url = format!("{}{}", self.inner.base_url, path);
295 let resp = self.inner.http.post(&url).multipart(form).send().await?;
296
297 let meta = parse_response_meta(&resp);
298
299 if !resp.status().is_success() {
300 return Err(parse_api_error(resp, &meta.request_id).await);
301 }
302
303 let result: Resp = resp.json().await?;
304 Ok((result, meta))
305 }
306
307 pub async fn get_stream_raw(
311 &self,
312 path: &str,
313 ) -> Result<(reqwest::Response, ResponseMeta)> {
314 let url = format!("{}{}", self.inner.base_url, path);
315
316 let stream_client = reqwest::Client::builder().build()?;
317
318 let resp = stream_client
319 .get(&url)
320 .header(AUTHORIZATION, self.inner.auth_header.clone())
321 .header("Accept", "text/event-stream")
322 .send()
323 .await?;
324
325 let meta = parse_response_meta(&resp);
326
327 if !resp.status().is_success() {
328 return Err(parse_api_error(resp, &meta.request_id).await);
329 }
330
331 Ok((resp, meta))
332 }
333
334 pub async fn post_stream_raw(
338 &self,
339 path: &str,
340 body: &impl Serialize,
341 ) -> Result<(reqwest::Response, ResponseMeta)> {
342 let url = format!("{}{}", self.inner.base_url, path);
343
344 let stream_client = reqwest::Client::builder().build()?;
346
347 let resp = stream_client
348 .post(&url)
349 .header(AUTHORIZATION, self.inner.auth_header.clone())
350 .header(CONTENT_TYPE, "application/json")
351 .header("Accept", "text/event-stream")
352 .json(body)
353 .send()
354 .await?;
355
356 let meta = parse_response_meta(&resp);
357
358 if !resp.status().is_success() {
359 return Err(parse_api_error(resp, &meta.request_id).await);
360 }
361
362 Ok((resp, meta))
363 }
364}
365
366fn parse_response_meta(resp: &reqwest::Response) -> ResponseMeta {
368 let headers = resp.headers();
369 let request_id = headers
370 .get("X-QAI-Request-Id")
371 .and_then(|v| v.to_str().ok())
372 .unwrap_or("")
373 .to_string();
374 let model = headers
375 .get("X-QAI-Model")
376 .and_then(|v| v.to_str().ok())
377 .unwrap_or("")
378 .to_string();
379 let cost_ticks = headers
380 .get("X-QAI-Cost-Ticks")
381 .and_then(|v| v.to_str().ok())
382 .and_then(|v| v.parse::<i64>().ok())
383 .unwrap_or(0);
384
385 ResponseMeta {
386 cost_ticks,
387 request_id,
388 model,
389 }
390}
391
392async fn parse_api_error(resp: reqwest::Response, request_id: &str) -> Error {
394 let status_code = resp.status().as_u16();
395 let status_text = resp
396 .status()
397 .canonical_reason()
398 .unwrap_or("Unknown")
399 .to_string();
400
401 let body = resp.text().await.unwrap_or_default();
402
403 let (code, message) = if let Ok(err_body) = serde_json::from_str::<ApiErrorBody>(&body) {
404 let msg = if err_body.error.message.is_empty() {
405 body.clone()
406 } else {
407 err_body.error.message
408 };
409 let c = if !err_body.error.code.is_empty() {
410 err_body.error.code
411 } else if !err_body.error.error_type.is_empty() {
412 err_body.error.error_type
413 } else {
414 status_text
415 };
416 (c, msg)
417 } else {
418 (status_text, body)
419 };
420
421 Error::Api(ApiError {
422 status_code,
423 code,
424 message,
425 request_id: request_id.to_string(),
426 })
427}
428
429fn parse_api_error_from_text(status: reqwest::StatusCode, body: &str, request_id: &str) -> Error {
430 let status_code = status.as_u16();
431 let status_text = status.canonical_reason().unwrap_or("Unknown").to_string();
432
433 let (code, message) = if let Ok(err_body) = serde_json::from_str::<ApiErrorBody>(body) {
434 let msg = if err_body.error.message.is_empty() { body.to_string() } else { err_body.error.message };
435 let c = if !err_body.error.code.is_empty() { err_body.error.code }
436 else if !err_body.error.error_type.is_empty() { err_body.error.error_type }
437 else { status_text };
438 (c, msg)
439 } else {
440 (status_text, body.to_string())
441 };
442
443 Error::Api(ApiError { status_code, code, message, request_id: request_id.to_string() })
444}