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 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
104fn 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#[derive(Debug, Clone)]
143pub struct VideoGenerationOptions {
144 pub max_poll_attempts: u32,
146 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 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 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}