use serde_json::{Value, json};
use crate::error::{LiterLlmError, Result};
const IMAGEN_ASPECT_RATIOS: [(u64, u64); 5] = [(1, 1), (3, 4), (4, 3), (9, 16), (16, 9)];
pub(super) fn is_image_request(body: &Value) -> bool {
body.get("prompt").is_some() && body.get("messages").is_none() && body.get("input").is_none()
}
fn gcd(a: u64, b: u64) -> u64 {
if b == 0 { a } else { gcd(b, a % b) }
}
fn aspect_ratio_for_size(size: &str) -> Result<String> {
let parsed = size.split_once('x').and_then(|(width, height)| {
let width = width.trim().parse::<u64>().ok().filter(|v| *v > 0)?;
let height = height.trim().parse::<u64>().ok().filter(|v| *v > 0)?;
Some((width, height))
});
let (width, height) = parsed.ok_or_else(|| LiterLlmError::BadRequest {
message: format!("image size `{size}` is not of the form WIDTHxHEIGHT"),
status: 400,
})?;
let divisor = gcd(width, height);
let ratio = (width / divisor, height / divisor);
if IMAGEN_ASPECT_RATIOS.contains(&ratio) {
Ok(format!("{}:{}", ratio.0, ratio.1))
} else {
Err(LiterLlmError::BadRequest {
message: format!(
"Imagen sets the shape of an image with an aspect ratio, and size `{size}` is not 1:1, 3:4, 4:3, 9:16 or 16:9"
),
status: 400,
})
}
}
pub(super) fn transform_imagen_request(body: &mut Value) -> Result<()> {
let prompt = body.get("prompt").cloned().unwrap_or_default();
if body.get("response_format").and_then(Value::as_str) == Some("url") {
return Err(LiterLlmError::BadRequest {
message: "Imagen returns image bytes, not URLs; omit response_format or set it to b64_json".into(),
status: 400,
});
}
let mut parameters = serde_json::Map::new();
if let Some(n) = body.get("n").and_then(Value::as_u64) {
parameters.insert("sampleCount".into(), json!(n));
}
if let Some(size) = body.get("size").and_then(Value::as_str) {
parameters.insert("aspectRatio".into(), json!(aspect_ratio_for_size(size)?));
}
for field in ["quality", "style", "user"] {
if body.get(field).is_some() {
tracing::warn!("image request set `{field}`, which has no Imagen equivalent and was dropped");
}
}
*body = json!({"instances": [{"prompt": prompt}], "parameters": parameters});
Ok(())
}
pub(super) fn is_image_response(body: &Value) -> bool {
body.get("predictions")
.and_then(Value::as_array)
.is_some_and(|predictions| {
predictions
.iter()
.any(|p| p.get("bytesBase64Encoded").is_some() || p.get("raiFilteredReason").is_some())
})
}
pub(super) fn transform_imagen_response(body: &Value) -> Value {
let data: Vec<Value> = body
.get("predictions")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|prediction| {
let bytes = prediction.get("bytesBase64Encoded")?;
let mut image = json!({"b64_json": bytes});
if let Some(prompt) = prediction.get("prompt") {
image["revised_prompt"] = prompt.clone();
}
Some(image)
})
.collect();
json!({"created": crate::provider::unix_timestamp_secs(), "data": data})
}
pub(super) fn transform_publisher_models(models: &[Value]) -> Value {
let data: Vec<Value> = models
.iter()
.map(|model| {
let name = model.get("name").and_then(Value::as_str).unwrap_or("");
let (publisher, id) = match name
.strip_prefix("publishers/")
.and_then(|rest| rest.split_once("/models/"))
{
Some((publisher, id)) => (publisher, id),
None => ("google", name),
};
json!({"id": id, "object": "model", "created": 0, "owned_by": publisher})
})
.collect();
json!({"object": "list", "data": data})
}