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
19pub const DEFAULT_VIDEO_MAX_POLL_ATTEMPTS: u32 = 240;
21pub 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 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#[derive(Debug, Clone)]
104pub struct VideoGenerationOptions {
105 pub max_poll_attempts: u32,
107 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 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 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}