use std::future::Future;
use std::pin::Pin;
use bytes::Bytes;
use futures::{StreamExt, stream::BoxStream};
use serde::de::DeserializeOwned;
use aimux_core::{AiMuxError, ApiCallError};
use crate::extract_response_headers::extract_response_headers;
use crate::read_response_with_size_limit::{
DEFAULT_MAX_DOWNLOAD_SIZE, DEFAULT_MAX_JSON_RESPONSE_SIZE, read_response_with_size_limit,
};
pub struct ResponseHandlerInput {
pub url: String,
pub request_body_values: serde_json::Value,
pub response: reqwest::Response,
pub abort_signal: Option<aimux_core::AbortSignal>,
pub max_json_response_bytes: Option<usize>,
}
#[derive(Debug)]
pub struct ResponseHandlerOutput<T> {
pub value: T,
pub raw_value: Option<serde_json::Value>,
pub response_headers: std::collections::HashMap<String, String>,
}
type HandlerFuture<T> =
Pin<Box<dyn Future<Output = Result<ResponseHandlerOutput<T>, AiMuxError>> + Send>>;
type HandlerFn<T> = dyn FnOnce(ResponseHandlerInput) -> HandlerFuture<T> + Send;
pub struct ProviderErrorParts {
pub message: String,
pub provider_code: Option<String>,
}
pub struct ResponseHandler<T> {
handler: Box<HandlerFn<T>>,
streaming: bool,
}
impl<T> ResponseHandler<T> {
pub fn new<F, Fut>(handler: F) -> Self
where
F: FnOnce(ResponseHandlerInput) -> Fut + Send + 'static,
Fut: Future<Output = Result<ResponseHandlerOutput<T>, AiMuxError>> + Send + 'static,
{
Self {
handler: Box::new(move |input| Box::pin(handler(input))),
streaming: false,
}
}
#[must_use]
pub fn streaming(mut self) -> Self {
self.streaming = true;
self
}
pub(crate) fn is_streaming(&self) -> bool {
self.streaming
}
pub async fn handle(
self,
input: ResponseHandlerInput,
) -> Result<ResponseHandlerOutput<T>, AiMuxError> {
(self.handler)(input).await
}
}
#[must_use]
pub fn create_json_response_handler<T>() -> ResponseHandler<T>
where
T: DeserializeOwned + Send + 'static,
{
ResponseHandler::new(|input| async move {
let status = input.response.status().as_u16();
let headers = extract_response_headers(input.response.headers());
let max_bytes = input
.max_json_response_bytes
.unwrap_or(DEFAULT_MAX_JSON_RESPONSE_SIZE);
let body = read_response_with_size_limit(
input.response,
&input.url,
&input.request_body_values,
max_bytes,
input.abort_signal.as_ref(),
)
.await?;
let value = serde_json::from_slice::<T>(&body).map_err(|error| {
AiMuxError::ApiCall(Box::new(ApiCallError {
status_code: Some(status),
response_body: Some(String::from_utf8_lossy(&body).into_owned()),
response_headers: Some(headers.clone()),
..ApiCallError::new(
format!("Invalid JSON response: {error}"),
input.url.clone(),
input.request_body_values.clone(),
)
}))
})?;
let raw_value = serde_json::from_slice::<serde_json::Value>(&body).ok();
Ok(ResponseHandlerOutput {
value,
raw_value,
response_headers: headers,
})
})
}
const ERROR_BODY_PUBLIC_CAP: usize = 64 * 1024;
const ERROR_BODY_PARSE_CAP: usize = 1024 * 1024;
fn error_body_string(body: &[u8], read_truncated: bool) -> String {
let mut value = String::from_utf8_lossy(body).into_owned();
let mut truncated = read_truncated;
if value.len() > ERROR_BODY_PUBLIC_CAP {
let mut end = ERROR_BODY_PUBLIC_CAP;
while !value.is_char_boundary(end) {
end -= 1;
}
value.truncate(end);
truncated = true;
}
if truncated {
while value.ends_with('\u{FFFD}') {
value.pop();
}
value.push_str("…(truncated)");
}
value
}
#[must_use]
pub fn create_standard_json_error_response_handler() -> ResponseHandler<AiMuxError> {
create_json_error_response_handler(|data| {
let error = data.get("error").unwrap_or(data);
ProviderErrorParts {
message: error
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_owned(),
provider_code: error
.get("type")
.or_else(|| error.get("code"))
.and_then(serde_json::Value::as_str)
.map(str::to_owned),
}
})
}
#[must_use]
pub fn create_json_error_response_handler<F>(
error_to_message_and_code: F,
) -> ResponseHandler<AiMuxError>
where
F: Fn(&serde_json::Value) -> ProviderErrorParts + Send + 'static,
{
ResponseHandler::new(move |input| async move {
let status = input.response.status().as_u16();
let status_text = input
.response
.status()
.canonical_reason()
.unwrap_or_default()
.to_string();
let headers = extract_response_headers(input.response.headers());
let (body, truncated) = crate::read_response_with_size_limit::read_error_body_truncated(
input.response,
ERROR_BODY_PARSE_CAP,
input.abort_signal.as_ref(),
)
.await?;
let response_body = error_body_string(&body, truncated);
let parsed_data = serde_json::from_slice::<serde_json::Value>(&body).ok();
let parts = parsed_data
.as_ref()
.map(error_to_message_and_code)
.unwrap_or_else(|| ProviderErrorParts {
message: status_text.clone(),
provider_code: None,
});
let message = if parts.message.trim().is_empty() {
status_text
} else {
parts.message
};
let error = AiMuxError::ApiCall(Box::new(ApiCallError {
status_code: Some(status),
provider_code: parts.provider_code,
response_body: Some(response_body),
response_headers: Some(headers.clone()),
data: parsed_data.map(crate::logging::redact_error_context),
is_retryable: aimux_core::error::is_retryable_status(status),
..ApiCallError::new(message, input.url, input.request_body_values)
}));
Ok(ResponseHandlerOutput {
value: error,
raw_value: None,
response_headers: headers,
})
})
}
#[must_use]
pub fn stream_error_api_call(
message: impl Into<String>,
provider_code: Option<String>,
status_code: Option<u16>,
error_payload: &serde_json::Value,
url: impl Into<String>,
request_body_values: serde_json::Value,
mut response_headers: std::collections::HashMap<String, String>,
) -> AiMuxError {
if !response_headers.contains_key("retry-after-ms")
&& !response_headers.contains_key("retry-after")
{
if let Some(ms) = error_payload
.get("retry_after_ms")
.and_then(serde_json::Value::as_f64)
{
response_headers.insert("retry-after-ms".into(), ms.to_string());
} else if let Some(seconds) = error_payload
.get("retry_after")
.and_then(serde_json::Value::as_f64)
{
response_headers.insert("retry-after".into(), seconds.to_string());
}
}
AiMuxError::ApiCall(Box::new(ApiCallError {
status_code,
provider_code,
response_body: Some(error_payload.to_string()),
response_headers: Some(response_headers),
data: Some(crate::logging::redact_error_context(error_payload.clone())),
is_retryable: status_code.is_some_and(aimux_core::error::is_retryable_status),
..ApiCallError::new(
message,
crate::http::sanitized_request_url(&url.into()),
crate::logging::redact_error_context(request_body_values),
)
}))
}
#[must_use]
pub fn create_binary_response_handler() -> ResponseHandler<Bytes> {
ResponseHandler::new(|input| async move {
let headers = extract_response_headers(input.response.headers());
let value = read_response_with_size_limit(
input.response,
&input.url,
&input.request_body_values,
DEFAULT_MAX_DOWNLOAD_SIZE,
input.abort_signal.as_ref(),
)
.await?;
Ok(ResponseHandlerOutput {
value,
raw_value: None,
response_headers: headers,
})
})
}
#[must_use]
pub fn create_event_source_response_handler<T>()
-> ResponseHandler<BoxStream<'static, Result<T, AiMuxError>>>
where
T: DeserializeOwned + Send + 'static,
{
ResponseHandler::new(|input| async move {
let headers = extract_response_headers(input.response.headers());
let output_headers = headers.clone();
let url = input.url;
let request_body_values = input.request_body_values;
let signal = input.abort_signal;
let body_url = url.clone();
let body_request_body_values = request_body_values.clone();
let body = input.response.bytes_stream().map(move |result| {
result.map_err(|error| {
AiMuxError::ApiCall(Box::new(ApiCallError {
response_headers: Some(headers.clone()),
is_retryable: true,
..ApiCallError::new(
error.to_string(),
body_url.clone(),
body_request_body_values.clone(),
)
}))
})
});
let stream_headers = output_headers.clone();
let stream_url = url.clone();
let stream_request_body_values = request_body_values.clone();
let value: BoxStream<'static, Result<T, AiMuxError>> = Box::pin(async_stream::stream! {
let events = aimux_stream::SseStream::new(body);
futures::pin_mut!(events);
loop {
let item = match signal.as_ref() {
Some(signal) => tokio::select! {
biased;
() = signal.cancelled() => {
yield Err(AiMuxError::from_abort_signal(signal));
break;
}
item = events.next() => item,
},
None => events.next().await,
};
let Some(item) = item else {
break;
};
match item {
Ok(event) if event.data == "[DONE]" => continue,
Ok(event) => yield serde_json::from_str::<T>(&event.data).map_err(AiMuxError::from),
Err(aimux_stream::SseError::Stream(message)) => {
yield Err(AiMuxError::ApiCall(Box::new(ApiCallError {
response_headers: Some(stream_headers.clone()),
is_retryable: true,
..ApiCallError::new(
message,
stream_url.clone(),
stream_request_body_values.clone(),
)
})));
break;
}
Err(error) => yield Err(AiMuxError::JsonParse(error.to_string())),
}
}
});
Ok(ResponseHandlerOutput {
value,
raw_value: None,
response_headers: output_headers,
})
})
.streaming()
}
#[must_use]
pub fn create_status_code_error_response_handler() -> ResponseHandler<AiMuxError> {
ResponseHandler::new(|input| async move {
let status = input.response.status().as_u16();
let message = input
.response
.status()
.canonical_reason()
.unwrap_or_default()
.to_string();
let headers = extract_response_headers(input.response.headers());
let (body, truncated) = crate::read_response_with_size_limit::read_error_body_truncated(
input.response,
ERROR_BODY_PUBLIC_CAP,
input.abort_signal.as_ref(),
)
.await?;
let error = AiMuxError::ApiCall(Box::new(ApiCallError {
status_code: Some(status),
response_body: Some(error_body_string(&body, truncated)),
response_headers: Some(headers.clone()),
is_retryable: aimux_core::error::is_retryable_status(status),
..ApiCallError::new(message, input.url, input.request_body_values)
}));
Ok(ResponseHandlerOutput {
value: error,
raw_value: None,
response_headers: headers,
})
})
}