use super::ImageSize;
use super::completion::usage_of;
use crate::error::{EncodeError, ProviderError};
use crate::image_generation;
use crate::image_generation::ImageGenerationRequest;
use crate::json_utils::Lenient;
use crate::message::{DocumentSourceKind, Image};
use crate::operation::ImageGeneration;
use crate::wire::{Body, Descriptor, DocumentDecoder, Encoded, Framing, Mode, Wire};
use base64::Engine;
use base64::prelude::BASE64_STANDARD;
use serde_json::{Value, json};
pub const GEMINI_NANO_BANANA_2_1: &str = "gemini-nano-banana-2.1";
pub const GEMINI_3_1_FLASH_IMAGE: &str = "gemini-3.1-flash-image";
pub const GEMINI_3_1_FLASH_LITE_IMAGE: &str = "gemini-3.1-flash-lite-image";
pub const GEMINI_3_PRO_IMAGE: &str = "gemini-3-pro-image";
pub const GEMINI_2_5_FLASH_IMAGE: &str = "gemini-2.5-flash-image";
const ASPECT_RATIOS: [(u32, u32); 14] = [
(1, 1),
(1, 4),
(4, 1),
(1, 8),
(8, 1),
(2, 3),
(3, 2),
(3, 4),
(4, 3),
(4, 5),
(5, 4),
(9, 16),
(16, 9),
(21, 9),
];
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ImageOptions {
#[serde(skip_serializing_if = "Option::is_none")]
aspect_ratio: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image_size: Option<ImageSize>,
#[serde(skip)]
text: bool,
}
impl ImageOptions {
pub fn new() -> Self {
Self::default()
}
pub fn aspect_ratio(mut self, ratio: impl Into<String>) -> Self {
self.aspect_ratio = Some(ratio.into());
self
}
pub fn image_size(mut self, size: ImageSize) -> Self {
self.image_size = Some(size);
self
}
pub fn with_text(mut self) -> Self {
self.text = true;
self
}
pub fn params(self) -> Value {
let modalities = self.text.then(|| json!(["TEXT", "IMAGE"]));
let mut config = json!({ "imageConfig": self });
if let (Some(modalities), Some(config)) = (modalities, config.as_object_mut()) {
config.insert("responseModalities".to_owned(), modalities);
}
json!({ "generationConfig": config })
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImagePart {
Text(String),
Image {
mime_type: String,
data: Vec<u8>,
},
}
pub fn parts_of(reply: &Value) -> Result<Vec<ImagePart>, ProviderError> {
let parts = reply
.arr("candidates")
.first()
.and_then(|candidate| candidate.get("content"))
.map(|content| content.arr("parts"))
.unwrap_or_default();
let mut out = Vec::new();
for part in parts
.iter()
.filter(|part| part.bool("thought") != Some(true))
{
if let Some(text) = part.str("text") {
out.push(ImagePart::Text(text.to_owned()));
}
let Some(blob) = part.get("inlineData") else {
continue;
};
let mime_type = blob.str("mimeType").unwrap_or_default();
let Some(data) = blob.str("data").filter(|_| mime_type.starts_with("image/")) else {
continue;
};
let data = BASE64_STANDARD.decode(data).map_err(|err| {
ProviderError::Response(format!("Gemini image data was not valid base64: {err}"))
})?;
out.push(ImagePart::Image {
mime_type: mime_type.to_owned(),
data,
});
}
Ok(out)
}
pub fn image_of(reply: &Value) -> Result<image_generation::ImageGenerationResponse, ProviderError> {
let image = parts_of(reply)?
.into_iter()
.find_map(|part| match part {
ImagePart::Image { data, .. } => Some(data),
ImagePart::Text(_) => None,
})
.ok_or_else(|| {
ProviderError::Response(
"Gemini image generation response did not include image data".into(),
)
})?;
Ok(image_generation::ImageGenerationResponse {
model: reply.str("modelVersion").map(str::to_owned),
response_id: Some(reply.str("responseId").unwrap_or_default().to_owned()),
usage: reply.get("usageMetadata").map(usage_of).unwrap_or_default(),
..image_generation::ImageGenerationResponse::new(image)
})
}
fn create_request_body(request: ImageGenerationRequest) -> Result<Value, EncodeError> {
let mut parts = vec![json!({ "text": request.prompt })];
for image in request.images {
parts.push(reference(image)?);
}
let mut image_config = serde_json::Map::new();
if let Some(ratio) = aspect_ratio(request.width, request.height) {
image_config.insert("aspectRatio".to_owned(), json!(ratio));
}
let mut body = json!({
"contents": [{ "role": "user", "parts": parts }],
"toolConfig": null,
"generationConfig": { "responseModalities": ["IMAGE"], "imageConfig": image_config },
"safetySettings": null,
"systemInstruction": null,
});
match (&mut body, request.additional_params) {
(Value::Object(body), Some(Value::Object(params))) => {
crate::completion::options::deep_merge(body, params);
}
(body, Some(params)) => *body = params,
(_, None) => {}
}
Ok(body)
}
fn reference(image: Image) -> Result<Value, EncodeError> {
let source = match image.data {
DocumentSourceKind::Raw(bytes) => DocumentSourceKind::Base64(BASE64_STANDARD.encode(bytes)),
source => source,
};
super::completion::media(super::completion::mime(image.media_type), &source, false)
}
fn aspect_ratio(width: u32, height: u32) -> Option<String> {
let (width, height) = (u64::from(width), u64::from(height));
ASPECT_RATIOS
.iter()
.find(|&&(w, h)| width > 0 && width * u64::from(h) == height * u64::from(w))
.map(|(w, h)| format!("{w}:{h}"))
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Images {
pub provider: super::GeminiConfig,
pub model: String,
}
impl Images {
pub fn new(provider: super::GeminiConfig, model: impl Into<String>) -> Self {
Self {
provider,
model: model.into(),
}
}
}
impl Wire for Images {
type Op = ImageGeneration;
type Payload = crate::wire::Encoded;
type Frame = crate::wire::WireFrame;
type Event = Value;
type Decoder<'id> = DocumentDecoder<ImageGeneration, Value>;
type Reassembler = crate::wire::document::Unreassembled;
fn describe(&self) -> Descriptor<'_> {
Descriptor::new(super::PROVIDER_NAME).model(self.model.as_str())
}
fn encode(&self, request: ImageGenerationRequest, _mode: Mode) -> Result<Encoded, EncodeError> {
let body = serde_json::to_vec(&create_request_body(request)?)?;
let request = http::Request::post(format!(
"{}/v1beta/models/{}:generateContent?key={}",
self.provider.base_url,
self.model,
self.provider.api_key.expose()
))
.header(http::header::CONTENT_TYPE, "application/json")
.body(Body::Bytes(body))?;
Ok(Encoded::new(request, Framing::Whole))
}
fn decoder<'id>(&self) -> Self::Decoder<'id> {
DocumentDecoder::new(super::completion::classify_reply, |reply| image_of(&reply))
}
}
impl super::GeminiConfig {
pub(crate) fn image_generation(&self, model: impl Into<String>) -> Images {
Images::new(self.clone(), model)
}
}
#[cfg(test)]
mod tests;