1use super::wire::{GenerationConfig, InlineData, StreamingError, ThinkingConfig};
2use super::*;
3use crate::config::constants::models;
4use crate::llm::error_display;
5use crate::llm::provider::LLMError;
6use crate::llm::provider::{ContentPart, MessageContent, ToolDefinition};
7use crate::llm::providers::common::{
8 collect_history_system_directives, merge_system_prompt_with_history_directives,
9};
10use crate::prompts::system::default_system_prompt;
11use serde_json::Map;
12use std::collections::BTreeMap;
13
14const GEMINI_PRESERVED_PARTS_PREFIX: &str = "__vtcode_gemini_parts__:";
15
16struct GeminiToolSpec {
17 generate_tools: Option<Vec<Tool>>,
18 interaction_tools: Option<Vec<InteractionTool>>,
19 uses_server_side_tools: bool,
20 has_function_tools: bool,
21}
22
23#[derive(Debug, Clone, Default)]
24pub(super) struct InteractionStreamOutputBuilder {
25 pub output_type: String,
26 pub text: String,
27 pub summary: String,
28 pub id: Option<String>,
29 pub name: Option<String>,
30 pub arguments: Option<Value>,
31 pub signature: Option<String>,
32}
33
34impl InteractionStreamOutputBuilder {
35 fn into_output(self) -> InteractionOutput {
36 InteractionOutput {
37 output_type: self.output_type,
38 text: (!self.text.is_empty()).then_some(self.text),
39 id: self.id,
40 name: self.name,
41 arguments: self.arguments,
42 signature: self.signature,
43 function_call: None,
44 summary: (!self.summary.is_empty()).then_some(self.summary),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Default)]
50pub(super) struct InteractionStreamState {
51 pub interaction_id: Option<String>,
52 pub status: Option<String>,
53 pub outputs: BTreeMap<usize, InteractionStreamOutputBuilder>,
54 pub usage: Option<wire::interactions::InteractionUsage>,
55 pub completed: bool,
56}
57
58impl GeminiProvider {
59 const HISTORY_DIRECTIVES_SECTION_HEADER: &str = "[History Directives]";
60
61 pub(super) fn is_gemini_3_pro_model(model: &str) -> bool {
62 model.contains("gemini-3") && model.contains("pro") && !model.contains("flash")
63 }
64
65 pub fn supports_caching(model: &str) -> bool {
67 models::google::CACHING_MODELS.contains(&model)
68 }
69
70 pub fn supports_code_execution(model: &str) -> bool {
72 models::google::CODE_EXECUTION_MODELS.contains(&model)
73 }
74
75 pub fn max_input_tokens(model: &str) -> usize {
77 if model.contains("gemini-3.1") {
78 1_048_576 } else if model.contains("3") || model.contains("1.5-pro") {
80 2_097_152 } else {
82 1_048_576 }
84 }
85
86 pub fn max_output_tokens(model: &str) -> usize {
88 if model.contains("3") {
89 65_536 } else {
91 8_192 }
93 }
94
95 pub fn supports_extended_thinking(model: &str) -> bool {
98 model.contains("gemini-3-flash")
99 }
100
101 pub fn supported_thinking_levels(model: &str) -> Vec<&'static str> {
104 if model.contains("gemini-3-flash") {
105 vec!["minimal", "low", "medium", "high"]
107 } else if model.contains("gemini-3") {
108 vec!["low", "high"]
110 } else {
111 vec!["low", "high"]
113 }
114 }
115 pub(super) fn apply_stream_delta(accumulator: &mut String, chunk: &str) -> Option<String> {
116 if chunk.is_empty() {
117 return None;
118 }
119
120 if chunk.starts_with(accumulator.as_str()) {
121 let delta = &chunk[accumulator.len()..];
122 if delta.is_empty() {
123 return None;
124 }
125 accumulator.clear();
126 accumulator.push_str(chunk);
127 return Some(delta.to_string());
128 }
129
130 if accumulator.starts_with(chunk) {
131 accumulator.clear();
132 accumulator.push_str(chunk);
133 return None;
134 }
135
136 accumulator.push_str(chunk);
137 Some(chunk.to_string())
138 }
139
140 pub(super) fn convert_to_gemini_request(
141 &self,
142 request: &LLMRequest,
143 ) -> Result<GenerateContentRequest, LLMError> {
144 if self.prompt_cache_enabled
145 && matches!(
146 self.prompt_cache_settings.mode,
147 GeminiPromptCacheMode::Explicit
148 )
149 {
150 }
154
155 let mut call_map: HashMap<String, String> = HashMap::with_capacity(request.messages.len());
156 for message in &request.messages {
157 if message.role == MessageRole::Assistant
158 && let Some(tool_calls) = &message.tool_calls
159 {
160 for tool_call in tool_calls {
161 if let Some(ref func) = tool_call.function {
162 call_map.insert(tool_call.id.clone(), func.name.clone());
163 }
164 }
165 }
166 }
167
168 let mut contents: Vec<Content> = Vec::with_capacity(request.messages.len());
169 let history_system_directives = collect_history_system_directives(request);
170 for message in &request.messages {
171 if message.role == MessageRole::System {
172 continue;
173 }
174
175 let mut parts: Vec<Part> = preserved_gemini_parts_from_message(message)
176 .unwrap_or_else(|| build_message_parts(message, request.model.as_str()));
177
178 if message.role == MessageRole::Tool {
179 if let Some(tool_call_id) = &message.tool_call_id {
180 let func_name = call_map
181 .get(tool_call_id)
182 .cloned()
183 .unwrap_or_else(|| tool_call_id.clone());
184 let response_text = serde_json::from_str::<Value>(&message.content.as_text())
185 .map(|value| {
186 serde_json::to_string_pretty(&value)
187 .unwrap_or_else(|_| message.content.as_text().into_owned())
188 })
189 .unwrap_or_else(|_| message.content.as_text().into_owned());
190
191 let response_payload = json!({
192 "name": func_name.clone(),
193 "content": [{
194 "text": response_text
195 }]
196 });
197
198 parts.push(Part::FunctionResponse {
199 function_response: FunctionResponse {
200 name: func_name,
201 response: response_payload,
202 id: Some(tool_call_id.clone()),
203 },
204 thought_signature: None, });
206 } else if !message.content.is_empty() {
207 parts.push(Part::Text {
208 text: message.content.as_text().into_owned(),
209 thought_signature: None,
210 });
211 }
212 }
213
214 if !parts.is_empty() {
215 contents.push(Content {
216 role: message.role.as_gemini_str().to_string(),
217 parts,
218 });
219 }
220 }
221
222 let tool_spec = collect_gemini_tool_spec(request.tools.as_deref().map(|v| v.as_slice()));
223 let tools = tool_spec.generate_tools;
224 let uses_server_side_tools = tool_spec.uses_server_side_tools;
225
226 let generation_config = build_generation_config(self, request);
227
228 if let Some(temp) = request.temperature {
230 if Self::is_gemini_3_pro_model(&request.model) && temp < 1.0 {
231 tracing::warn!(
232 "When using Gemini 3 Pro with temperature values below 1.0, be aware that this may cause looping or degraded performance on complex tasks. Consider using 1.0 or higher for optimal results."
233 );
234 }
235 }
236
237 let has_tools = request
238 .tools
239 .as_ref()
240 .map(|defs| !defs.is_empty())
241 .unwrap_or(false);
242 let has_function_tools = tool_spec.has_function_tools;
243 let tool_config = if has_tools || request.tool_choice.is_some() {
244 let function_calling_config = if has_function_tools {
245 Some(match request.tool_choice.as_ref() {
246 Some(ToolChoice::None) => FunctionCallingConfig::none(),
247 Some(ToolChoice::Any) => FunctionCallingConfig::any(),
248 Some(ToolChoice::Specific(spec)) => {
249 let mut config = if uses_server_side_tools {
250 FunctionCallingConfig::validated()
251 } else {
252 FunctionCallingConfig::any()
253 };
254 if spec.tool_type == "function" {
255 config.allowed_function_names = Some(vec![spec.function.name.clone()]);
256 }
257 config
258 }
259 _ => {
260 if uses_server_side_tools {
261 FunctionCallingConfig::validated()
262 } else {
263 FunctionCallingConfig::auto()
264 }
265 }
266 })
267 } else {
268 None
269 };
270
271 Some(ToolConfig {
272 function_calling_config,
273 include_server_side_tool_invocations: uses_server_side_tools.then_some(true),
274 })
275 } else {
276 None
277 };
278
279 Ok(GenerateContentRequest {
280 contents,
281 tools,
282 tool_config,
283 system_instruction: {
284 let base_system_prompt = request
285 .system_prompt
286 .as_ref()
287 .map(|prompt| prompt.as_str())
288 .or_else(|| self.prompt_cache_enabled.then_some(default_system_prompt()));
289 let merged_system_prompt = merge_system_prompt_with_history_directives(
290 base_system_prompt,
291 &history_system_directives,
292 Self::HISTORY_DIRECTIVES_SECTION_HEADER,
293 );
294
295 if self.prompt_cache_enabled
296 && matches!(
297 self.prompt_cache_settings.mode,
298 GeminiPromptCacheMode::Explicit
299 )
300 {
301 if let Some(ttl) = self.prompt_cache_settings.explicit_ttl_seconds {
302 merged_system_prompt.map(|text| SystemInstruction::with_ttl(text, ttl))
303 } else {
304 merged_system_prompt.map(SystemInstruction::new)
305 }
306 } else if request.system_prompt.is_some()
307 || self.prompt_cache_enabled
308 || !history_system_directives.is_empty()
309 {
310 merged_system_prompt.map(SystemInstruction::new)
311 } else {
312 None
313 }
314 },
315 generation_config: Some(generation_config.into()),
316 })
317 }
318
319 pub(super) fn should_use_interactions(&self, request: &LLMRequest) -> bool {
320 if request.previous_response_id.is_some() {
321 return true;
322 }
323
324 request.model.contains("gemini-3")
325 && collect_gemini_tool_spec(request.tools.as_deref().map(|v| v.as_slice()))
326 .uses_server_side_tools
327 }
328
329 pub(super) fn convert_to_interaction_request(
330 &self,
331 request: &LLMRequest,
332 ) -> Result<InteractionRequest, LLMError> {
333 let history_system_directives = collect_history_system_directives(request);
334 let base_system_prompt = request
335 .system_prompt
336 .as_ref()
337 .map(|prompt| prompt.as_str())
338 .or_else(|| self.prompt_cache_enabled.then_some(default_system_prompt()));
339 let merged_system_prompt = merge_system_prompt_with_history_directives(
340 base_system_prompt,
341 &history_system_directives,
342 Self::HISTORY_DIRECTIVES_SECTION_HEADER,
343 );
344
345 let tool_spec = collect_gemini_tool_spec(request.tools.as_deref().map(|v| v.as_slice()));
346 let generation_config = build_generation_config(self, request);
347 let interaction_input = build_interaction_input(request)?;
348
349 Ok(InteractionRequest {
350 model: request.model.clone(),
351 input: interaction_input,
352 tools: tool_spec.interaction_tools,
353 system_instruction: merged_system_prompt,
354 response_format: request.output_format.clone(),
355 response_mime_type: request
356 .output_format
357 .as_ref()
358 .map(|_| "application/json".to_string()),
359 stream: request.stream.then_some(true),
360 store: request.response_store,
361 generation_config: Some(generation_config.into()),
362 tool_choice: build_interaction_tool_choice(
363 request.tool_choice.as_ref(),
364 tool_spec.has_function_tools,
365 tool_spec.uses_server_side_tools,
366 ),
367 previous_interaction_id: request.previous_response_id.clone(),
368 })
369 }
370
371 pub(super) fn convert_from_gemini_response(
372 response: GenerateContentResponse,
373 model: String,
374 ) -> Result<LLMResponse, LLMError> {
375 let mut candidates = response.candidates.into_iter();
376 let candidate = candidates.next().ok_or_else(|| {
377 let formatted_error =
378 error_display::format_llm_error("Gemini", "No candidate in response");
379 LLMError::Provider {
380 message: formatted_error,
381 metadata: None,
382 }
383 })?;
384
385 if candidate.content.parts.is_empty() {
386 return Ok(LLMResponse {
387 content: Some(String::new()),
388 tool_calls: None,
389 model,
390 usage: None,
391 finish_reason: FinishReason::Stop,
392 reasoning: None,
393 reasoning_details: None,
394 tool_references: Vec::new(),
395 request_id: None,
396 organization_id: None,
397 compaction: None,
398 });
399 }
400
401 let raw_parts = candidate.content.parts.clone();
402 let mut text_content = String::new();
403 let mut tool_calls = Vec::new();
404 let mut last_text_thought_signature: Option<String> = None;
408
409 for part in candidate.content.parts {
410 match part {
411 Part::Text {
412 text,
413 thought_signature,
414 } => {
415 text_content.push_str(&text);
416 if thought_signature.is_some() {
417 last_text_thought_signature = thought_signature;
418 }
419 }
420 Part::InlineData { .. } => {}
421 Part::FunctionCall {
422 function_call,
423 thought_signature,
424 } => {
425 let call_id = function_call
426 .id
427 .clone()
428 .unwrap_or_else(|| format!("call_{}", tool_calls.len()));
429
430 let effective_signature =
432 thought_signature.or(last_text_thought_signature.clone());
433
434 tool_calls.push(ToolCall {
435 id: call_id,
436 call_type: "function".to_string(),
437 function: Some(FunctionCall {
438 namespace: None,
439 name: function_call.name,
440 arguments: serde_json::to_string(&function_call.args)
441 .unwrap_or_else(|_| "{}".to_string()),
442 }),
443 text: None,
444 thought_signature: effective_signature,
445 });
446 }
447 Part::FunctionResponse { .. } => {}
448 Part::ToolCall { .. } => {}
449 Part::ToolResponse { .. } => {}
450 Part::ExecutableCode { .. } => {}
451 Part::CodeExecutionResult { .. } => {}
452 Part::CacheControl { .. } => {}
453 }
454 }
455
456 let finish_reason = match candidate.finish_reason.as_deref() {
457 Some("STOP") => FinishReason::Stop,
458 Some("MAX_TOKENS") => FinishReason::Length,
459 Some("SAFETY") => FinishReason::ContentFilter,
460 Some("FUNCTION_CALL") => FinishReason::ToolCalls,
461 Some(other) => FinishReason::Error(other.to_string()),
462 None => FinishReason::Stop,
463 };
464
465 let (cleaned_content, extracted_reasoning) = if !text_content.is_empty() {
466 let (reasoning_segments, cleaned) =
467 crate::llm::providers::split_reasoning_from_text(&text_content);
468 let final_reasoning = if reasoning_segments.is_empty() {
469 None
470 } else {
471 let combined_reasoning: Vec<String> =
472 reasoning_segments.into_iter().map(|s| s.text).collect();
473 let combined_reasoning = combined_reasoning.join("\n");
474 if combined_reasoning.trim().is_empty() {
475 None
476 } else {
477 Some(combined_reasoning)
478 }
479 };
480 let final_content = cleaned.unwrap_or_else(|| text_content.clone());
481 (
482 if final_content.trim().is_empty() {
483 None
484 } else {
485 Some(final_content)
486 },
487 final_reasoning,
488 )
489 } else {
490 (None, None)
491 };
492
493 Ok(LLMResponse {
494 content: cleaned_content,
495 tool_calls: if tool_calls.is_empty() {
496 None
497 } else {
498 Some(tool_calls)
499 },
500 model,
501 usage: None,
502 finish_reason,
503 reasoning: extracted_reasoning,
504 reasoning_details: preserved_gemini_parts_detail(&raw_parts),
505 tool_references: Vec::new(),
506 request_id: None,
507 organization_id: None,
508 compaction: None,
509 })
510 }
511
512 pub(super) fn convert_from_interaction_response(
513 response: Interaction,
514 model: String,
515 ) -> Result<LLMResponse, LLMError> {
516 let mut text_content = String::new();
517 let mut tool_calls = Vec::new();
518 let mut thought_summaries = Vec::new();
519 let mut thought_details = Vec::new();
520
521 for output in response.outputs {
522 match output.output_type.as_str() {
523 "text" => {
524 if let Some(text) = output.text {
525 text_content.push_str(&text);
526 }
527 }
528 "thought" => {
529 let summary = output.summary.or(output.text).unwrap_or_default();
530 if !summary.trim().is_empty() {
531 thought_summaries.push(summary.clone());
532 }
533 thought_details.push(
534 json!({
535 "type": "thought",
536 "signature": output.signature,
537 "summary": summary,
538 })
539 .to_string(),
540 );
541 }
542 "function_call" => {
543 let (name, arguments, id, signature) =
544 if let Some(function_call) = output.function_call {
545 (
546 function_call.name,
547 function_call.arguments,
548 function_call.id.or(output.id),
549 function_call.signature.or(output.signature),
550 )
551 } else {
552 (
553 output.name.unwrap_or_default(),
554 output.arguments.unwrap_or(Value::Null),
555 output.id,
556 output.signature,
557 )
558 };
559
560 let call_id = id.unwrap_or_else(|| format!("call_{}", tool_calls.len()));
561
562 tool_calls.push(ToolCall {
563 id: call_id,
564 call_type: "function".to_string(),
565 function: Some(FunctionCall {
566 namespace: None,
567 name,
568 arguments: serde_json::to_string(&arguments)
569 .unwrap_or_else(|_| "{}".to_string()),
570 }),
571 text: None,
572 thought_signature: signature,
573 });
574 }
575 _ => {}
576 }
577 }
578
579 let finish_reason = if tool_calls.is_empty() {
580 FinishReason::Stop
581 } else {
582 FinishReason::ToolCalls
583 };
584 let (reasoning_segments, cleaned) =
585 crate::llm::providers::split_reasoning_from_text(&text_content);
586 let extracted_reasoning = if reasoning_segments.is_empty() {
587 None
588 } else {
589 Some(
590 reasoning_segments
591 .into_iter()
592 .map(|segment| segment.text)
593 .collect::<Vec<_>>()
594 .join("\n"),
595 )
596 .filter(|value| !value.trim().is_empty())
597 };
598 let content = cleaned
599 .or_else(|| (!text_content.trim().is_empty()).then_some(text_content))
600 .filter(|value| !value.trim().is_empty());
601 let reasoning = if thought_summaries.is_empty() {
602 extracted_reasoning
603 } else {
604 Some(thought_summaries.join("\n"))
605 };
606 let reasoning_details = if thought_details.is_empty() {
607 None
608 } else {
609 Some(thought_details)
610 };
611
612 Ok(LLMResponse {
613 content,
614 tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
615 model,
616 usage: response.usage.map(|usage| vtcode_commons::llm::Usage {
617 prompt_tokens: usage.total_input_tokens.unwrap_or_default(),
618 completion_tokens: usage.total_output_tokens.unwrap_or_default(),
619 total_tokens: usage.total_tokens.unwrap_or_default(),
620 cached_prompt_tokens: usage.total_cached_tokens,
621 cache_creation_tokens: None,
622 cache_read_tokens: usage.total_cached_tokens,
623 }),
624 finish_reason,
625 reasoning,
626 reasoning_details,
627 tool_references: Vec::new(),
628 request_id: Some(response.id),
629 organization_id: None,
630 compaction: None,
631 })
632 }
633
634 pub(super) fn apply_interaction_stream_payload(
635 state: &mut InteractionStreamState,
636 payload: &Value,
637 ) -> Result<Vec<LLMStreamEvent>, LLMError> {
638 let mut events = Vec::new();
639 let Some(event_type) = payload.get("event_type").and_then(Value::as_str) else {
640 return Ok(events);
641 };
642
643 match event_type {
644 "interaction.start" | "interaction.status_update" | "interaction.complete" => {
645 let interaction = interaction_object(payload);
646 if let Some(id) = interaction.get("id").and_then(Value::as_str) {
647 state.interaction_id = Some(id.to_string());
648 }
649 if let Some(status) = interaction.get("status").and_then(Value::as_str) {
650 state.status = Some(status.to_string());
651 }
652 if let Some(usage) = interaction.get("usage")
653 && let Ok(usage) = serde_json::from_value(usage.clone())
654 {
655 state.usage = Some(usage);
656 }
657 if event_type == "interaction.complete" {
658 state.completed = true;
659 }
660 }
661 "content.start" => {
662 let index = payload
663 .get("index")
664 .and_then(Value::as_u64)
665 .unwrap_or_default() as usize;
666 let builder = state.outputs.entry(index).or_default();
667 if let Some(output_type) = payload
668 .get("content")
669 .and_then(Value::as_object)
670 .and_then(|content| content.get("type"))
671 .and_then(Value::as_str)
672 {
673 builder.output_type = output_type.to_string();
674 }
675 }
676 "content.delta" => {
677 let index = payload
678 .get("index")
679 .and_then(Value::as_u64)
680 .unwrap_or_default() as usize;
681 let Some(delta) = payload.get("delta").and_then(Value::as_object) else {
682 return Ok(events);
683 };
684 let builder = state.outputs.entry(index).or_default();
685 apply_interaction_delta(builder, delta, &mut events);
686 }
687 "content.stop" => {}
688 "error" => {
689 let error_message = payload
690 .get("error")
691 .and_then(Value::as_object)
692 .and_then(|error| error.get("message"))
693 .and_then(Value::as_str)
694 .unwrap_or("Unknown Gemini interactions streaming error");
695 let formatted = error_display::format_llm_error("Gemini", error_message);
696 return Err(LLMError::Provider {
697 message: formatted,
698 metadata: None,
699 });
700 }
701 _ => {}
702 }
703
704 Ok(events)
705 }
706
707 pub(super) fn finalize_interaction_stream_state(
708 state: InteractionStreamState,
709 model: String,
710 ) -> Result<LLMResponse, LLMError> {
711 let interaction = Interaction {
712 id: state
713 .interaction_id
714 .unwrap_or_else(|| "interaction_stream".to_string()),
715 model: model.clone(),
716 status: state.status,
717 outputs: state
718 .outputs
719 .into_values()
720 .map(InteractionStreamOutputBuilder::into_output)
721 .collect(),
722 usage: state.usage,
723 };
724
725 Self::convert_from_interaction_response(interaction, model)
726 }
727
728 pub(super) fn convert_from_streaming_response(
729 response: StreamingResponse,
730 model: String,
731 ) -> Result<LLMResponse, LLMError> {
732 let converted_candidates: Vec<Candidate> = response
733 .candidates
734 .into_iter()
735 .map(|candidate| Candidate {
736 content: candidate.content,
737 finish_reason: candidate.finish_reason,
738 })
739 .collect();
740
741 let converted = GenerateContentResponse {
742 candidates: converted_candidates,
743 prompt_feedback: None,
744 usage_metadata: response.usage_metadata,
745 };
746
747 Self::convert_from_gemini_response(converted, model)
748 }
749
750 #[cold]
751 pub(super) fn map_streaming_error(error: StreamingError) -> LLMError {
752 match error {
753 StreamingError::NetworkError { message, .. } => {
754 let formatted = error_display::format_llm_error(
755 "Gemini",
756 &format!("Network error: {}", message),
757 );
758 LLMError::Network {
759 message: formatted,
760 metadata: None,
761 }
762 }
763 StreamingError::ApiError {
764 status_code,
765 message,
766 ..
767 } => {
768 if status_code == 401 || status_code == 403 {
769 let formatted = error_display::format_llm_error(
770 "Gemini",
771 &format!("HTTP {}: {}", status_code, message),
772 );
773 LLMError::Authentication {
774 message: formatted,
775 metadata: None,
776 }
777 } else if status_code == 429 {
778 LLMError::RateLimit { metadata: None }
779 } else {
780 let formatted = error_display::format_llm_error(
781 "Gemini",
782 &format!("API error ({}): {}", status_code, message),
783 );
784 LLMError::Provider {
785 message: formatted,
786 metadata: None,
787 }
788 }
789 }
790 StreamingError::ParseError { message, .. } => {
791 let formatted =
792 error_display::format_llm_error("Gemini", &format!("Parse error: {}", message));
793 LLMError::Provider {
794 message: formatted,
795 metadata: None,
796 }
797 }
798 StreamingError::TimeoutError {
799 operation,
800 duration,
801 } => {
802 let formatted = error_display::format_llm_error(
803 "Gemini",
804 &format!(
805 "Streaming timeout during {} after {:?}",
806 operation, duration
807 ),
808 );
809 LLMError::Network {
810 message: formatted,
811 metadata: None,
812 }
813 }
814 StreamingError::ContentError { message } => {
815 let formatted = error_display::format_llm_error(
816 "Gemini",
817 &format!("Content error: {}", message),
818 );
819 LLMError::Provider {
820 message: formatted,
821 metadata: None,
822 }
823 }
824 StreamingError::StreamingError { message, .. } => {
825 let formatted = error_display::format_llm_error(
826 "Gemini",
827 &format!("Streaming error: {}", message),
828 );
829 LLMError::Provider {
830 message: formatted,
831 metadata: None,
832 }
833 }
834 }
835 }
836}
837
838fn parts_from_message_content(content: &MessageContent) -> Vec<Part> {
839 match content {
840 MessageContent::Text(text) => {
841 if text.is_empty() {
842 Vec::new()
843 } else {
844 vec![Part::Text {
845 text: text.clone(),
846 thought_signature: None,
847 }]
848 }
849 }
850 MessageContent::Parts(parts) => {
851 let mut converted = Vec::new();
852 for part in parts {
853 match part {
854 ContentPart::Text { text } => {
855 if !text.is_empty() {
856 converted.push(Part::Text {
857 text: text.clone(),
858 thought_signature: None,
859 });
860 }
861 }
862 ContentPart::Image {
863 data, mime_type, ..
864 } => {
865 converted.push(Part::InlineData {
866 inline_data: InlineData {
867 mime_type: mime_type.clone(),
868 data: data.clone(),
869 },
870 });
871 }
872 ContentPart::File {
873 filename,
874 file_id,
875 file_url,
876 ..
877 } => {
878 let fallback = filename
879 .clone()
880 .or_else(|| file_id.clone())
881 .or_else(|| file_url.clone())
882 .unwrap_or_else(|| "attached file".to_string());
883 converted.push(Part::Text {
884 text: format!("[File input not directly supported: {}]", fallback),
885 thought_signature: None,
886 });
887 }
888 }
889 }
890 converted
891 }
892 }
893}
894
895fn build_interaction_content(content: &MessageContent) -> Vec<InteractionContent> {
896 match content {
897 MessageContent::Text(text) => {
898 if text.is_empty() {
899 Vec::new()
900 } else {
901 vec![InteractionContent::Text { text: text.clone() }]
902 }
903 }
904 MessageContent::Parts(parts) => {
905 let mut converted = Vec::new();
906 for part in parts {
907 match part {
908 ContentPart::Text { text } => {
909 if !text.is_empty() {
910 converted.push(InteractionContent::Text { text: text.clone() });
911 }
912 }
913 ContentPart::Image {
914 data, mime_type, ..
915 } => converted.push(InteractionContent::Image {
916 data: data.clone(),
917 mime_type: mime_type.clone(),
918 }),
919 ContentPart::File {
920 filename,
921 file_id,
922 file_url,
923 ..
924 } => {
925 let fallback = filename
926 .clone()
927 .or_else(|| file_id.clone())
928 .or_else(|| file_url.clone())
929 .unwrap_or_else(|| "attached file".to_string());
930 converted.push(InteractionContent::Text {
931 text: {
932 let mut s = String::with_capacity(38 + fallback.len());
933 s.push_str("[File input not directly supported: ");
934 s.push_str(&fallback);
935 s.push(']');
936 s
937 },
938 });
939 }
940 }
941 }
942 converted
943 }
944 }
945}
946
947fn build_message_parts(message: &Message, model: &str) -> Vec<Part> {
948 let mut parts = Vec::new();
949 if message.role != MessageRole::Tool {
950 parts.extend(parts_from_message_content(&message.content));
951 }
952
953 if message.role == MessageRole::Assistant
954 && let Some(tool_calls) = &message.tool_calls
955 {
956 let is_gemini3 = model.contains("gemini-3");
957 for tool_call in tool_calls {
958 if let Some(ref func) = tool_call.function {
959 let parsed_args = tool_call.parsed_arguments().unwrap_or_else(|_| json!({}));
960
961 let thought_signature = if is_gemini3 && tool_call.thought_signature.is_none() {
962 tracing::trace!(
963 function_name = %func.name,
964 "Gemini 3: using skip_thought_signature_validator fallback"
965 );
966 Some("skip_thought_signature_validator".to_string())
967 } else {
968 tool_call.thought_signature.clone()
969 };
970
971 parts.push(Part::FunctionCall {
972 function_call: GeminiFunctionCall {
973 name: func.name.clone(),
974 args: parsed_args,
975 id: Some(tool_call.id.clone()),
976 },
977 thought_signature,
978 });
979 }
980 }
981 }
982
983 parts
984}
985
986fn preserved_gemini_parts_from_message(message: &Message) -> Option<Vec<Part>> {
987 let details = message.reasoning_details.as_ref()?;
988 for detail in details {
989 let Some(text) = detail.as_str() else {
990 continue;
991 };
992 let Some(payload) = text.strip_prefix(GEMINI_PRESERVED_PARTS_PREFIX) else {
993 continue;
994 };
995 if let Ok(parts) = serde_json::from_str::<Vec<Part>>(payload) {
996 return Some(parts);
997 }
998 }
999 None
1000}
1001
1002fn preserved_gemini_parts_detail(parts: &[Part]) -> Option<Vec<String>> {
1003 if !parts_require_roundtrip_history(parts) {
1004 return None;
1005 }
1006
1007 serde_json::to_string(parts)
1008 .ok()
1009 .map(|serialized| vec![format!("{GEMINI_PRESERVED_PARTS_PREFIX}{serialized}")])
1010}
1011
1012fn parts_require_roundtrip_history(parts: &[Part]) -> bool {
1013 parts.iter().any(|part| {
1014 part.thought_signature().is_some()
1015 || matches!(
1016 part,
1017 Part::ToolCall { .. }
1018 | Part::ToolResponse { .. }
1019 | Part::ExecutableCode { .. }
1020 | Part::CodeExecutionResult { .. }
1021 | Part::FunctionResponse { .. }
1022 | Part::InlineData { .. }
1023 )
1024 })
1025}
1026
1027fn gemini_built_in_tool(tool: &ToolDefinition) -> Option<Tool> {
1028 match tool.tool_type.as_str() {
1029 "web_search" | "google_search" => Some(Tool {
1030 google_search: Some(tool.web_search.clone().unwrap_or_else(|| json!({}))),
1031 ..Tool::default()
1032 }),
1033 "google_maps" => Some(Tool {
1034 google_maps: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1035 ..Tool::default()
1036 }),
1037 "url_context" => Some(Tool {
1038 url_context: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1039 ..Tool::default()
1040 }),
1041 "file_search" => Some(Tool {
1042 file_search: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1043 ..Tool::default()
1044 }),
1045 "code_execution" => Some(Tool {
1046 code_execution: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1047 ..Tool::default()
1048 }),
1049 other if other.starts_with("code_execution_") => Some(Tool {
1050 code_execution: Some(json!({})),
1051 ..Tool::default()
1052 }),
1053 _ => None,
1054 }
1055}
1056
1057fn gemini_interaction_built_in_tool(tool: &ToolDefinition) -> Option<InteractionTool> {
1058 let (tool_type, config) = match tool.tool_type.as_str() {
1059 "web_search" | "google_search" => ("google_search", tool.web_search.as_ref()),
1060 "google_maps" => ("google_maps", tool.hosted_tool_config.as_ref()),
1061 "url_context" => ("url_context", tool.hosted_tool_config.as_ref()),
1062 "file_search" => ("file_search", tool.hosted_tool_config.as_ref()),
1063 "code_execution" => ("code_execution", tool.hosted_tool_config.as_ref()),
1064 other if other.starts_with("code_execution_") => ("code_execution", None),
1065 _ => return None,
1066 };
1067
1068 Some(InteractionTool::built_in(tool_type, config))
1069}
1070
1071fn collect_gemini_tool_spec(definitions: Option<&[ToolDefinition]>) -> GeminiToolSpec {
1072 let Some(definitions) = definitions else {
1073 return GeminiToolSpec {
1074 generate_tools: None,
1075 interaction_tools: None,
1076 uses_server_side_tools: false,
1077 has_function_tools: false,
1078 };
1079 };
1080
1081 let mut generate_tools = Vec::new();
1082 let mut interaction_tools = Vec::new();
1083 let mut function_declarations = Vec::new();
1084 let mut seen = hashbrown::HashSet::new();
1085 let mut uses_server_side_tools = false;
1086 let mut has_function_tools = false;
1087
1088 for tool in definitions {
1089 if let Some(built_in_tool) = gemini_built_in_tool(tool) {
1090 uses_server_side_tools = true;
1091 generate_tools.push(built_in_tool);
1092 }
1093 if let Some(interaction_tool) = gemini_interaction_built_in_tool(tool) {
1094 interaction_tools.push(interaction_tool);
1095 }
1096
1097 let Some(func) = tool.function.as_ref() else {
1098 continue;
1099 };
1100 has_function_tools = true;
1101 let name = func.name.clone();
1102 if !seen.insert(name.clone()) {
1103 continue;
1104 }
1105
1106 let description = func.description.clone();
1107 let parameters = sanitize_function_parameters(func.parameters.clone());
1108 function_declarations.push(FunctionDeclaration {
1109 name: name.clone(),
1110 description: description.clone(),
1111 parameters: parameters.clone(),
1112 });
1113 interaction_tools.push(InteractionTool::function(name, description, parameters));
1114 }
1115
1116 if !function_declarations.is_empty() {
1117 generate_tools.push(Tool {
1118 function_declarations: Some(function_declarations),
1119 ..Tool::default()
1120 });
1121 }
1122
1123 GeminiToolSpec {
1124 generate_tools: (!generate_tools.is_empty()).then_some(generate_tools),
1125 interaction_tools: (!interaction_tools.is_empty()).then_some(interaction_tools),
1126 uses_server_side_tools,
1127 has_function_tools,
1128 }
1129}
1130
1131fn build_generation_config(provider: &GeminiProvider, request: &LLMRequest) -> GenerationConfig {
1132 let mut generation_config = GenerationConfig {
1133 max_output_tokens: request.max_tokens,
1134 temperature: request.temperature,
1135 top_p: request.top_p,
1136 top_k: request.top_k,
1137 presence_penalty: request.presence_penalty,
1138 frequency_penalty: request.frequency_penalty,
1139 stop_sequences: request.stop_sequences.clone(),
1140 ..Default::default()
1141 };
1142
1143 if let Some(format) = &request.output_format {
1144 generation_config.response_mime_type = Some("application/json".to_string());
1145 if format.is_object() {
1146 generation_config.response_schema = Some(format.clone());
1147 }
1148 }
1149
1150 if let Some(effort) = request.reasoning_effort
1151 && provider.supports_reasoning_effort(&request.model)
1152 {
1153 let is_gemini3_flash = request.model.contains("gemini-3-flash");
1154 let thinking_level = match effort {
1155 ReasoningEffortLevel::None => Some("low"),
1156 ReasoningEffortLevel::Minimal => {
1157 if is_gemini3_flash {
1158 Some("minimal")
1159 } else {
1160 Some("low")
1161 }
1162 }
1163 ReasoningEffortLevel::Low => Some("low"),
1164 ReasoningEffortLevel::Medium => {
1165 if is_gemini3_flash {
1166 Some("medium")
1167 } else {
1168 Some("high")
1169 }
1170 }
1171 ReasoningEffortLevel::High
1172 | ReasoningEffortLevel::XHigh
1173 | ReasoningEffortLevel::Max => Some("high"),
1174 };
1175
1176 if let Some(level) = thinking_level {
1177 generation_config.thinking_config = Some(ThinkingConfig {
1178 thinking_level: Some(level.to_string()),
1179 });
1180 }
1181 }
1182
1183 generation_config
1184}
1185
1186fn build_interaction_tool_choice(
1187 tool_choice: Option<&ToolChoice>,
1188 has_function_tools: bool,
1189 uses_server_side_tools: bool,
1190) -> Option<InteractionToolChoice> {
1191 if !has_function_tools {
1192 return None;
1193 }
1194
1195 let mut choice = match tool_choice {
1196 Some(ToolChoice::None) => InteractionToolChoice::new("none"),
1197 Some(ToolChoice::Any) => InteractionToolChoice::new("any"),
1198 Some(ToolChoice::Specific(spec)) => {
1199 let mut choice = InteractionToolChoice::new("validated");
1200 if spec.tool_type == "function" {
1201 choice.tools = Some(vec![spec.function.name.clone()]);
1202 }
1203 choice
1204 }
1205 _ => {
1206 if uses_server_side_tools {
1207 InteractionToolChoice::new("validated")
1208 } else {
1209 InteractionToolChoice::new("auto")
1210 }
1211 }
1212 };
1213
1214 if choice.tools.as_ref().is_some_and(|tools| tools.is_empty()) {
1215 choice.tools = None;
1216 }
1217
1218 Some(choice)
1219}
1220
1221fn build_interaction_input(request: &LLMRequest) -> Result<InteractionInput, LLMError> {
1222 let relevant_messages = if request.previous_response_id.is_some() {
1223 interaction_delta_messages(&request.messages)
1224 } else {
1225 request.messages.clone()
1226 };
1227 let turns = build_interaction_turns(&relevant_messages, &request.messages)?;
1228
1229 if request.previous_response_id.is_none() {
1230 if let [turn] = turns.as_slice()
1231 && turn.role == "user"
1232 {
1233 return Ok(match &turn.content {
1234 InteractionTurnContent::Text(text) => InteractionInput::Text(text.clone()),
1235 InteractionTurnContent::Content(content) => {
1236 InteractionInput::Content(content.clone())
1237 }
1238 });
1239 }
1240 return Ok(InteractionInput::Turns(turns));
1241 }
1242
1243 if let [turn] = turns.as_slice()
1244 && turn.role == "user"
1245 {
1246 return Ok(match &turn.content {
1247 InteractionTurnContent::Text(text) => InteractionInput::Text(text.clone()),
1248 InteractionTurnContent::Content(content) => InteractionInput::Content(content.clone()),
1249 });
1250 }
1251
1252 Ok(InteractionInput::Turns(turns))
1253}
1254
1255fn interaction_delta_messages(messages: &[Message]) -> Vec<Message> {
1256 let start = messages
1257 .iter()
1258 .rposition(|message| message.role == MessageRole::Assistant)
1259 .map_or(0, |index| index.saturating_add(1));
1260 let delta = messages[start..].to_vec();
1261 if delta.is_empty() {
1262 messages.to_vec()
1263 } else {
1264 delta
1265 }
1266}
1267
1268fn build_interaction_turns(
1269 messages: &[Message],
1270 full_messages: &[Message],
1271) -> Result<Vec<InteractionTurn>, LLMError> {
1272 let mut call_map: HashMap<String, String> = HashMap::with_capacity(full_messages.len());
1273 for message in full_messages {
1274 if message.role == MessageRole::Assistant
1275 && let Some(tool_calls) = &message.tool_calls
1276 {
1277 for tool_call in tool_calls {
1278 if let Some(func) = &tool_call.function {
1279 call_map.insert(tool_call.id.clone(), func.name.clone());
1280 }
1281 }
1282 }
1283 }
1284
1285 let mut turns = Vec::new();
1286 for message in messages {
1287 if message.role == MessageRole::System {
1288 continue;
1289 }
1290
1291 let mut content = if message.role == MessageRole::Tool {
1292 Vec::new()
1293 } else {
1294 build_interaction_content(&message.content)
1295 };
1296 if message.role == MessageRole::Assistant
1297 && let Some(tool_calls) = &message.tool_calls
1298 {
1299 for tool_call in tool_calls {
1300 if let Some(func) = &tool_call.function {
1301 content.push(InteractionContent::FunctionCall {
1302 id: tool_call.id.clone(),
1303 name: func.name.clone(),
1304 arguments: tool_call.parsed_arguments().unwrap_or(Value::Null),
1305 signature: tool_call.thought_signature.clone(),
1306 });
1307 }
1308 }
1309 }
1310 if message.role == MessageRole::Tool {
1311 let tool_call_id =
1312 message
1313 .tool_call_id
1314 .clone()
1315 .ok_or_else(|| LLMError::InvalidRequest {
1316 message: "Gemini interactions require tool_call_id for tool messages"
1317 .to_string(),
1318 metadata: None,
1319 })?;
1320 content.push(InteractionContent::FunctionResult {
1321 call_id: tool_call_id.clone(),
1322 name: call_map.get(&tool_call_id).cloned(),
1323 result: interaction_result_from_message_content(&message.content),
1324 is_error: None,
1325 signature: None,
1326 });
1327 }
1328 if content.is_empty() {
1329 continue;
1330 }
1331
1332 let role = if message.role == MessageRole::Assistant {
1333 "model"
1334 } else {
1335 "user"
1336 };
1337 let content = match content.as_slice() {
1338 [InteractionContent::Text { text }] => InteractionTurnContent::Text(text.clone()),
1339 _ => InteractionTurnContent::Content(content),
1340 };
1341 turns.push(InteractionTurn {
1342 role: role.to_string(),
1343 content,
1344 });
1345 }
1346
1347 Ok(turns)
1348}
1349
1350fn interaction_result_from_message_content(content: &MessageContent) -> InteractionResult {
1351 match content {
1352 MessageContent::Text(text) => interaction_result_from_text(text),
1353 MessageContent::Parts(_) => {
1354 let parts = build_interaction_content(content);
1355 if let [InteractionContent::Text { text }] = parts.as_slice() {
1356 interaction_result_from_text(text)
1357 } else {
1358 InteractionResult::Content(parts)
1359 }
1360 }
1361 }
1362}
1363
1364fn interaction_result_from_text(text: &str) -> InteractionResult {
1365 let trimmed = text.trim();
1366 if trimmed.is_empty() {
1367 return InteractionResult::String(String::new());
1368 }
1369
1370 if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
1371 if let Some(content) = interaction_result_content_array(&value) {
1372 return InteractionResult::Content(content);
1373 }
1374 if value.is_object() {
1375 return InteractionResult::Json(value);
1376 }
1377 }
1378
1379 InteractionResult::String(text.to_string())
1380}
1381
1382fn interaction_result_content_array(value: &Value) -> Option<Vec<InteractionContent>> {
1383 let items = value.as_array()?;
1384 let mut content = Vec::with_capacity(items.len());
1385 for item in items {
1386 let item_type = item.get("type")?.as_str()?;
1387 match item_type {
1388 "text" => content.push(InteractionContent::Text {
1389 text: item.get("text")?.as_str()?.to_string(),
1390 }),
1391 "image" => {
1392 let mime_type = item.get("mime_type")?.as_str()?.to_string();
1393 let data = item.get("data")?.as_str()?.to_string();
1394 content.push(InteractionContent::Image { data, mime_type });
1395 }
1396 _ => return None,
1397 }
1398 }
1399
1400 Some(content)
1401}
1402
1403fn interaction_object(payload: &Value) -> &Map<String, Value> {
1404 payload
1405 .get("interaction")
1406 .and_then(Value::as_object)
1407 .or_else(|| payload.as_object())
1408 .expect("stream payload should be an object")
1409}
1410
1411fn apply_interaction_delta(
1412 builder: &mut InteractionStreamOutputBuilder,
1413 delta: &Map<String, Value>,
1414 events: &mut Vec<LLMStreamEvent>,
1415) {
1416 let delta_type = delta
1417 .get("type")
1418 .and_then(Value::as_str)
1419 .unwrap_or_default();
1420
1421 match delta_type {
1422 "text" => {
1423 builder.output_type = "text".to_string();
1424 if let Some(text) = delta.get("text").and_then(Value::as_str) {
1425 builder.text.push_str(text);
1426 events.push(LLMStreamEvent::Token {
1427 delta: text.to_string(),
1428 });
1429 }
1430 }
1431 "thought" => {
1432 builder.output_type = "thought".to_string();
1433 if let Some(text) = delta
1434 .get("thought")
1435 .and_then(Value::as_str)
1436 .or_else(|| delta.get("text").and_then(Value::as_str))
1437 {
1438 builder.summary.push_str(text);
1439 events.push(LLMStreamEvent::Reasoning {
1440 delta: text.to_string(),
1441 });
1442 }
1443 }
1444 "thought_summary" => {
1445 builder.output_type = "thought".to_string();
1446 if let Some(text) = delta
1447 .get("content")
1448 .and_then(Value::as_object)
1449 .and_then(|content| content.get("text"))
1450 .and_then(Value::as_str)
1451 .or_else(|| delta.get("text").and_then(Value::as_str))
1452 {
1453 builder.summary.push_str(text);
1454 events.push(LLMStreamEvent::Reasoning {
1455 delta: text.to_string(),
1456 });
1457 }
1458 }
1459 "thought_signature" => {
1460 builder.output_type = "thought".to_string();
1461 if let Some(signature) = delta.get("signature").and_then(Value::as_str) {
1462 builder.signature = Some(signature.to_string());
1463 }
1464 }
1465 "function_call" => {
1466 builder.output_type = "function_call".to_string();
1467 if let Some(id) = delta.get("id").and_then(Value::as_str) {
1468 builder.id = Some(id.to_string());
1469 }
1470 if let Some(name) = delta.get("name").and_then(Value::as_str) {
1471 builder.name = Some(name.to_string());
1472 }
1473 if let Some(arguments) = delta.get("arguments") {
1474 builder.arguments = Some(arguments.clone());
1475 }
1476 if let Some(signature) = delta.get("signature").and_then(Value::as_str) {
1477 builder.signature = Some(signature.to_string());
1478 }
1479 }
1480 _ => {}
1481 }
1482}