use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use deepseek_recipe_core::multimodal::{
ImageDetail, ImageMediaType, ImageSource, MAX_IMAGE_URL_LEN, is_http_url,
};
use crate::request::ConversionError;
pub const UNSUPPORTED_DOCUMENT_PLACEHOLDER: &str = "[Unsupported Document]";
pub const UNSUPPORTED_IMAGE_ERR: &str = "You have uploaded an unsupported image. Please make sure your image is valid and has one of the following formats: webp, png, jpeg, and gif.";
pub(super) fn external_image_source(
url: String,
detail: Option<ImageDetail>,
path: &str,
) -> Result<ImageSource, ConversionError> {
if !is_http_url(&url) {
return Err(ConversionError::bad_request(format!(
"{path}: invalid url: {url:?}"
)));
}
if url.len() > MAX_IMAGE_URL_LEN {
return Err(ConversionError::bad_request(format!(
"{path}: external link length {} too long, max link length {MAX_IMAGE_URL_LEN}. Consider passing file data directly with \
base64 data url instead.",
url.len()
)));
}
Ok(ImageSource::Url {
url,
detail: detail.unwrap_or_default(),
})
}
pub(super) fn data_url_image_source(data_url: String, detail: Option<ImageDetail>) -> ImageSource {
ImageSource::DataUrl {
data_url,
detail: detail.unwrap_or_default(),
}
}
pub(super) fn image_url_source(
url: String,
detail: Option<ImageDetail>,
path: &str,
) -> Result<ImageSource, ConversionError> {
if is_http_url(&url) {
external_image_source(url, detail, path)
} else if url.contains("base64,") {
Ok(data_url_image_source(url, detail))
} else {
Err(ConversionError::bad_request(format!(
"{path}: Unsupported image_url format"
)))
}
}
pub(super) fn base64_image_source(
media_type: &str,
data: String,
path: &str,
) -> Result<ImageSource, ConversionError> {
let media_type =
ImageMediaType::from_mime(media_type).ok_or_else(|| unsupported_image(path))?;
let data = normalize_base64_padding(data);
if STANDARD.decode(&data).is_err() {
return Err(ConversionError::bad_request(format!(
"{path}: base64 decode error"
)));
}
Ok(ImageSource::DataUrl {
data_url: format!("data:{};base64,{data}", media_type.mime()),
detail: ImageDetail::default(),
})
}
pub(super) fn unsupported_image(path: &str) -> ConversionError {
ConversionError::bad_request(format!("{path}: {UNSUPPORTED_IMAGE_ERR}"))
}
pub(super) fn image_not_allowed(role: &str) -> ConversionError {
ConversionError::bad_request(format!("Image in {role} message is unsupported"))
}
pub(super) fn file_id_unsupported(path: &str) -> ConversionError {
ConversionError::bad_request(format!(
"{path}: file_id is not supported. Pass the image as a base64 data url instead."
))
}
fn normalize_base64_padding(mut data: String) -> String {
if !data.ends_with('=') {
return data;
}
let unpadded_len = data.trim_end_matches('=').len();
let remainder = unpadded_len % 4;
if remainder == 1 {
return data;
}
data.truncate(unpadded_len);
data.push_str(&"=".repeat((4 - remainder) % 4));
data
}