1use std::future::Future;
4use std::pin::Pin;
5
6use bytes::Bytes;
7use futures::{StreamExt, stream::BoxStream};
8use serde::de::DeserializeOwned;
9
10use aimux_core::{AiMuxError, ApiCallError};
11
12use crate::extract_response_headers::extract_response_headers;
13use crate::read_response_with_size_limit::{
14 DEFAULT_MAX_DOWNLOAD_SIZE, DEFAULT_MAX_JSON_RESPONSE_SIZE, read_response_with_size_limit,
15};
16
17pub struct ResponseHandlerInput {
19 pub url: String,
20 pub request_body_values: serde_json::Value,
21 pub response: reqwest::Response,
22 pub abort_signal: Option<aimux_core::AbortSignal>,
23 pub max_json_response_bytes: Option<usize>,
28}
29
30#[derive(Debug)]
32pub struct ResponseHandlerOutput<T> {
33 pub value: T,
34 pub raw_value: Option<serde_json::Value>,
35 pub response_headers: std::collections::HashMap<String, String>,
36}
37
38type HandlerFuture<T> =
39 Pin<Box<dyn Future<Output = Result<ResponseHandlerOutput<T>, AiMuxError>> + Send>>;
40type HandlerFn<T> = dyn FnOnce(ResponseHandlerInput) -> HandlerFuture<T> + Send;
41
42pub struct ProviderErrorParts {
44 pub message: String,
45 pub provider_code: Option<String>,
46}
47
48pub struct ResponseHandler<T> {
51 handler: Box<HandlerFn<T>>,
52 streaming: bool,
56}
57
58impl<T> ResponseHandler<T> {
59 pub fn new<F, Fut>(handler: F) -> Self
61 where
62 F: FnOnce(ResponseHandlerInput) -> Fut + Send + 'static,
63 Fut: Future<Output = Result<ResponseHandlerOutput<T>, AiMuxError>> + Send + 'static,
64 {
65 Self {
66 handler: Box::new(move |input| Box::pin(handler(input))),
67 streaming: false,
68 }
69 }
70
71 #[must_use]
75 pub fn streaming(mut self) -> Self {
76 self.streaming = true;
77 self
78 }
79
80 pub(crate) fn is_streaming(&self) -> bool {
81 self.streaming
82 }
83
84 pub async fn handle(
90 self,
91 input: ResponseHandlerInput,
92 ) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
93 (self.handler)(input).await
94 }
95}
96
97#[must_use]
104pub fn create_json_response_handler<T>() -> ResponseHandler<T>
105where
106 T: DeserializeOwned + Send + 'static,
107{
108 ResponseHandler::new(|input| async move {
109 let status = input.response.status().as_u16();
110 let headers = extract_response_headers(input.response.headers());
111 let max_bytes = input
112 .max_json_response_bytes
113 .unwrap_or(DEFAULT_MAX_JSON_RESPONSE_SIZE);
114 let body = read_response_with_size_limit(
115 input.response,
116 &input.url,
117 &input.request_body_values,
118 max_bytes,
119 input.abort_signal.as_ref(),
120 )
121 .await?;
122 let value = serde_json::from_slice::<T>(&body).map_err(|error| {
130 AiMuxError::ApiCall(Box::new(ApiCallError {
131 status_code: Some(status),
132 response_body: Some(String::from_utf8_lossy(&body).into_owned()),
133 response_headers: Some(headers.clone()),
134 ..ApiCallError::new(
135 format!("Invalid JSON response: {error}"),
136 input.url.clone(),
137 input.request_body_values.clone(),
138 )
139 }))
140 })?;
141 let raw_value = serde_json::from_slice::<serde_json::Value>(&body).ok();
142 Ok(ResponseHandlerOutput {
143 value,
144 raw_value,
145 response_headers: headers,
146 })
147 })
148}
149
150const ERROR_BODY_PUBLIC_CAP: usize = 64 * 1024;
154
155const ERROR_BODY_PARSE_CAP: usize = 1024 * 1024;
160
161fn error_body_string(body: &[u8], read_truncated: bool) -> String {
166 let mut value = String::from_utf8_lossy(body).into_owned();
167 let mut truncated = read_truncated;
168 if value.len() > ERROR_BODY_PUBLIC_CAP {
169 let mut end = ERROR_BODY_PUBLIC_CAP;
170 while !value.is_char_boundary(end) {
171 end -= 1;
172 }
173 value.truncate(end);
174 truncated = true;
175 }
176 if truncated {
177 while value.ends_with('\u{FFFD}') {
180 value.pop();
181 }
182 value.push_str("…(truncated)");
183 }
184 value
185}
186
187#[must_use]
191pub fn create_standard_json_error_response_handler() -> ResponseHandler<AiMuxError> {
192 create_json_error_response_handler(|data| {
193 let error = data.get("error").unwrap_or(data);
194 ProviderErrorParts {
195 message: error
196 .get("message")
197 .and_then(serde_json::Value::as_str)
198 .unwrap_or_default()
199 .to_owned(),
200 provider_code: error
201 .get("type")
202 .or_else(|| error.get("code"))
203 .and_then(serde_json::Value::as_str)
204 .map(str::to_owned),
205 }
206 })
207}
208
209#[must_use]
211pub fn create_json_error_response_handler<F>(
212 error_to_message_and_code: F,
213) -> ResponseHandler<AiMuxError>
214where
215 F: Fn(&serde_json::Value) -> ProviderErrorParts + Send + 'static,
216{
217 ResponseHandler::new(move |input| async move {
218 let status = input.response.status().as_u16();
219 let status_text = input
220 .response
221 .status()
222 .canonical_reason()
223 .unwrap_or_default()
224 .to_string();
225 let headers = extract_response_headers(input.response.headers());
226 let (body, truncated) = crate::read_response_with_size_limit::read_error_body_truncated(
229 input.response,
230 ERROR_BODY_PARSE_CAP,
231 input.abort_signal.as_ref(),
232 )
233 .await?;
234 let response_body = error_body_string(&body, truncated);
235 let parsed_data = serde_json::from_slice::<serde_json::Value>(&body).ok();
236 let parts = parsed_data
237 .as_ref()
238 .map(error_to_message_and_code)
239 .unwrap_or_else(|| ProviderErrorParts {
240 message: status_text.clone(),
241 provider_code: None,
242 });
243 let message = if parts.message.trim().is_empty() {
244 status_text
245 } else {
246 parts.message
247 };
248 let error = AiMuxError::ApiCall(Box::new(ApiCallError {
249 status_code: Some(status),
250 provider_code: parts.provider_code,
251 response_body: Some(response_body),
252 response_headers: Some(headers.clone()),
253 data: parsed_data.map(crate::logging::redact_error_context),
254 is_retryable: aimux_core::error::is_retryable_status(status),
255 ..ApiCallError::new(message, input.url, input.request_body_values)
256 }));
257 Ok(ResponseHandlerOutput {
258 value: error,
259 raw_value: None,
260 response_headers: headers,
261 })
262 })
263}
264
265#[must_use]
280pub fn stream_error_api_call(
281 message: impl Into<String>,
282 provider_code: Option<String>,
283 status_code: Option<u16>,
284 error_payload: &serde_json::Value,
285 url: impl Into<String>,
286 request_body_values: serde_json::Value,
287 mut response_headers: std::collections::HashMap<String, String>,
288) -> AiMuxError {
289 if !response_headers.contains_key("retry-after-ms")
292 && !response_headers.contains_key("retry-after")
293 {
294 if let Some(ms) = error_payload
295 .get("retry_after_ms")
296 .and_then(serde_json::Value::as_f64)
297 {
298 response_headers.insert("retry-after-ms".into(), ms.to_string());
299 } else if let Some(seconds) = error_payload
300 .get("retry_after")
301 .and_then(serde_json::Value::as_f64)
302 {
303 response_headers.insert("retry-after".into(), seconds.to_string());
304 }
305 }
306 AiMuxError::ApiCall(Box::new(ApiCallError {
307 status_code,
308 provider_code,
309 response_body: Some(error_payload.to_string()),
310 response_headers: Some(response_headers),
311 data: Some(crate::logging::redact_error_context(error_payload.clone())),
312 is_retryable: status_code.is_some_and(aimux_core::error::is_retryable_status),
313 ..ApiCallError::new(
314 message,
315 crate::http::sanitized_request_url(&url.into()),
316 crate::logging::redact_error_context(request_body_values),
317 )
318 }))
319}
320
321#[must_use]
323pub fn create_binary_response_handler() -> ResponseHandler<Bytes> {
324 ResponseHandler::new(|input| async move {
325 let headers = extract_response_headers(input.response.headers());
326 let value = read_response_with_size_limit(
327 input.response,
328 &input.url,
329 &input.request_body_values,
330 DEFAULT_MAX_DOWNLOAD_SIZE,
331 input.abort_signal.as_ref(),
332 )
333 .await?;
334 Ok(ResponseHandlerOutput {
335 value,
336 raw_value: None,
337 response_headers: headers,
338 })
339 })
340}
341
342#[must_use]
344pub fn create_event_source_response_handler<T>()
345-> ResponseHandler<BoxStream<'static, Result<T, AiMuxError>>>
346where
347 T: DeserializeOwned + Send + 'static,
348{
349 ResponseHandler::new(|input| async move {
350 let headers = extract_response_headers(input.response.headers());
351 let output_headers = headers.clone();
352 let url = input.url;
353 let request_body_values = input.request_body_values;
354 let signal = input.abort_signal;
355 let body_url = url.clone();
356 let body_request_body_values = request_body_values.clone();
357 let body = input.response.bytes_stream().map(move |result| {
358 result.map_err(|error| {
359 AiMuxError::ApiCall(Box::new(ApiCallError {
360 response_headers: Some(headers.clone()),
361 is_retryable: true,
362 ..ApiCallError::new(
363 error.to_string(),
364 body_url.clone(),
365 body_request_body_values.clone(),
366 )
367 }))
368 })
369 });
370
371 let stream_headers = output_headers.clone();
372 let stream_url = url.clone();
373 let stream_request_body_values = request_body_values.clone();
374 let value: BoxStream<'static, Result<T, AiMuxError>> = Box::pin(async_stream::stream! {
375 let events = aimux_stream::SseStream::new(body);
376 futures::pin_mut!(events);
377 loop {
378 let item = match signal.as_ref() {
379 Some(signal) => tokio::select! {
380 biased;
381 () = signal.cancelled() => {
382 yield Err(AiMuxError::from_abort_signal(signal));
383 break;
384 }
385 item = events.next() => item,
386 },
387 None => events.next().await,
388 };
389 let Some(item) = item else {
390 break;
391 };
392 match item {
393 Ok(event) if event.data == "[DONE]" => continue,
394 Ok(event) => yield serde_json::from_str::<T>(&event.data).map_err(AiMuxError::from),
395 Err(aimux_stream::SseError::Stream(message)) => {
396 yield Err(AiMuxError::ApiCall(Box::new(ApiCallError {
399 response_headers: Some(stream_headers.clone()),
400 is_retryable: true,
401 ..ApiCallError::new(
402 message,
403 stream_url.clone(),
404 stream_request_body_values.clone(),
405 )
406 })));
407 break;
408 }
409 Err(error) => yield Err(AiMuxError::JsonParse(error.to_string())),
410 }
411 }
412 });
413 Ok(ResponseHandlerOutput {
414 value,
415 raw_value: None,
416 response_headers: output_headers,
417 })
418 })
419 .streaming()
420}
421
422#[must_use]
424pub fn create_status_code_error_response_handler() -> ResponseHandler<AiMuxError> {
425 ResponseHandler::new(|input| async move {
426 let status = input.response.status().as_u16();
427 let message = input
428 .response
429 .status()
430 .canonical_reason()
431 .unwrap_or_default()
432 .to_string();
433 let headers = extract_response_headers(input.response.headers());
434 let (body, truncated) = crate::read_response_with_size_limit::read_error_body_truncated(
438 input.response,
439 ERROR_BODY_PUBLIC_CAP,
440 input.abort_signal.as_ref(),
441 )
442 .await?;
443 let error = AiMuxError::ApiCall(Box::new(ApiCallError {
444 status_code: Some(status),
445 response_body: Some(error_body_string(&body, truncated)),
446 response_headers: Some(headers.clone()),
447 is_retryable: aimux_core::error::is_retryable_status(status),
448 ..ApiCallError::new(message, input.url, input.request_body_values)
449 }));
450 Ok(ResponseHandlerOutput {
451 value: error,
452 raw_value: None,
453 response_headers: headers,
454 })
455 })
456}