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};
10use crate::mapping::{build_request_body, parse_response};
11use crate::stream::{spawn_stream_driver, ConverseStreamOutputHandle};
12use crate::types::{
13 ConverseOutput, InferenceConfiguration, Message, SystemContentBlock, ToolConfiguration,
14};
15use crate::Error;
16
17const USER_AGENT: &str = concat!("audacity-sdk-rust/", env!("CARGO_PKG_VERSION"));
18
19#[derive(Clone)]
21pub struct Client {
22 config: Config,
23 http: reqwest::Client,
24}
25
26impl Client {
27 pub fn new(config: &Config) -> Self {
29 let http = reqwest::Client::builder()
30 .timeout(config.timeout)
31 .build()
32 .expect("failed to build reqwest client");
33 Self {
34 config: config.clone(),
35 http,
36 }
37 }
38
39 pub fn from_env() -> Result<Self, Error> {
42 let config = Config::builder().build()?;
43 Ok(Self::new(&config))
44 }
45
46 pub fn converse(&self) -> ConverseBuilder {
48 ConverseBuilder::new(self.clone())
49 }
50
51 pub fn converse_stream(&self) -> ConverseStreamBuilder {
53 ConverseStreamBuilder::new(self.clone())
54 }
55
56 pub(crate) async fn send_converse(
59 &self,
60 input: &ConverseInput,
61 ) -> Result<ConverseOutput, Error> {
62 let body = build_request_body(input, false);
63 let (resp, start) = self.request_with_retries(&body, "application/json").await?;
64
65 let latency = start.elapsed().as_millis() as u64;
66 let raw: Value = resp
67 .json()
68 .await
69 .map_err(|e| Error::Sdk(format!("JSON decode error: {e}")))?;
70 parse_response(&raw, latency)
71 }
72
73 pub(crate) async fn send_converse_stream(
74 &self,
75 input: &ConverseInput,
76 ) -> Result<ConverseStreamOutputHandle, Error> {
77 let body = build_request_body(input, true);
78 let (resp, start) = self
79 .request_with_retries(&body, "text/event-stream")
80 .await?;
81
82 Ok(spawn_stream_driver(resp, start))
84 }
85
86 async fn request_with_retries(
90 &self,
91 body: &Value,
92 accept: &str,
93 ) -> Result<(reqwest::Response, Instant), Error> {
94 let url = format!("{}/v1/chat/completions", self.config.base_url);
95 let max_attempts = self.config.max_retries + 1;
96 let mut attempt = 0u32;
97
98 loop {
99 let start = Instant::now();
100 let result = self
101 .http
102 .post(&url)
103 .header("Authorization", format!("Bearer {}", self.config.api_key))
104 .header("Content-Type", "application/json")
105 .header("Accept", accept)
106 .header("User-Agent", USER_AGENT)
107 .json(body)
108 .send()
109 .await;
110
111 let err = match result {
112 Ok(resp) if resp.status().as_u16() == 200 => return Ok((resp, start)),
113 Ok(resp) => {
114 let status = resp.status().as_u16();
115 let retry_after = resp
116 .headers()
117 .get("retry-after")
118 .and_then(|v| v.to_str().ok())
119 .and_then(parse_retry_after);
120 let body_text = resp.text().await.unwrap_or_default();
121 parse_error(status, &body_text, retry_after)
122 }
123 Err(e) => Error::Sdk(e.to_string()),
124 };
125
126 attempt += 1;
127 if attempt >= max_attempts || !err.is_retryable() {
128 return Err(err);
129 }
130 backoff(attempt, err.retry_after_seconds()).await;
131 }
132 }
133}
134
135async fn backoff(attempt: u32, retry_after: Option<f64>) {
138 let cap = 20.0_f64;
139 let base = 0.5_f64 * 2f64.powi(attempt as i32);
140 let ceiling = base.min(cap);
141 let jittered: f64 = rand::thread_rng().gen_range(0.0..=ceiling);
142 let sleep_secs = if let Some(ra) = retry_after {
143 jittered.max(ra)
144 } else {
145 jittered
146 };
147 tokio::time::sleep(Duration::from_secs_f64(sleep_secs)).await;
148}
149
150#[derive(Default)]
154pub struct ConverseInput {
155 pub model_id: String,
156 pub messages: Vec<Message>,
157 pub system: Vec<SystemContentBlock>,
158 pub inference_config: Option<InferenceConfiguration>,
159 pub tool_config: Option<ToolConfiguration>,
160 pub additional_model_request_fields: Option<Value>,
161}
162
163impl ConverseInput {
164 fn validate(&self) -> Result<(), Error> {
166 if self.model_id.is_empty() {
167 return Err(Error::Sdk("model_id is required".into()));
168 }
169 if self.messages.is_empty() {
170 return Err(Error::Sdk("messages must not be empty".into()));
171 }
172 Ok(())
173 }
174}
175
176macro_rules! impl_converse_input_builder {
182 ($builder:ident) => {
183 impl $builder {
184 fn new(client: Client) -> Self {
185 Self {
186 client,
187 input: ConverseInput::default(),
188 }
189 }
190
191 pub fn model_id(mut self, id: impl Into<String>) -> Self {
192 self.input.model_id = id.into();
193 self
194 }
195
196 pub fn messages(mut self, msg: Message) -> Self {
197 self.input.messages.push(msg);
198 self
199 }
200
201 pub fn set_messages(mut self, msgs: Vec<Message>) -> Self {
202 self.input.messages = msgs;
203 self
204 }
205
206 pub fn system(mut self, block: SystemContentBlock) -> Self {
207 self.input.system.push(block);
208 self
209 }
210
211 pub fn set_system(mut self, blocks: Vec<SystemContentBlock>) -> Self {
212 self.input.system = blocks;
213 self
214 }
215
216 pub fn inference_config(mut self, cfg: InferenceConfiguration) -> Self {
217 self.input.inference_config = Some(cfg);
218 self
219 }
220
221 pub fn tool_config(mut self, tc: ToolConfiguration) -> Self {
222 self.input.tool_config = Some(tc);
223 self
224 }
225
226 pub fn additional_model_request_fields(mut self, v: Value) -> Self {
227 self.input.additional_model_request_fields = Some(v);
228 self
229 }
230 }
231 };
232}
233
234pub struct ConverseBuilder {
236 client: Client,
237 input: ConverseInput,
238}
239
240impl_converse_input_builder!(ConverseBuilder);
241
242impl ConverseBuilder {
243 pub async fn send(self) -> Result<ConverseOutput, Error> {
244 self.input.validate()?;
245 self.client.send_converse(&self.input).await
246 }
247}
248
249pub struct ConverseStreamBuilder {
251 client: Client,
252 input: ConverseInput,
253}
254
255impl_converse_input_builder!(ConverseStreamBuilder);
256
257impl ConverseStreamBuilder {
258 pub async fn send(self) -> Result<ConverseStreamOutputHandle, Error> {
259 self.input.validate()?;
260 self.client.send_converse_stream(&self.input).await
261 }
262}