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) -> Result<Self, Error> {
35 let http = reqwest::Client::builder()
36 .build()
37 .map_err(|e| Error::sdk(format!("failed to build HTTP client: {e}")))?;
38 Ok(Self {
39 config: config.clone(),
40 http,
41 })
42 }
43
44 pub fn from_env() -> Result<Self, Error> {
47 let config = Config::builder().build()?;
48 Self::new(&config)
49 }
50
51 pub fn converse(&self) -> ConverseBuilder {
53 ConverseBuilder::new(self.clone())
54 }
55
56 pub fn converse_stream(&self) -> ConverseStreamBuilder {
58 ConverseStreamBuilder::new(self.clone())
59 }
60
61 pub(crate) async fn send_converse(
64 &self,
65 input: &ConverseInput,
66 ) -> Result<ConverseOutput, Error> {
67 let body = build_request_body(input, false);
68 let start = Instant::now();
71 let text = self.post_converse(&body).await?;
72 let raw: Value =
73 serde_json::from_str(&text).map_err(|e| Error::sdk(format!("JSON decode error: {e}")))?;
74 let latency = start.elapsed().as_millis() as u64;
75 parse_response(&raw, latency)
76 }
77
78 pub(crate) async fn send_converse_stream(
79 &self,
80 input: &ConverseInput,
81 ) -> Result<ConverseStreamOutputHandle, Error> {
82 let body = build_request_body(input, true);
83 let start = Instant::now();
84 let resp = self.post_converse_stream(&body).await?;
85
86 Ok(spawn_stream_driver(resp, start))
88 }
89
90 async fn post_converse(&self, body: &Value) -> Result<String, Error> {
92 match self.request_with_retries(body, RequestMode::Converse).await? {
93 Attempted::Body(text) => Ok(text),
94 Attempted::Stream(_) => unreachable!("Converse mode always yields Body"),
95 }
96 }
97
98 async fn post_converse_stream(&self, body: &Value) -> Result<reqwest::Response, Error> {
100 match self.request_with_retries(body, RequestMode::Stream).await? {
101 Attempted::Stream(resp) => Ok(resp),
102 Attempted::Body(_) => unreachable!("Stream mode always yields Stream"),
103 }
104 }
105
106 async fn request_with_retries(&self, body: &Value, mode: RequestMode) -> Result<Attempted, Error> {
115 let url = format!("{}/v1/chat/completions", self.config.base_url);
116 let max_attempts = self.config.max_retries + 1;
117 let mut attempt = 0u32;
118
119 loop {
120 let accept = match mode {
121 RequestMode::Converse => "application/json",
122 RequestMode::Stream => "text/event-stream",
123 };
124 let req = self
125 .http
126 .post(&url)
127 .header("Authorization", format!("Bearer {}", self.config.api_key))
128 .header("Content-Type", "application/json")
129 .header("Accept", accept)
130 .header("User-Agent", USER_AGENT)
131 .json(body);
132
133 let outcome: Result<reqwest::Response, Error> = match mode {
134 RequestMode::Converse => req
135 .timeout(self.config.timeout)
136 .send()
137 .await
138 .map_err(|e| Error::sdk_network(format!("network error: {e}"))),
139 RequestMode::Stream => {
140 match tokio::time::timeout(self.config.timeout, req.send()).await {
141 Ok(Ok(resp)) => Ok(resp),
142 Ok(Err(e)) => Err(Error::sdk_network(format!("network error: {e}"))),
143 Err(_) => Err(Error::sdk_network(
144 "timed out waiting for response headers",
145 )),
146 }
147 }
148 };
149
150 let err = match outcome {
151 Ok(resp) if resp.status().as_u16() == 200 => match mode {
152 RequestMode::Stream => return Ok(Attempted::Stream(resp)),
153 RequestMode::Converse => match resp.text().await {
154 Ok(text) => return Ok(Attempted::Body(text)),
155 Err(e) => {
156 Error::sdk_network(format!("network error reading response body: {e}"))
157 }
158 },
159 },
160 Ok(resp) => {
161 let status = resp.status().as_u16();
162 let retry_after = resp
163 .headers()
164 .get("retry-after")
165 .and_then(|v| v.to_str().ok())
166 .and_then(parse_retry_after);
167 let body_text = resp.text().await.unwrap_or_default();
168 parse_error(status, &body_text, retry_after)
169 }
170 Err(e) => e,
171 };
172
173 attempt += 1;
174 if attempt >= max_attempts || !err.is_retryable() {
175 return Err(err);
176 }
177 backoff(attempt, err.retry_after_seconds()).await;
178 }
179 }
180}
181
182#[derive(Clone, Copy)]
184enum RequestMode {
185 Converse,
186 Stream,
187}
188
189enum Attempted {
191 Body(String),
193 Stream(reqwest::Response),
195}
196
197async fn backoff(attempt: u32, retry_after: Option<f64>) {
200 let cap = 20.0_f64;
201 let base = 0.5_f64 * 2f64.powi(attempt as i32);
202 let ceiling = base.min(cap);
203 let jittered: f64 = rand::thread_rng().gen_range(0.0..=ceiling);
204 let sleep_secs = if let Some(ra) = retry_after {
205 jittered.max(ra)
206 } else {
207 jittered
208 };
209 tokio::time::sleep(Duration::from_secs_f64(sleep_secs)).await;
210}
211
212#[derive(Default)]
216pub struct ConverseInput {
217 pub model_id: String,
218 pub messages: Vec<Message>,
219 pub system: Vec<SystemContentBlock>,
220 pub inference_config: Option<InferenceConfiguration>,
221 pub tool_config: Option<ToolConfiguration>,
222 pub additional_model_request_fields: Option<Value>,
223}
224
225impl ConverseInput {
226 fn validate(&self) -> Result<(), Error> {
228 if self.model_id.is_empty() {
229 return Err(Error::client_validation("model_id is required"));
230 }
231 if self.messages.is_empty() {
232 return Err(Error::client_validation("messages must not be empty"));
233 }
234 if let Some(v) = &self.additional_model_request_fields {
235 if !v.is_object() {
236 return Err(Error::client_validation(
237 "additional_model_request_fields must be a JSON object",
238 ));
239 }
240 }
241 Ok(())
242 }
243}
244
245macro_rules! impl_converse_input_builder {
251 ($builder:ident) => {
252 impl $builder {
253 fn new(client: Client) -> Self {
254 Self {
255 client,
256 input: ConverseInput::default(),
257 }
258 }
259
260 pub fn model_id(mut self, id: impl Into<String>) -> Self {
261 self.input.model_id = id.into();
262 self
263 }
264
265 pub fn messages(mut self, msg: Message) -> Self {
266 self.input.messages.push(msg);
267 self
268 }
269
270 pub fn set_messages(mut self, msgs: Vec<Message>) -> Self {
271 self.input.messages = msgs;
272 self
273 }
274
275 pub fn system(mut self, block: SystemContentBlock) -> Self {
276 self.input.system.push(block);
277 self
278 }
279
280 pub fn set_system(mut self, blocks: Vec<SystemContentBlock>) -> Self {
281 self.input.system = blocks;
282 self
283 }
284
285 pub fn inference_config(mut self, cfg: InferenceConfiguration) -> Self {
286 self.input.inference_config = Some(cfg);
287 self
288 }
289
290 pub fn tool_config(mut self, tc: ToolConfiguration) -> Self {
291 self.input.tool_config = Some(tc);
292 self
293 }
294
295 pub fn additional_model_request_fields(mut self, v: Value) -> Self {
296 self.input.additional_model_request_fields = Some(v);
297 self
298 }
299 }
300 };
301}
302
303pub struct ConverseBuilder {
305 client: Client,
306 input: ConverseInput,
307}
308
309impl_converse_input_builder!(ConverseBuilder);
310
311impl ConverseBuilder {
312 pub async fn send(self) -> Result<ConverseOutput, Error> {
313 self.input.validate()?;
314 self.client.send_converse(&self.input).await
315 }
316}
317
318pub struct ConverseStreamBuilder {
320 client: Client,
321 input: ConverseInput,
322}
323
324impl_converse_input_builder!(ConverseStreamBuilder);
325
326impl ConverseStreamBuilder {
327 pub async fn send(self) -> Result<ConverseStreamOutputHandle, Error> {
328 self.input.validate()?;
329 self.client.send_converse_stream(&self.input).await
330 }
331}