1use std::time::{Duration, Instant};
4
5use rand::Rng;
6use serde_json::Value;
7
8use crate::config::Config;
9use crate::error::{parse_error, parse_retry_after, status_error};
10use crate::mapping::{build_request_body, parse_response};
11use crate::stream::{spawn_stream_driver, ConverseStreamOutputHandle};
12use crate::types::{
13 ConverseOutput, GenerateImageOutput, InferenceConfiguration, MediaResolution, Message,
14 SystemContentBlock, ToolConfiguration, UploadFileOutput,
15};
16use crate::Error;
17
18const USER_AGENT: &str = concat!("audacity-sdk-rust/", env!("CARGO_PKG_VERSION"));
19
20const DEFAULT_UPLOAD_CHUNK_SIZE: usize = 8 * 1024 * 1024;
23
24const MAX_UPLOAD_RECOVERY_ATTEMPTS: u32 = 5;
26
27#[derive(Clone)]
29pub struct Client {
30 config: Config,
31 http: reqwest::Client,
32}
33
34impl Client {
35 pub fn new(config: &Config) -> Result<Self, Error> {
43 let http = reqwest::Client::builder()
44 .build()
45 .map_err(|e| Error::sdk(format!("failed to build HTTP client: {e}")))?;
46 Ok(Self {
47 config: config.clone(),
48 http,
49 })
50 }
51
52 pub fn from_env() -> Result<Self, Error> {
55 let config = Config::builder().build()?;
56 Self::new(&config)
57 }
58
59 pub fn converse(&self) -> ConverseBuilder {
61 ConverseBuilder::new(self.clone())
62 }
63
64 pub fn converse_stream(&self) -> ConverseStreamBuilder {
66 ConverseStreamBuilder::new(self.clone())
67 }
68
69 pub fn upload_file(&self) -> UploadFileBuilder {
73 UploadFileBuilder::new(self.clone())
74 }
75
76 pub fn generate_image(&self) -> GenerateImageBuilder {
79 GenerateImageBuilder::new(self.clone())
80 }
81
82 pub(crate) async fn send_converse(
85 &self,
86 input: &ConverseInput,
87 ) -> Result<ConverseOutput, Error> {
88 let body = build_request_body(input, false);
89 let start = Instant::now();
92 let text = self.post_converse(&body).await?;
93 let raw: Value =
94 serde_json::from_str(&text).map_err(|e| Error::sdk(format!("JSON decode error: {e}")))?;
95 let latency = start.elapsed().as_millis() as u64;
96 parse_response(&raw, latency)
97 }
98
99 pub(crate) async fn send_converse_stream(
100 &self,
101 input: &ConverseInput,
102 ) -> Result<ConverseStreamOutputHandle, Error> {
103 let body = build_request_body(input, true);
104 let start = Instant::now();
105 let resp = self.post_converse_stream(&body).await?;
106
107 Ok(spawn_stream_driver(resp, start))
109 }
110
111 pub(crate) async fn send_upload_file(
112 &self,
113 data: Vec<u8>,
114 content_type: String,
115 chunk_size: usize,
116 ) -> Result<UploadFileOutput, Error> {
117 let url = format!("{}/v1/files", self.config.base_url);
120 let body = serde_json::json!({
121 "content_type": content_type,
122 "size_bytes": data.len(),
123 });
124 let text = match self
125 .request_with_retries(&url, &body, RequestMode::Converse)
126 .await?
127 {
128 Attempted::Body(text) => text,
129 Attempted::Stream(_) => unreachable!("Converse mode always yields Body"),
130 };
131 let ticket: UploadFileOutput = serde_json::from_str(&text)
132 .map_err(|e| Error::sdk(format!("JSON decode error on /v1/files response: {e}")))?;
133
134 let session_uri = self
139 .initiate_resumable_session(&ticket.upload_url, &content_type)
140 .await?;
141 self.upload_chunks(&session_uri, &data, chunk_size).await?;
142
143 Ok(ticket)
144 }
145
146 async fn initiate_resumable_session(
153 &self,
154 upload_url: &str,
155 content_type: &str,
156 ) -> Result<String, Error> {
157 let mut attempt = 0u32;
158 loop {
159 let outcome = self
160 .http
161 .post(upload_url)
162 .header("x-goog-resumable", "start")
163 .header("Content-Type", content_type)
164 .header("User-Agent", USER_AGENT)
165 .timeout(self.config.timeout)
166 .send()
167 .await;
168
169 let (err, retry_after) = match outcome {
170 Ok(resp) if resp.status().is_success() => {
171 return resp
172 .headers()
173 .get("location")
174 .and_then(|v| v.to_str().ok())
175 .map(str::to_owned)
176 .ok_or_else(|| {
177 Error::sdk(
178 "resumable upload initiation response missing Location header",
179 )
180 });
181 }
182 Ok(resp) => {
183 let status = resp.status().as_u16();
184 let retry_after = header_retry_after(&resp);
185 let transient = status == 429 || status >= 500;
186 let body_text = resp.text().await.unwrap_or_default();
187 let err = status_error(status, &body_text);
188 if !transient {
189 return Err(err);
190 }
191 (err, retry_after)
192 }
193 Err(e) => (
194 Error::sdk_network(format!("network error initiating resumable upload: {e}")),
195 None,
196 ),
197 };
198
199 attempt += 1;
200 if attempt > MAX_UPLOAD_RECOVERY_ATTEMPTS {
201 return Err(err);
202 }
203 backoff(attempt, retry_after).await;
204 }
205 }
206
207 async fn upload_chunks(
216 &self,
217 session_uri: &str,
218 data: &[u8],
219 chunk_size: usize,
220 ) -> Result<(), Error> {
221 let total = data.len();
222 let mut offset = 0usize;
223 let mut failures = 0u32;
224 let mut querying = total == 0;
227
228 loop {
229 let req = if querying {
230 self.http
231 .put(session_uri)
232 .header("Content-Range", format!("bytes */{total}"))
233 } else {
234 let end = (offset + chunk_size).min(total);
235 self.http
236 .put(session_uri)
237 .header(
238 "Content-Range",
239 format!("bytes {offset}-{}/{total}", end - 1),
240 )
241 .body(data[offset..end].to_vec())
242 };
243 let outcome = req
244 .header("User-Agent", USER_AGENT)
245 .timeout(self.config.timeout)
246 .send()
247 .await;
248
249 let (err, retry_after) = match outcome {
250 Ok(resp) if resp.status().is_success() => return Ok(()),
251 Ok(resp) if resp.status().as_u16() == 308 => {
252 let confirmed = confirmed_offset(resp.headers());
257 if confirmed > offset {
258 failures = 0; }
260 offset = confirmed;
261 querying = false;
262 continue;
263 }
264 Ok(resp) => {
265 let status = resp.status().as_u16();
266 let retry_after = header_retry_after(&resp);
267 let transient = status == 429 || status >= 500;
268 let body_text = resp.text().await.unwrap_or_default();
269 let err = status_error(status, &body_text);
270 if !transient {
271 return Err(err);
272 }
273 (err, retry_after)
274 }
275 Err(e) => (
276 Error::sdk_network(format!("network error uploading file chunk: {e}")),
277 None,
278 ),
279 };
280
281 failures += 1;
282 if failures > MAX_UPLOAD_RECOVERY_ATTEMPTS {
283 return Err(err);
284 }
285 backoff(failures, retry_after).await;
286 querying = true;
287 }
288 }
289
290 pub(crate) async fn send_generate_image(
291 &self,
292 body: &Value,
293 ) -> Result<GenerateImageOutput, Error> {
294 let url = format!("{}/v1/images/generations", self.config.base_url);
295 let text = match self
296 .request_with_retries(&url, body, RequestMode::Converse)
297 .await?
298 {
299 Attempted::Body(text) => text,
300 Attempted::Stream(_) => unreachable!("Converse mode always yields Body"),
301 };
302 serde_json::from_str(&text).map_err(|e| {
306 Error::sdk(format!(
307 "JSON decode error on /v1/images/generations response: {e}"
308 ))
309 })
310 }
311
312 async fn post_converse(&self, body: &Value) -> Result<String, Error> {
314 let url = format!("{}/v1/chat/completions", self.config.base_url);
315 match self.request_with_retries(&url, body, RequestMode::Converse).await? {
316 Attempted::Body(text) => Ok(text),
317 Attempted::Stream(_) => unreachable!("Converse mode always yields Body"),
318 }
319 }
320
321 async fn post_converse_stream(&self, body: &Value) -> Result<reqwest::Response, Error> {
323 let url = format!("{}/v1/chat/completions", self.config.base_url);
324 match self.request_with_retries(&url, body, RequestMode::Stream).await? {
325 Attempted::Stream(resp) => Ok(resp),
326 Attempted::Body(_) => unreachable!("Stream mode always yields Stream"),
327 }
328 }
329
330 async fn request_with_retries(
339 &self,
340 url: &str,
341 body: &Value,
342 mode: RequestMode,
343 ) -> Result<Attempted, Error> {
344 let max_attempts = self.config.max_retries + 1;
345 let mut attempt = 0u32;
346
347 loop {
348 let accept = match mode {
349 RequestMode::Converse => "application/json",
350 RequestMode::Stream => "text/event-stream",
351 };
352 let req = self
353 .http
354 .post(url)
355 .header("Authorization", format!("Bearer {}", self.config.api_key))
356 .header("Content-Type", "application/json")
357 .header("Accept", accept)
358 .header("User-Agent", USER_AGENT)
359 .json(body);
360
361 let outcome: Result<reqwest::Response, Error> = match mode {
362 RequestMode::Converse => req
363 .timeout(self.config.timeout)
364 .send()
365 .await
366 .map_err(|e| Error::sdk_network(format!("network error: {e}"))),
367 RequestMode::Stream => {
368 match tokio::time::timeout(self.config.timeout, req.send()).await {
369 Ok(Ok(resp)) => Ok(resp),
370 Ok(Err(e)) => Err(Error::sdk_network(format!("network error: {e}"))),
371 Err(_) => Err(Error::sdk_network(
372 "timed out waiting for response headers",
373 )),
374 }
375 }
376 };
377
378 let err = match outcome {
379 Ok(resp) if resp.status().as_u16() == 200 => match mode {
380 RequestMode::Stream => return Ok(Attempted::Stream(resp)),
381 RequestMode::Converse => match resp.text().await {
382 Ok(text) => return Ok(Attempted::Body(text)),
383 Err(e) => {
384 Error::sdk_network(format!("network error reading response body: {e}"))
385 }
386 },
387 },
388 Ok(resp) => {
389 let status = resp.status().as_u16();
390 let retry_after = resp
391 .headers()
392 .get("retry-after")
393 .and_then(|v| v.to_str().ok())
394 .and_then(parse_retry_after);
395 let body_text = resp.text().await.unwrap_or_default();
396 parse_error(status, &body_text, retry_after)
397 }
398 Err(e) => e,
399 };
400
401 attempt += 1;
402 if attempt >= max_attempts || !err.is_retryable() {
403 return Err(err);
404 }
405 backoff(attempt, err.retry_after_seconds()).await;
406 }
407 }
408}
409
410#[derive(Clone, Copy)]
412enum RequestMode {
413 Converse,
414 Stream,
415}
416
417enum Attempted {
419 Body(String),
421 Stream(reqwest::Response),
423}
424
425fn confirmed_offset(headers: &reqwest::header::HeaderMap) -> usize {
428 headers
429 .get("range")
430 .and_then(|v| v.to_str().ok())
431 .and_then(|s| s.trim().strip_prefix("bytes=0-"))
432 .and_then(|n| n.parse::<usize>().ok())
433 .map(|n| n + 1)
434 .unwrap_or(0)
435}
436
437fn header_retry_after(resp: &reqwest::Response) -> Option<f64> {
439 resp.headers()
440 .get("retry-after")
441 .and_then(|v| v.to_str().ok())
442 .and_then(parse_retry_after)
443}
444
445async fn backoff(attempt: u32, retry_after: Option<f64>) {
448 let cap = 20.0_f64;
449 let base = 0.5_f64 * 2f64.powi(attempt as i32);
450 let ceiling = base.min(cap);
451 let jittered: f64 = rand::thread_rng().gen_range(0.0..=ceiling);
452 let sleep_secs = if let Some(ra) = retry_after {
453 jittered.max(ra)
454 } else {
455 jittered
456 };
457 tokio::time::sleep(Duration::from_secs_f64(sleep_secs)).await;
458}
459
460#[derive(Default)]
464pub struct ConverseInput {
465 pub model_id: String,
466 pub messages: Vec<Message>,
467 pub system: Vec<SystemContentBlock>,
468 pub inference_config: Option<InferenceConfiguration>,
469 pub media_resolution: Option<MediaResolution>,
470 pub tool_config: Option<ToolConfiguration>,
471 pub additional_model_request_fields: Option<Value>,
472}
473
474impl ConverseInput {
475 fn validate(&self) -> Result<(), Error> {
477 if self.model_id.is_empty() {
478 return Err(Error::client_validation("model_id is required"));
479 }
480 if self.messages.is_empty() {
481 return Err(Error::client_validation("messages must not be empty"));
482 }
483 if let Some(v) = &self.additional_model_request_fields {
484 if !v.is_object() {
485 return Err(Error::client_validation(
486 "additional_model_request_fields must be a JSON object",
487 ));
488 }
489 }
490 Ok(())
491 }
492}
493
494macro_rules! impl_converse_input_builder {
500 ($builder:ident) => {
501 impl $builder {
502 fn new(client: Client) -> Self {
503 Self {
504 client,
505 input: ConverseInput::default(),
506 }
507 }
508
509 pub fn model_id(mut self, id: impl Into<String>) -> Self {
510 self.input.model_id = id.into();
511 self
512 }
513
514 pub fn messages(mut self, msg: Message) -> Self {
515 self.input.messages.push(msg);
516 self
517 }
518
519 pub fn set_messages(mut self, msgs: Vec<Message>) -> Self {
520 self.input.messages = msgs;
521 self
522 }
523
524 pub fn system(mut self, block: SystemContentBlock) -> Self {
525 self.input.system.push(block);
526 self
527 }
528
529 pub fn set_system(mut self, blocks: Vec<SystemContentBlock>) -> Self {
530 self.input.system = blocks;
531 self
532 }
533
534 pub fn inference_config(mut self, cfg: InferenceConfiguration) -> Self {
535 self.input.inference_config = Some(cfg);
536 self
537 }
538
539 pub fn media_resolution(mut self, v: MediaResolution) -> Self {
540 self.input.media_resolution = Some(v);
541 self
542 }
543
544 pub fn tool_config(mut self, tc: ToolConfiguration) -> Self {
545 self.input.tool_config = Some(tc);
546 self
547 }
548
549 pub fn additional_model_request_fields(mut self, v: Value) -> Self {
550 self.input.additional_model_request_fields = Some(v);
551 self
552 }
553 }
554 };
555}
556
557pub struct ConverseBuilder {
559 client: Client,
560 input: ConverseInput,
561}
562
563impl_converse_input_builder!(ConverseBuilder);
564
565impl ConverseBuilder {
566 pub async fn send(self) -> Result<ConverseOutput, Error> {
567 self.input.validate()?;
568 self.client.send_converse(&self.input).await
569 }
570}
571
572pub struct ConverseStreamBuilder {
574 client: Client,
575 input: ConverseInput,
576}
577
578impl_converse_input_builder!(ConverseStreamBuilder);
579
580impl ConverseStreamBuilder {
581 pub async fn send(self) -> Result<ConverseStreamOutputHandle, Error> {
582 self.input.validate()?;
583 self.client.send_converse_stream(&self.input).await
584 }
585}
586
587pub struct UploadFileBuilder {
589 client: Client,
590 data: Option<Vec<u8>>,
591 content_type: Option<String>,
592 chunk_size: usize,
593}
594
595impl UploadFileBuilder {
596 fn new(client: Client) -> Self {
597 Self {
598 client,
599 data: None,
600 content_type: None,
601 chunk_size: DEFAULT_UPLOAD_CHUNK_SIZE,
602 }
603 }
604
605 pub fn data(mut self, data: impl Into<Vec<u8>>) -> Self {
607 self.data = Some(data.into());
608 self
609 }
610
611 pub fn content_type(mut self, content_type: impl Into<String>) -> Self {
614 self.content_type = Some(content_type.into());
615 self
616 }
617
618 #[doc(hidden)]
621 pub fn chunk_size(mut self, n: usize) -> Self {
622 self.chunk_size = n;
623 self
624 }
625
626 pub async fn send(self) -> Result<UploadFileOutput, Error> {
627 let data = self
628 .data
629 .ok_or_else(|| Error::client_validation("upload_file data is required"))?;
630 let content_type = self
631 .content_type
632 .ok_or_else(|| Error::client_validation("upload_file content_type is required"))?;
633 self.client
634 .send_upload_file(data, content_type, self.chunk_size)
635 .await
636 }
637}
638
639pub struct GenerateImageBuilder {
641 client: Client,
642 model: Option<String>,
643 prompt: Option<String>,
644 n: Option<u32>,
645 size: Option<String>,
646 quality: Option<String>,
647 response_format: Option<String>,
648 user: Option<String>,
649}
650
651impl GenerateImageBuilder {
652 fn new(client: Client) -> Self {
653 Self {
654 client,
655 model: None,
656 prompt: None,
657 n: None,
658 size: None,
659 quality: None,
660 response_format: None,
661 user: None,
662 }
663 }
664
665 pub fn model(mut self, model: impl Into<String>) -> Self {
668 self.model = Some(model.into());
669 self
670 }
671
672 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
674 self.prompt = Some(prompt.into());
675 self
676 }
677
678 pub fn n(mut self, n: u32) -> Self {
680 self.n = Some(n);
681 self
682 }
683
684 pub fn size(mut self, size: impl Into<String>) -> Self {
687 self.size = Some(size.into());
688 self
689 }
690
691 pub fn quality(mut self, quality: impl Into<String>) -> Self {
693 self.quality = Some(quality.into());
694 self
695 }
696
697 pub fn response_format(mut self, response_format: impl Into<String>) -> Self {
700 self.response_format = Some(response_format.into());
701 self
702 }
703
704 pub fn user(mut self, user: impl Into<String>) -> Self {
706 self.user = Some(user.into());
707 self
708 }
709
710 pub async fn send(self) -> Result<GenerateImageOutput, Error> {
711 let model = self
712 .model
713 .filter(|m| !m.is_empty())
714 .ok_or_else(|| Error::client_validation("generate_image model is required"))?;
715 let prompt = self
716 .prompt
717 .filter(|p| !p.is_empty())
718 .ok_or_else(|| Error::client_validation("generate_image prompt is required"))?;
719
720 let mut body = serde_json::Map::new();
722 body.insert("model".into(), Value::String(model));
723 body.insert("prompt".into(), Value::String(prompt));
724 if let Some(n) = self.n {
725 body.insert("n".into(), Value::from(n));
726 }
727 if let Some(size) = self.size {
728 body.insert("size".into(), Value::String(size));
729 }
730 if let Some(quality) = self.quality {
731 body.insert("quality".into(), Value::String(quality));
732 }
733 if let Some(response_format) = self.response_format {
734 body.insert("response_format".into(), Value::String(response_format));
735 }
736 if let Some(user) = self.user {
737 body.insert("user".into(), Value::String(user));
738 }
739
740 self.client.send_generate_image(&Value::Object(body)).await
741 }
742}