Skip to main content

litellm_rust/providers/
gemini.rs

1use crate::config::ProviderConfig;
2use crate::error::{LiteLLMError, Result};
3use crate::http::send_json;
4use crate::providers::{merge_extra_body, resolve_api_key};
5use crate::types::{
6    ChatContentPart, ChatContentPartFile, ChatContentPartImageUrl, ChatContentPartInputAudio,
7    ChatContentPartText, ChatFile, ChatImageUrl, ChatInputAudio, ChatMessage, ChatMessageContent,
8    ChatRequest, ChatResponse, ImageData, ImageEditRequest, ImageInputData, ImageRequest,
9    ImageResponse, Usage, VideoRequest, VideoResponse,
10};
11use base64::{engine::general_purpose, Engine as _};
12use mime_guess::MimeGuess;
13use reqwest::header::CONTENT_TYPE;
14use reqwest::Client;
15use serde_json::Value;
16use std::env;
17use tokio::time::{sleep, Duration};
18
19/// Default maximum polling attempts for video generation (240 * 5s = 20 minutes)
20pub const DEFAULT_VIDEO_MAX_POLL_ATTEMPTS: u32 = 240;
21/// Default polling interval for video generation status checks
22pub const DEFAULT_VIDEO_POLL_INTERVAL_SECS: u64 = 5;
23
24pub async fn chat(client: &Client, cfg: &ProviderConfig, req: ChatRequest) -> Result<ChatResponse> {
25    let base = cfg
26        .base_url
27        .clone()
28        .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
29    let key = resolve_api_key(cfg)?
30        .ok_or_else(|| LiteLLMError::MissingApiKey("GEMINI_API_KEY".into()))?;
31    let model = req.model.trim_start_matches("models/");
32    let url = format!(
33        "{}/models/{}:generateContent",
34        base.trim_end_matches('/'),
35        model
36    );
37
38    let mut messages = req.messages;
39    let system_instruction = extract_system_instruction(&mut messages)?;
40    if messages.is_empty() && !system_instruction.is_empty() {
41        messages.push(ChatMessage {
42            role: "user".to_string(),
43            content: ChatMessageContent::Text(" ".to_string()),
44            name: None,
45            tool_call_id: None,
46            tool_calls: None,
47            function_call: None,
48            provider_specific_fields: None,
49        });
50    }
51
52    let contents = gemini_contents_from_messages(client, messages, model).await?;
53
54    let mut body = serde_json::json!({ "contents": contents });
55    if !system_instruction.is_empty() {
56        body["system_instruction"] = serde_json::json!({ "parts": system_instruction });
57    }
58
59    // Build generationConfig safely without unwrap
60    let mut generation_config = serde_json::Map::new();
61    if let Some(temp) = req.temperature {
62        generation_config.insert("temperature".to_string(), serde_json::json!(temp));
63    }
64    if let Some(max_tokens) = req.max_tokens {
65        generation_config.insert("maxOutputTokens".to_string(), serde_json::json!(max_tokens));
66    }
67    if !generation_config.is_empty() {
68        body["generationConfig"] = Value::Object(generation_config);
69    }
70    merge_extra_body(
71        &mut body,
72        req.extra_body.as_ref(),
73        &["contents", "system_instruction", "generationConfig"],
74    )?;
75
76    let mut builder = client.post(url).header("x-goog-api-key", key).json(&body);
77    for (k, v) in &cfg.extra_headers {
78        builder = builder.header(k, v);
79    }
80
81    let (resp, _headers) = send_json::<Value>(builder).await?;
82    let content = extract_text(&resp);
83    let usage = parse_usage(&resp);
84    let debug = env::var("LITELLM_GEMINI_DEBUG").ok().as_deref() == Some("1")
85        || env::var("YOETZ_GEMINI_DEBUG").ok().as_deref() == Some("1");
86    if debug {
87        if let Ok(pretty) = serde_json::to_string_pretty(&resp) {
88            eprintln!("litellm-rust gemini raw response:\n{pretty}");
89        }
90    }
91
92    Ok(ChatResponse {
93        content,
94        reasoning: None,
95        usage,
96        response_id: None,
97        header_cost: None,
98        raw: if debug { Some(resp) } else { None },
99    })
100}
101
102/// Video generation options for configurable timeouts.
103#[derive(Debug, Clone)]
104pub struct VideoGenerationOptions {
105    /// Maximum number of polling attempts
106    pub max_poll_attempts: u32,
107    /// Interval between polling attempts in seconds
108    pub poll_interval_secs: u64,
109}
110
111impl Default for VideoGenerationOptions {
112    fn default() -> Self {
113        Self {
114            max_poll_attempts: DEFAULT_VIDEO_MAX_POLL_ATTEMPTS,
115            poll_interval_secs: DEFAULT_VIDEO_POLL_INTERVAL_SECS,
116        }
117    }
118}
119
120pub async fn video_generation(
121    client: &Client,
122    cfg: &ProviderConfig,
123    req: VideoRequest,
124) -> Result<VideoResponse> {
125    video_generation_with_options(client, cfg, req, VideoGenerationOptions::default()).await
126}
127
128pub async fn video_generation_with_options(
129    client: &Client,
130    cfg: &ProviderConfig,
131    req: VideoRequest,
132    options: VideoGenerationOptions,
133) -> Result<VideoResponse> {
134    let base = cfg
135        .base_url
136        .clone()
137        .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
138    let key = resolve_api_key(cfg)?
139        .ok_or_else(|| LiteLLMError::MissingApiKey("GEMINI_API_KEY".into()))?;
140    let model = req.model.trim_start_matches("models/");
141
142    let url = format!(
143        "{}/models/{}:predictLongRunning",
144        base.trim_end_matches('/'),
145        model
146    );
147
148    let mut parameters = serde_json::Map::new();
149    if let Some(seconds) = req.seconds {
150        parameters.insert("durationSeconds".to_string(), serde_json::json!(seconds));
151    }
152    if let Some(size) = req.size {
153        parameters.insert("resolution".to_string(), serde_json::json!(size));
154    }
155
156    let body = serde_json::json!({
157        "instances": [{ "prompt": req.prompt }],
158        "parameters": Value::Object(parameters),
159    });
160
161    let mut builder = client.post(url).header("x-goog-api-key", &key).json(&body);
162    for (k, v) in &cfg.extra_headers {
163        builder = builder.header(k, v);
164    }
165
166    let (resp, _headers) = send_json::<Value>(builder).await?;
167    let op_name = resp
168        .get("name")
169        .and_then(|v| v.as_str())
170        .ok_or_else(|| LiteLLMError::Parse("missing operation name".into()))?;
171
172    let op_url = if op_name.starts_with("http") {
173        op_name.to_string()
174    } else {
175        format!("{}/{}", base.trim_end_matches('/'), op_name)
176    };
177
178    let poll_interval = Duration::from_secs(options.poll_interval_secs);
179
180    for attempt in 0..options.max_poll_attempts {
181        let mut poll = client.get(&op_url).header("x-goog-api-key", &key);
182        for (k, v) in &cfg.extra_headers {
183            poll = poll.header(k, v);
184        }
185        let (op_resp, _headers) = send_json::<Value>(poll).await?;
186
187        if op_resp.get("done").and_then(|v| v.as_bool()) == Some(true) {
188            if op_resp.get("error").is_some() {
189                return Err(LiteLLMError::http("video generation failed"));
190            }
191
192            let uri = extract_video_uri(&op_resp)
193                .ok_or_else(|| LiteLLMError::Parse("missing video uri".into()))?;
194
195            return Ok(VideoResponse {
196                video_url: Some(uri),
197                raw: None,
198            });
199        }
200
201        if attempt + 1 >= options.max_poll_attempts {
202            return Err(LiteLLMError::http(format!(
203                "video generation timed out after {} attempts",
204                options.max_poll_attempts
205            )));
206        }
207        sleep(poll_interval).await;
208    }
209
210    Err(LiteLLMError::http("video generation timed out"))
211}
212
213pub async fn image_generation(
214    client: &Client,
215    cfg: &ProviderConfig,
216    req: ImageRequest,
217) -> Result<ImageResponse> {
218    let base = cfg
219        .base_url
220        .clone()
221        .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
222    let key = resolve_api_key(cfg)?
223        .ok_or_else(|| LiteLLMError::MissingApiKey("GEMINI_API_KEY".into()))?;
224    let model = req.model.trim_start_matches("models/");
225    let n = req.n.unwrap_or(1);
226
227    if model.contains("gemini") {
228        // Path A: Gemini native image generation via generateContent
229        let url = format!(
230            "{}/models/{}:generateContent",
231            base.trim_end_matches('/'),
232            model
233        );
234
235        let mut body = serde_json::json!({
236            "contents": [{ "parts": [{ "text": req.prompt }] }],
237            "generationConfig": { "response_modalities": ["IMAGE", "TEXT"] }
238        });
239        merge_extra_body(
240            &mut body,
241            req.extra_body.as_ref(),
242            &["contents", "generationConfig"],
243        )?;
244
245        let mut builder = client.post(url).header("x-goog-api-key", &key).json(&body);
246        for (k, v) in &cfg.extra_headers {
247            builder = builder.header(k, v);
248        }
249
250        let (resp, _headers) = send_json::<Value>(builder).await?;
251        let usage = parse_usage(&resp);
252        let images = extract_images_from_candidates(&resp);
253
254        Ok(ImageResponse {
255            images,
256            usage,
257            raw: Some(resp),
258        })
259    } else {
260        // Path B: Imagen models via predict endpoint
261        let url = format!("{}/models/{}:predict", base.trim_end_matches('/'), model);
262
263        let mut body = serde_json::json!({
264            "instances": [{ "prompt": req.prompt }],
265            "parameters": { "sampleCount": n }
266        });
267        merge_extra_body(
268            &mut body,
269            req.extra_body.as_ref(),
270            &["instances", "parameters"],
271        )?;
272
273        let mut builder = client.post(url).header("x-goog-api-key", &key).json(&body);
274        for (k, v) in &cfg.extra_headers {
275            builder = builder.header(k, v);
276        }
277
278        let (resp, _headers) = send_json::<Value>(builder).await?;
279        let images = extract_images_from_predictions(&resp);
280
281        Ok(ImageResponse {
282            images,
283            usage: Usage::default(),
284            raw: Some(resp),
285        })
286    }
287}
288
289pub async fn image_editing(
290    client: &Client,
291    cfg: &ProviderConfig,
292    req: ImageEditRequest,
293) -> Result<ImageResponse> {
294    let base = cfg
295        .base_url
296        .clone()
297        .ok_or_else(|| LiteLLMError::Config("base_url required".into()))?;
298    let key = resolve_api_key(cfg)?
299        .ok_or_else(|| LiteLLMError::MissingApiKey("GEMINI_API_KEY".into()))?;
300    let model = req.model.trim_start_matches("models/");
301
302    let url = format!(
303        "{}/models/{}:generateContent",
304        base.trim_end_matches('/'),
305        model
306    );
307
308    let mut parts = Vec::with_capacity(req.images.len() + 1);
309    parts.push(serde_json::json!({ "text": req.prompt }));
310    for image in &req.images {
311        parts.push(image_input_to_part(client, image).await?);
312    }
313
314    let body = serde_json::json!({
315        "contents": [{ "role": "user", "parts": parts }],
316        "generationConfig": {
317            "response_modalities": ["IMAGE", "TEXT"]
318        }
319    });
320
321    let mut builder = client.post(url).header("x-goog-api-key", &key).json(&body);
322    for (k, v) in &cfg.extra_headers {
323        builder = builder.header(k, v);
324    }
325
326    let (resp, _headers) = send_json::<Value>(builder).await?;
327    let usage = parse_usage(&resp);
328    let images = extract_images_from_candidates(&resp);
329
330    Ok(ImageResponse {
331        images,
332        usage,
333        raw: Some(resp),
334    })
335}
336
337async fn image_input_to_part(_client: &Client, input: &ImageInputData) -> Result<Value> {
338    if let Some(b64) = &input.b64_json {
339        let mime = input
340            .mime_type
341            .as_deref()
342            .unwrap_or("image/png")
343            .to_string();
344        return Ok(serde_json::json!({
345            "inline_data": { "mime_type": mime, "data": b64 }
346        }));
347    }
348    if let Some(url) = &input.url {
349        return process_gemini_media_url(url, input.mime_type.as_deref(), None, None, "");
350    }
351    Err(LiteLLMError::Config(
352        "ImageInputData requires b64_json or url".into(),
353    ))
354}
355
356fn extract_images_from_candidates(resp: &Value) -> Vec<ImageData> {
357    let mut images = Vec::new();
358    if let Some(candidates) = resp.get("candidates").and_then(|v| v.as_array()) {
359        for candidate in candidates {
360            if let Some(parts) = candidate
361                .get("content")
362                .and_then(|v| v.get("parts"))
363                .and_then(|v| v.as_array())
364            {
365                for part in parts {
366                    if let Some(data) = part
367                        .get("inlineData")
368                        .and_then(|v| v.get("data"))
369                        .and_then(|v| v.as_str())
370                    {
371                        let mime = part
372                            .get("inlineData")
373                            .and_then(|v| v.get("mimeType"))
374                            .and_then(|v| v.as_str())
375                            .map(|s| s.to_string());
376                        images.push(ImageData {
377                            b64_json: Some(data.to_string()),
378                            url: None,
379                            revised_prompt: None,
380                            mime_type: mime,
381                        });
382                    }
383                }
384            }
385        }
386    }
387    images
388}
389
390fn extract_images_from_predictions(resp: &Value) -> Vec<ImageData> {
391    let mut images = Vec::new();
392    if let Some(predictions) = resp.get("predictions").and_then(|v| v.as_array()) {
393        for prediction in predictions {
394            if let Some(data) = prediction
395                .get("bytesBase64Encoded")
396                .and_then(|v| v.as_str())
397            {
398                let mime = prediction
399                    .get("mimeType")
400                    .and_then(|v| v.as_str())
401                    .map(|s| s.to_string());
402                images.push(ImageData {
403                    b64_json: Some(data.to_string()),
404                    url: None,
405                    revised_prompt: None,
406                    mime_type: mime,
407                });
408            }
409        }
410    }
411    images
412}
413
414fn extract_text(resp: &Value) -> String {
415    if let Some(candidates) = resp.get("candidates").and_then(|v| v.as_array()) {
416        if let Some(first) = candidates.first() {
417            if let Some(parts) = first
418                .get("content")
419                .and_then(|v| v.get("parts"))
420                .and_then(|v| v.as_array())
421            {
422                let mut text = String::new();
423                for part in parts {
424                    if let Some(piece) = part.get("text").and_then(|v| v.as_str()) {
425                        text.push_str(piece);
426                    }
427                }
428                return text;
429            }
430        }
431    }
432    String::new()
433}
434
435fn parse_usage(resp: &Value) -> Usage {
436    if let Some(meta) = resp.get("usageMetadata").and_then(|v| v.as_object()) {
437        return Usage {
438            prompt_tokens: meta.get("promptTokenCount").and_then(|v| v.as_u64()),
439            completion_tokens: meta.get("candidatesTokenCount").and_then(|v| v.as_u64()),
440            thoughts_tokens: meta.get("thoughtsTokenCount").and_then(|v| v.as_u64()),
441            total_tokens: meta.get("totalTokenCount").and_then(|v| v.as_u64()),
442            cost_usd: None,
443        };
444    }
445    Usage::default()
446}
447
448fn extract_system_instruction(messages: &mut Vec<ChatMessage>) -> Result<Vec<Value>> {
449    let mut parts = Vec::new();
450    let mut indices = Vec::new();
451    for (idx, msg) in messages.iter().enumerate() {
452        if msg.role == "system" {
453            match &msg.content {
454                ChatMessageContent::Text(text) => {
455                    if !text.is_empty() {
456                        parts.push(serde_json::json!({ "text": text }));
457                    }
458                }
459                ChatMessageContent::Parts(content_parts) => {
460                    let mut system_text = String::new();
461                    for part in content_parts {
462                        if let ChatContentPart::Text(ChatContentPartText { text, .. }) = part {
463                            system_text.push_str(text);
464                        }
465                    }
466                    if !system_text.is_empty() {
467                        parts.push(serde_json::json!({ "text": system_text }));
468                    }
469                }
470            }
471            indices.push(idx);
472        }
473    }
474    for idx in indices.into_iter().rev() {
475        messages.remove(idx);
476    }
477    Ok(parts)
478}
479
480async fn gemini_contents_from_messages(
481    client: &Client,
482    messages: Vec<ChatMessage>,
483    model: &str,
484) -> Result<Vec<Value>> {
485    let mut contents: Vec<Value> = Vec::new();
486    let mut msg_i = 0;
487    let mut tool_call_responses: Vec<Value> = Vec::new();
488    let mut last_tool_calls: Vec<ToolCallInfo> = Vec::new();
489
490    while msg_i < messages.len() {
491        let role = messages[msg_i].role.as_str();
492        if role == "user" {
493            let mut parts: Vec<Value> = Vec::new();
494            while msg_i < messages.len() && messages[msg_i].role == "user" {
495                parts.extend(
496                    gemini_parts_from_content(client, &messages[msg_i].content, model).await?,
497                );
498                msg_i += 1;
499            }
500            if !parts.is_empty() {
501                contents.push(serde_json::json!({ "role": "user", "parts": parts }));
502            }
503            continue;
504        }
505
506        if role == "assistant" {
507            let mut parts: Vec<Value> = Vec::new();
508            while msg_i < messages.len() && messages[msg_i].role == "assistant" {
509                let message = &messages[msg_i];
510                parts.extend(gemini_parts_from_content(client, &message.content, model).await?);
511                let (tool_parts, tool_infos) = tool_call_parts_from_message(message)?;
512                if !tool_parts.is_empty() {
513                    parts.extend(tool_parts);
514                    if !tool_infos.is_empty() {
515                        last_tool_calls = tool_infos;
516                    }
517                }
518                msg_i += 1;
519            }
520            if !parts.is_empty() {
521                contents.push(serde_json::json!({ "role": "model", "parts": parts }));
522            }
523            continue;
524        }
525
526        if role == "tool" || role == "function" {
527            while msg_i < messages.len()
528                && (messages[msg_i].role == "tool" || messages[msg_i].role == "function")
529            {
530                let response_parts =
531                    tool_response_parts(client, &messages[msg_i], &last_tool_calls).await?;
532                tool_call_responses.extend(response_parts);
533                msg_i += 1;
534            }
535            if !tool_call_responses.is_empty() {
536                contents.push(serde_json::json!({ "parts": tool_call_responses }));
537                tool_call_responses = Vec::new();
538            }
539            continue;
540        }
541
542        let parts = gemini_parts_from_content(client, &messages[msg_i].content, model).await?;
543        if !parts.is_empty() {
544            contents.push(serde_json::json!({ "role": "user", "parts": parts }));
545        }
546        msg_i += 1;
547    }
548
549    if !tool_call_responses.is_empty() {
550        contents.push(serde_json::json!({ "parts": tool_call_responses }));
551    }
552    if contents.is_empty() {
553        contents.push(serde_json::json!({
554            "role": "user",
555            "parts": [{ "text": " " }]
556        }));
557    }
558    Ok(contents)
559}
560
561async fn gemini_parts_from_content(
562    _client: &Client,
563    content: &ChatMessageContent,
564    model: &str,
565) -> Result<Vec<Value>> {
566    match content {
567        ChatMessageContent::Text(text) => Ok(vec![serde_json::json!({ "text": text })]),
568        ChatMessageContent::Parts(parts) => {
569            let mut out = Vec::new();
570            for part in parts {
571                match part {
572                    ChatContentPart::Text(ChatContentPartText { text, .. }) => {
573                        if !text.is_empty() {
574                            out.push(serde_json::json!({ "text": text }));
575                        }
576                    }
577                    ChatContentPart::ImageUrl(ChatContentPartImageUrl { image_url, .. }) => {
578                        let detail = match image_url {
579                            ChatImageUrl::Object(obj) => obj.detail.as_deref(),
580                            ChatImageUrl::Url(_) => None,
581                        };
582                        out.push(process_gemini_media(image_url, detail, None, None, model)?);
583                    }
584                    ChatContentPart::InputAudio(ChatContentPartInputAudio {
585                        input_audio, ..
586                    }) => {
587                        out.push(process_gemini_audio(input_audio)?);
588                    }
589                    ChatContentPart::File(ChatContentPartFile { file, .. }) => {
590                        out.push(process_gemini_file(file, model)?);
591                    }
592                    ChatContentPart::Other(value) => {
593                        return Err(LiteLLMError::Config(format!(
594                            "unsupported gemini content part: {}",
595                            value
596                        )));
597                    }
598                }
599            }
600            Ok(out)
601        }
602    }
603}
604
605#[derive(Debug, Clone)]
606struct ToolCallInfo {
607    id: Option<String>,
608    name: String,
609}
610
611fn tool_call_parts_from_message(message: &ChatMessage) -> Result<(Vec<Value>, Vec<ToolCallInfo>)> {
612    let mut parts = Vec::new();
613    let mut infos = Vec::new();
614
615    if let Some(tool_calls) = &message.tool_calls {
616        if let Some(array) = tool_calls.as_array() {
617            for tool in array {
618                let function = tool
619                    .get("function")
620                    .ok_or_else(|| LiteLLMError::Config("tool_call missing function".into()))?;
621                let name = function
622                    .get("name")
623                    .and_then(|v| v.as_str())
624                    .ok_or_else(|| LiteLLMError::Config("tool_call missing name".into()))?;
625                let args_raw = function
626                    .get("arguments")
627                    .and_then(|v| v.as_str())
628                    .unwrap_or("");
629                let args = if args_raw.trim().is_empty() {
630                    Value::Object(serde_json::Map::new())
631                } else {
632                    serde_json::from_str(args_raw)
633                        .map_err(|e| LiteLLMError::Parse(e.to_string()))?
634                };
635                parts.push(serde_json::json!({
636                    "function_call": { "name": name, "args": args }
637                }));
638                let id = tool
639                    .get("id")
640                    .and_then(|v| v.as_str())
641                    .map(|s| s.to_string());
642                infos.push(ToolCallInfo {
643                    id,
644                    name: name.to_string(),
645                });
646            }
647            return Ok((parts, infos));
648        }
649    }
650
651    if let Some(function_call) = &message.function_call {
652        let name = function_call
653            .get("name")
654            .and_then(|v| v.as_str())
655            .ok_or_else(|| LiteLLMError::Config("function_call missing name".into()))?;
656        let args_raw = function_call
657            .get("arguments")
658            .and_then(|v| v.as_str())
659            .unwrap_or("");
660        let args = if args_raw.trim().is_empty() {
661            Value::Object(serde_json::Map::new())
662        } else {
663            serde_json::from_str(args_raw).map_err(|e| LiteLLMError::Parse(e.to_string()))?
664        };
665        parts.push(serde_json::json!({
666            "function_call": { "name": name, "args": args }
667        }));
668        infos.push(ToolCallInfo {
669            id: None,
670            name: name.to_string(),
671        });
672    }
673
674    Ok((parts, infos))
675}
676
677async fn tool_response_parts(
678    client: &Client,
679    message: &ChatMessage,
680    last_tool_calls: &[ToolCallInfo],
681) -> Result<Vec<Value>> {
682    let name = resolve_tool_name(message, last_tool_calls)?;
683    let (content_str, mut inline_parts) = extract_tool_content(client, &message.content).await?;
684    let response = parse_tool_response_data(&content_str);
685    let function_part = serde_json::json!({
686        "function_response": { "name": name, "response": response }
687    });
688    let mut parts = vec![function_part];
689    parts.append(&mut inline_parts);
690    Ok(parts)
691}
692
693fn resolve_tool_name(message: &ChatMessage, last_tool_calls: &[ToolCallInfo]) -> Result<String> {
694    if let Some(name) = &message.name {
695        return Ok(name.clone());
696    }
697    if let Some(tool_call_id) = &message.tool_call_id {
698        if let Some(info) = last_tool_calls
699            .iter()
700            .find(|info| info.id.as_deref() == Some(tool_call_id.as_str()))
701        {
702            return Ok(info.name.clone());
703        }
704    }
705    Err(LiteLLMError::Config(
706        "missing tool name for tool response".into(),
707    ))
708}
709
710async fn extract_tool_content(
711    client: &Client,
712    content: &ChatMessageContent,
713) -> Result<(String, Vec<Value>)> {
714    let mut text = String::new();
715    let mut inline_parts = Vec::new();
716    match content {
717        ChatMessageContent::Text(t) => {
718            text.push_str(t);
719        }
720        ChatMessageContent::Parts(parts) => {
721            for part in parts {
722                match part {
723                    ChatContentPart::Text(ChatContentPartText { text: t, .. }) => {
724                        text.push_str(t);
725                    }
726                    ChatContentPart::ImageUrl(ChatContentPartImageUrl { image_url, .. }) => {
727                        inline_parts.push(inline_data_from_image_url(client, image_url).await?);
728                    }
729                    ChatContentPart::InputAudio(ChatContentPartInputAudio {
730                        input_audio, ..
731                    }) => {
732                        inline_parts.push(inline_data_from_audio(input_audio)?);
733                    }
734                    ChatContentPart::File(ChatContentPartFile { file, .. }) => {
735                        inline_parts.push(inline_data_from_file(client, file).await?);
736                    }
737                    ChatContentPart::Other(_) => {}
738                }
739            }
740        }
741    }
742    Ok((text, inline_parts))
743}
744
745fn parse_tool_response_data(content: &str) -> Value {
746    let trimmed = content.trim();
747    if trimmed.starts_with('{') || trimmed.starts_with('[') {
748        if let Ok(value) = serde_json::from_str(trimmed) {
749            return value;
750        }
751    }
752    serde_json::json!({ "content": content })
753}
754
755async fn inline_data_from_image_url(client: &Client, image_url: &ChatImageUrl) -> Result<Value> {
756    let (url, format) = match image_url {
757        ChatImageUrl::Url(url) => (url.as_str(), None),
758        ChatImageUrl::Object(obj) => (obj.url.as_str(), obj.format.as_deref()),
759    };
760    inline_data_from_url(client, url, format).await
761}
762
763fn inline_data_from_audio(input_audio: &ChatInputAudio) -> Result<Value> {
764    let format = if input_audio.format.starts_with("audio/") {
765        input_audio.format.clone()
766    } else {
767        format!("audio/{}", input_audio.format)
768    };
769    Ok(serde_json::json!({
770        "inline_data": { "mime_type": format, "data": input_audio.data }
771    }))
772}
773
774async fn inline_data_from_file(client: &Client, file: &ChatFile) -> Result<Value> {
775    let passed = file
776        .file_id
777        .as_ref()
778        .or(file.file_data.as_ref())
779        .ok_or_else(|| LiteLLMError::Config("file_id or file_data required".into()))?;
780    inline_data_from_url(client, passed, file.format.as_deref()).await
781}
782
783async fn inline_data_from_url(client: &Client, url: &str, format: Option<&str>) -> Result<Value> {
784    if let Some((mime, data)) = parse_data_url(url, format) {
785        return Ok(serde_json::json!({
786            "inline_data": { "mime_type": mime, "data": data }
787        }));
788    }
789    if url.starts_with("http://") || url.starts_with("https://") {
790        let (mime, data) = fetch_bytes_with_mime(client, url, format).await?;
791        return Ok(serde_json::json!({
792            "inline_data": { "mime_type": mime, "data": data }
793        }));
794    }
795    Err(LiteLLMError::Config("unsupported inline data url".into()))
796}
797
798async fn fetch_bytes_with_mime(
799    client: &Client,
800    url: &str,
801    format: Option<&str>,
802) -> Result<(String, String)> {
803    let resp = client.get(url).send().await.map_err(LiteLLMError::from)?;
804    let headers = resp.headers().clone();
805    let bytes = resp.bytes().await.map_err(LiteLLMError::from)?;
806    let header_mime = headers
807        .get(CONTENT_TYPE)
808        .and_then(|v| v.to_str().ok())
809        .map(|v| v.split(';').next().unwrap_or(v).trim().to_string());
810    let mime = format
811        .map(|v| v.to_string())
812        .or(header_mime)
813        .or_else(|| mime_type_from_url(url))
814        .unwrap_or_else(|| "application/octet-stream".to_string());
815    let data = general_purpose::STANDARD.encode(bytes);
816    Ok((mime, data))
817}
818
819fn process_gemini_audio(input_audio: &ChatInputAudio) -> Result<Value> {
820    let format = if input_audio.format.starts_with("audio/") {
821        input_audio.format.clone()
822    } else {
823        format!("audio/{}", input_audio.format)
824    };
825    Ok(serde_json::json!({
826        "inline_data": {
827            "mime_type": format,
828            "data": input_audio.data
829        }
830    }))
831}
832
833fn process_gemini_file(file: &ChatFile, model: &str) -> Result<Value> {
834    let passed = file
835        .file_id
836        .as_ref()
837        .or(file.file_data.as_ref())
838        .ok_or_else(|| LiteLLMError::Config("file_id or file_data required".into()))?;
839    process_gemini_media_url(
840        passed,
841        file.format.as_deref(),
842        file.detail.as_deref(),
843        file.video_metadata.as_ref(),
844        model,
845    )
846}
847
848fn process_gemini_media(
849    image_url: &ChatImageUrl,
850    detail: Option<&str>,
851    video_metadata: Option<&Value>,
852    format: Option<&str>,
853    model: &str,
854) -> Result<Value> {
855    let (url, format) = match image_url {
856        ChatImageUrl::Url(url) => (url.as_str(), format),
857        ChatImageUrl::Object(obj) => (obj.url.as_str(), obj.format.as_deref().or(format)),
858    };
859    process_gemini_media_url(url, format, detail, video_metadata, model)
860}
861
862fn process_gemini_media_url(
863    url: &str,
864    format: Option<&str>,
865    detail: Option<&str>,
866    video_metadata: Option<&Value>,
867    _model: &str,
868) -> Result<Value> {
869    if url.starts_with("gs://") || url.starts_with("https://") || url.starts_with("http://") {
870        let mime_type = format
871            .map(|v| v.to_string())
872            .or_else(|| mime_type_from_url(url))
873            .ok_or_else(|| LiteLLMError::Config("missing media mime type".into()))?;
874        let mut part = serde_json::json!({
875            "file_data": { "mime_type": mime_type, "file_uri": url }
876        });
877        apply_gemini_metadata(&mut part, detail, video_metadata);
878        return Ok(part);
879    }
880
881    if let Some((media_type, data)) = parse_data_url(url, format) {
882        let mut part = serde_json::json!({
883            "inline_data": { "mime_type": media_type, "data": data }
884        });
885        apply_gemini_metadata(&mut part, detail, video_metadata);
886        return Ok(part);
887    }
888
889    Err(LiteLLMError::Config("unsupported gemini media url".into()))
890}
891
892fn apply_gemini_metadata(part: &mut Value, detail: Option<&str>, video_metadata: Option<&Value>) {
893    if let Some(detail) = detail {
894        if let Some(level) = detail_to_media_resolution(detail) {
895            if let Some(obj) = part.as_object_mut() {
896                obj.insert(
897                    "media_resolution".to_string(),
898                    serde_json::json!({ "level": level }),
899                );
900            }
901        }
902    }
903    if let Some(video_metadata) = video_metadata {
904        if let Some(obj) = part.as_object_mut() {
905            obj.insert("video_metadata".to_string(), video_metadata.clone());
906        }
907    }
908}
909
910fn detail_to_media_resolution(detail: &str) -> Option<&'static str> {
911    match detail {
912        "low" => Some("MEDIA_RESOLUTION_LOW"),
913        "medium" => Some("MEDIA_RESOLUTION_MEDIUM"),
914        "high" => Some("MEDIA_RESOLUTION_HIGH"),
915        "ultra_high" => Some("MEDIA_RESOLUTION_ULTRA_HIGH"),
916        _ => None,
917    }
918}
919
920fn parse_data_url(url: &str, override_format: Option<&str>) -> Option<(String, String)> {
921    if url.starts_with("data:") {
922        let stripped = url.strip_prefix("data:").unwrap_or(url);
923        if let Some((meta, data)) = stripped.split_once(',') {
924            let mut media_type = meta.split(';').next().unwrap_or("application/octet-stream");
925            if let Some(fmt) = override_format {
926                media_type = fmt;
927            }
928            return Some((media_type.to_string(), data.to_string()));
929        }
930    }
931    None
932}
933
934fn mime_type_from_url(url: &str) -> Option<String> {
935    let path = url.split('?').next().unwrap_or(url);
936    MimeGuess::from_path(path)
937        .first_raw()
938        .map(|m| m.to_string())
939}
940
941fn extract_video_uri(resp: &Value) -> Option<String> {
942    let response = resp.get("response")?;
943    if let Some(uri) = response
944        .pointer("/generateVideoResponse/generatedSamples/0/video/uri")
945        .and_then(|v| v.as_str())
946    {
947        return Some(uri.to_string());
948    }
949    if let Some(uri) = response
950        .pointer("/generatedVideos/0/uri")
951        .and_then(|v| v.as_str())
952    {
953        return Some(uri.to_string());
954    }
955    if let Some(uri) = response.pointer("/videos/0/uri").and_then(|v| v.as_str()) {
956        return Some(uri.to_string());
957    }
958    None
959}