Skip to main content

vtcode_core/llm/providers/gemini/
helpers.rs

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    /// Check if model supports context caching
66    pub fn supports_caching(model: &str) -> bool {
67        models::google::CACHING_MODELS.contains(&model)
68    }
69
70    /// Check if model supports code execution
71    pub fn supports_code_execution(model: &str) -> bool {
72        models::google::CODE_EXECUTION_MODELS.contains(&model)
73    }
74
75    /// Get maximum input token limit for a model
76    pub fn max_input_tokens(model: &str) -> usize {
77        if model.contains("gemini-3.1") {
78            1_048_576 // 1M tokens for Gemini 3.1 models
79        } else if model.contains("3") || model.contains("1.5-pro") {
80            2_097_152 // 2M tokens for Gemini 1.5 Pro and 3.x models
81        } else {
82            1_048_576 // 1M tokens for other current models
83        }
84    }
85
86    /// Get maximum output token limit for a model
87    pub fn max_output_tokens(model: &str) -> usize {
88        if model.contains("3") {
89            65_536 // 65K tokens for Gemini 3 models
90        } else {
91            8_192 // Conservative default
92        }
93    }
94
95    /// Check if model supports extended thinking levels (minimal, medium)
96    /// Only Gemini 3 Flash supports these additional levels
97    pub fn supports_extended_thinking(model: &str) -> bool {
98        model.contains("gemini-3-flash")
99    }
100
101    /// Get supported thinking levels for a model
102    /// Reference: <https://ai.google.dev/gemini-api/docs/gemini-3>
103    pub fn supported_thinking_levels(model: &str) -> Vec<&'static str> {
104        if model.contains("gemini-3-flash") {
105            // Gemini 3 Flash supports all levels
106            vec!["minimal", "low", "medium", "high"]
107        } else if model.contains("gemini-3") {
108            // Gemini 3 Pro supports low and high
109            vec!["low", "high"]
110        } else {
111            // Unknown model, conservative default
112            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            // Explicit cache handling requires separate cache lifecycle APIs which are
151            // coordinated outside of the request payload. Placeholder ensures we surface
152            // configuration usage even when implicit mode is active.
153        }
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, // Function responses don't carry thought signatures
205                    });
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        // For Gemini 3 Pro, Google recommends keeping temperature at 1.0 default
229        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        // Track thought signature from text parts to attach to subsequent function calls
405        // This is needed because Gemini 3 sometimes attaches the signature to the reasoning text
406        // but requires it to be present on the function call when replayed in history.
407        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                    // Use the signature from the function call, or fall back to the one from preceding text
431                    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}