rig_core/providers/gemini/
image_generation.rs1use super::ImageSize;
26use super::completion::usage_of;
27use crate::error::{EncodeError, ProviderError};
28use crate::image_generation;
29use crate::image_generation::ImageGenerationRequest;
30use crate::json_utils::Lenient;
31use crate::message::{DocumentSourceKind, Image};
32use crate::operation::ImageGeneration;
33use crate::wire::{Body, Descriptor, DocumentDecoder, Encoded, Framing, Mode, Wire};
34use base64::Engine;
35use base64::prelude::BASE64_STANDARD;
36use serde_json::{Value, json};
37
38pub const GEMINI_NANO_BANANA_2_1: &str = "gemini-nano-banana-2.1";
40pub const GEMINI_3_1_FLASH_IMAGE: &str = "gemini-3.1-flash-image";
42pub const GEMINI_3_1_FLASH_LITE_IMAGE: &str = "gemini-3.1-flash-lite-image";
44pub const GEMINI_3_PRO_IMAGE: &str = "gemini-3-pro-image";
46pub const GEMINI_2_5_FLASH_IMAGE: &str = "gemini-2.5-flash-image";
48
49const ASPECT_RATIOS: [(u32, u32); 14] = [
51 (1, 1),
52 (1, 4),
53 (4, 1),
54 (1, 8),
55 (8, 1),
56 (2, 3),
57 (3, 2),
58 (3, 4),
59 (4, 3),
60 (4, 5),
61 (5, 4),
62 (9, 16),
63 (16, 9),
64 (21, 9),
65];
66
67#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize)]
70#[serde(rename_all = "camelCase")]
71pub struct ImageOptions {
72 #[serde(skip_serializing_if = "Option::is_none")]
73 aspect_ratio: Option<String>,
74 #[serde(skip_serializing_if = "Option::is_none")]
75 image_size: Option<ImageSize>,
76 #[serde(skip)]
77 text: bool,
78}
79
80impl ImageOptions {
81 pub fn new() -> Self {
85 Self::default()
86 }
87
88 pub fn aspect_ratio(mut self, ratio: impl Into<String>) -> Self {
91 self.aspect_ratio = Some(ratio.into());
92 self
93 }
94
95 pub fn image_size(mut self, size: ImageSize) -> Self {
97 self.image_size = Some(size);
98 self
99 }
100
101 pub fn with_text(mut self) -> Self {
103 self.text = true;
104 self
105 }
106
107 pub fn params(self) -> Value {
110 let modalities = self.text.then(|| json!(["TEXT", "IMAGE"]));
111 let mut config = json!({ "imageConfig": self });
112 if let (Some(modalities), Some(config)) = (modalities, config.as_object_mut()) {
113 config.insert("responseModalities".to_owned(), modalities);
114 }
115 json!({ "generationConfig": config })
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
121pub enum ImagePart {
122 Text(String),
124 Image {
126 mime_type: String,
128 data: Vec<u8>,
130 },
131}
132
133pub fn parts_of(reply: &Value) -> Result<Vec<ImagePart>, ProviderError> {
141 let parts = reply
142 .arr("candidates")
143 .first()
144 .and_then(|candidate| candidate.get("content"))
145 .map(|content| content.arr("parts"))
146 .unwrap_or_default();
147 let mut out = Vec::new();
148 for part in parts
149 .iter()
150 .filter(|part| part.bool("thought") != Some(true))
151 {
152 if let Some(text) = part.str("text") {
153 out.push(ImagePart::Text(text.to_owned()));
154 }
155 let Some(blob) = part.get("inlineData") else {
156 continue;
157 };
158 let mime_type = blob.str("mimeType").unwrap_or_default();
159 let Some(data) = blob.str("data").filter(|_| mime_type.starts_with("image/")) else {
160 continue;
161 };
162 let data = BASE64_STANDARD.decode(data).map_err(|err| {
163 ProviderError::Response(format!("Gemini image data was not valid base64: {err}"))
164 })?;
165 out.push(ImagePart::Image {
166 mime_type: mime_type.to_owned(),
167 data,
168 });
169 }
170 Ok(out)
171}
172
173pub fn image_of(reply: &Value) -> Result<image_generation::ImageGenerationResponse, ProviderError> {
179 let image = parts_of(reply)?
180 .into_iter()
181 .find_map(|part| match part {
182 ImagePart::Image { data, .. } => Some(data),
183 ImagePart::Text(_) => None,
184 })
185 .ok_or_else(|| {
186 ProviderError::Response(
187 "Gemini image generation response did not include image data".into(),
188 )
189 })?;
190 Ok(image_generation::ImageGenerationResponse {
191 model: reply.str("modelVersion").map(str::to_owned),
192 response_id: Some(reply.str("responseId").unwrap_or_default().to_owned()),
193 usage: reply.get("usageMetadata").map(usage_of).unwrap_or_default(),
194 ..image_generation::ImageGenerationResponse::new(image)
195 })
196}
197
198fn create_request_body(request: ImageGenerationRequest) -> Result<Value, EncodeError> {
199 let mut parts = vec![json!({ "text": request.prompt })];
200 for image in request.images {
201 parts.push(reference(image)?);
202 }
203 let mut image_config = serde_json::Map::new();
204 if let Some(ratio) = aspect_ratio(request.width, request.height) {
205 image_config.insert("aspectRatio".to_owned(), json!(ratio));
206 }
207 let mut body = json!({
208 "contents": [{ "role": "user", "parts": parts }],
209 "toolConfig": null,
210 "generationConfig": { "responseModalities": ["IMAGE"], "imageConfig": image_config },
211 "safetySettings": null,
212 "systemInstruction": null,
213 });
214 match (&mut body, request.additional_params) {
215 (Value::Object(body), Some(Value::Object(params))) => {
216 crate::completion::options::deep_merge(body, params);
217 }
218 (body, Some(params)) => *body = params,
219 (_, None) => {}
220 }
221 Ok(body)
222}
223
224fn reference(image: Image) -> Result<Value, EncodeError> {
226 let source = match image.data {
227 DocumentSourceKind::Raw(bytes) => DocumentSourceKind::Base64(BASE64_STANDARD.encode(bytes)),
228 source => source,
229 };
230 super::completion::media(super::completion::mime(image.media_type), &source, false)
231}
232
233fn aspect_ratio(width: u32, height: u32) -> Option<String> {
237 let (width, height) = (u64::from(width), u64::from(height));
238 ASPECT_RATIOS
239 .iter()
240 .find(|&&(w, h)| width > 0 && width * u64::from(h) == height * u64::from(w))
241 .map(|(w, h)| format!("{w}:{h}"))
242}
243
244#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
248pub struct Images {
249 pub provider: super::GeminiConfig,
251 pub model: String,
253}
254
255impl Images {
256 pub fn new(provider: super::GeminiConfig, model: impl Into<String>) -> Self {
258 Self {
259 provider,
260 model: model.into(),
261 }
262 }
263}
264
265impl Wire for Images {
266 type Op = ImageGeneration;
267 type Payload = crate::wire::Encoded;
268 type Frame = crate::wire::WireFrame;
269 type Event = Value;
270 type Decoder<'id> = DocumentDecoder<ImageGeneration, Value>;
271 type Reassembler = crate::wire::document::Unreassembled;
272
273 fn describe(&self) -> Descriptor<'_> {
274 Descriptor::new(super::PROVIDER_NAME).model(self.model.as_str())
275 }
276
277 fn encode(&self, request: ImageGenerationRequest, _mode: Mode) -> Result<Encoded, EncodeError> {
278 let body = serde_json::to_vec(&create_request_body(request)?)?;
279 let request = http::Request::post(format!(
280 "{}/v1beta/models/{}:generateContent?key={}",
281 self.provider.base_url,
282 self.model,
283 self.provider.api_key.expose()
284 ))
285 .header(http::header::CONTENT_TYPE, "application/json")
286 .body(Body::Bytes(body))?;
287 Ok(Encoded::new(request, Framing::Whole))
289 }
290
291 fn decoder<'id>(&self) -> Self::Decoder<'id> {
294 DocumentDecoder::new(super::completion::classify_reply, |reply| image_of(&reply))
295 }
296}
297
298impl super::GeminiConfig {
299 pub(crate) fn image_generation(&self, model: impl Into<String>) -> Images {
301 Images::new(self.clone(), model)
302 }
303}
304
305#[cfg(test)]
306mod tests;