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