use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use bytes::Bytes;
use serde::de::DeserializeOwned;
use super::envelope::ProviderEnvelope;
use crate::client::{Client, Provider};
use crate::http_client::{self, HttpClientExt};
use crate::image_generation::{self, ImageGenerationError, ImageGenerationRequest};
use crate::wasm_compat::{WasmCompatSend, WasmCompatSync};
pub(crate) fn decode_base64_image<T>(
response: T,
select: fn(&T) -> Option<&str>,
missing_message: &'static str,
decode_error_prefix: Option<&'static str>,
) -> Result<image_generation::ImageGenerationResponse<T>, ImageGenerationError> {
let encoded = select(&response)
.ok_or_else(|| ImageGenerationError::ResponseError(missing_message.to_owned()))?;
let image = BASE64_STANDARD.decode(encoded).map_err(|error| {
ImageGenerationError::ResponseError(match decode_error_prefix {
Some(prefix) => format!("{prefix}{error}"),
None => error.to_string(),
})
})?;
Ok(image_generation::ImageGenerationResponse { image, response })
}
#[doc(hidden)]
pub trait JsonImageGenerationProvider: Provider {
const IMAGE_GENERATION_PATH: &'static str;
type Response: DeserializeOwned
+ WasmCompatSend
+ WasmCompatSync
+ TryInto<
image_generation::ImageGenerationResponse<Self::Response>,
Error = ImageGenerationError,
>;
fn image_generation_request_builder<H>(
client: &Client<Self, H>,
_model: &str,
) -> Result<http_client::Builder, ImageGenerationError>
where
H: HttpClientExt,
{
Ok(client.post(Self::IMAGE_GENERATION_PATH)?)
}
fn image_generation_request_body(
model: &str,
request: ImageGenerationRequest,
) -> Result<serde_json::Value, ImageGenerationError>;
}
#[doc(hidden)]
#[derive(Clone)]
pub struct GenericImageGenerationModel<Ext, H = reqwest::Client> {
client: Client<Ext, H>,
pub model: String,
}
impl<Ext, H> GenericImageGenerationModel<Ext, H> {
pub fn new(client: Client<Ext, H>, model: impl Into<String>) -> Self {
Self {
client,
model: model.into(),
}
}
pub fn with_model(client: Client<Ext, H>, model: &str) -> Self {
Self::new(client, model)
}
}
impl<Ext, H> image_generation::ImageGenerationModel for GenericImageGenerationModel<Ext, H>
where
Ext: JsonImageGenerationProvider + Clone + WasmCompatSend + WasmCompatSync + 'static,
H: HttpClientExt + Clone + WasmCompatSend + WasmCompatSync + 'static,
{
type Response = Ext::Response;
type Client = Client<Ext, H>;
fn make(client: &Self::Client, model: impl Into<String>) -> Self {
Self::new(client.clone(), model)
}
async fn image_generation(
&self,
request: ImageGenerationRequest,
) -> Result<image_generation::ImageGenerationResponse<Self::Response>, ImageGenerationError>
{
let builder = Ext::image_generation_request_builder(&self.client, &self.model)?;
let body = Ext::image_generation_request_body(&self.model, request)?;
send_image_generation::<_, crate::providers::openai::client::ApiResponse<Ext::Response>>(
&self.client,
builder,
body,
)
.await
}
}
pub(crate) async fn send_image_generation<C, A>(
client: &C,
builder: http_client::Builder,
body: serde_json::Value,
) -> Result<image_generation::ImageGenerationResponse<A::Payload>, ImageGenerationError>
where
C: HttpClientExt,
A: DeserializeOwned + ProviderEnvelope,
A::Payload: TryInto<image_generation::ImageGenerationResponse<A::Payload>, Error = ImageGenerationError>,
{
let body = serde_json::to_vec(&body)?;
let req = builder
.body(body)
.map_err(|e| ImageGenerationError::HttpError(e.into()))?;
let response = client.send::<_, Bytes>(req).await?;
let (parts, body) = response.into_parts();
let status = parts.status;
let headers = Box::new(parts.headers);
let response_body = body.into_future().await?;
if !status.is_success() {
return Err(ImageGenerationError::from_http_response(
status,
String::from_utf8_lossy(&response_body).into_owned(),
)
.with_response_headers(Some(headers)));
}
match serde_json::from_slice::<A>(&response_body)?.into_payload() {
Ok(response) => response.try_into(),
Err(message) => {
tracing::warn!(message = %message, "provider returned an error response");
Err(ImageGenerationError::from_http_response(
status,
String::from_utf8_lossy(&response_body).into_owned(),
)
.with_response_headers(Some(headers)))
}
}
}
#[cfg(test)]
mod header_preservation_tests {
use super::*;
use crate::providers::internal::envelope::DirectPayload;
use crate::test_utils::RecordingHttpClient;
#[derive(serde::Deserialize)]
struct Payload;
impl TryFrom<Payload> for image_generation::ImageGenerationResponse<Payload> {
type Error = ImageGenerationError;
fn try_from(_: Payload) -> Result<Self, Self::Error> {
unreachable!("a 429 never reaches payload conversion")
}
}
#[tokio::test]
async fn non_success_response_preserves_headers() {
let mut headers = http::HeaderMap::new();
headers.insert(http::header::RETRY_AFTER, "20".parse().expect("value"));
let client = RecordingHttpClient::with_error_response_headers(
http::StatusCode::TOO_MANY_REQUESTS,
r#"{"error":"slow down"}"#,
headers,
);
let error = send_image_generation::<_, DirectPayload<Payload>>(
&client,
http_client::Request::builder()
.method(http::Method::POST)
.uri("https://example.test/v1/images/generations"),
serde_json::json!({}),
)
.await
.err()
.expect("a 429 should fail");
assert_eq!(
error
.provider_response_headers()
.and_then(|headers| headers.get(http::header::RETRY_AFTER))
.and_then(|value| value.to_str().ok()),
Some("20"),
);
assert_eq!(
error.provider_response_status(),
Some(http::StatusCode::TOO_MANY_REQUESTS)
);
}
}