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                iterations: None,
624            }),
625            finish_reason,
626            reasoning,
627            reasoning_details,
628            tool_references: Vec::new(),
629            request_id: Some(response.id),
630            organization_id: None,
631            compaction: None,
632        })
633    }
634
635    pub(super) fn apply_interaction_stream_payload(
636        state: &mut InteractionStreamState,
637        payload: &Value,
638    ) -> Result<Vec<LLMStreamEvent>, LLMError> {
639        let mut events = Vec::new();
640        let Some(event_type) = payload.get("event_type").and_then(Value::as_str) else {
641            return Ok(events);
642        };
643
644        match event_type {
645            "interaction.start" | "interaction.status_update" | "interaction.complete" => {
646                let interaction = interaction_object(payload);
647                if let Some(id) = interaction.get("id").and_then(Value::as_str) {
648                    state.interaction_id = Some(id.to_string());
649                }
650                if let Some(status) = interaction.get("status").and_then(Value::as_str) {
651                    state.status = Some(status.to_string());
652                }
653                if let Some(usage) = interaction.get("usage")
654                    && let Ok(usage) = serde_json::from_value(usage.clone())
655                {
656                    state.usage = Some(usage);
657                }
658                if event_type == "interaction.complete" {
659                    state.completed = true;
660                }
661            }
662            "content.start" => {
663                let index = payload
664                    .get("index")
665                    .and_then(Value::as_u64)
666                    .unwrap_or_default() as usize;
667                let builder = state.outputs.entry(index).or_default();
668                if let Some(output_type) = payload
669                    .get("content")
670                    .and_then(Value::as_object)
671                    .and_then(|content| content.get("type"))
672                    .and_then(Value::as_str)
673                {
674                    builder.output_type = output_type.to_string();
675                }
676            }
677            "content.delta" => {
678                let index = payload
679                    .get("index")
680                    .and_then(Value::as_u64)
681                    .unwrap_or_default() as usize;
682                let Some(delta) = payload.get("delta").and_then(Value::as_object) else {
683                    return Ok(events);
684                };
685                let builder = state.outputs.entry(index).or_default();
686                apply_interaction_delta(builder, delta, &mut events);
687            }
688            "content.stop" => {}
689            "error" => {
690                let error_message = payload
691                    .get("error")
692                    .and_then(Value::as_object)
693                    .and_then(|error| error.get("message"))
694                    .and_then(Value::as_str)
695                    .unwrap_or("Unknown Gemini interactions streaming error");
696                let formatted = error_display::format_llm_error("Gemini", error_message);
697                return Err(LLMError::Provider {
698                    message: formatted,
699                    metadata: None,
700                });
701            }
702            _ => {}
703        }
704
705        Ok(events)
706    }
707
708    pub(super) fn finalize_interaction_stream_state(
709        state: InteractionStreamState,
710        model: String,
711    ) -> Result<LLMResponse, LLMError> {
712        let interaction = Interaction {
713            id: state
714                .interaction_id
715                .unwrap_or_else(|| "interaction_stream".to_string()),
716            model: model.clone(),
717            status: state.status,
718            outputs: state
719                .outputs
720                .into_values()
721                .map(InteractionStreamOutputBuilder::into_output)
722                .collect(),
723            usage: state.usage,
724        };
725
726        Self::convert_from_interaction_response(interaction, model)
727    }
728
729    pub(super) fn convert_from_streaming_response(
730        response: StreamingResponse,
731        model: String,
732    ) -> Result<LLMResponse, LLMError> {
733        let converted_candidates: Vec<Candidate> = response
734            .candidates
735            .into_iter()
736            .map(|candidate| Candidate {
737                content: candidate.content,
738                finish_reason: candidate.finish_reason,
739            })
740            .collect();
741
742        let converted = GenerateContentResponse {
743            candidates: converted_candidates,
744            prompt_feedback: None,
745            usage_metadata: response.usage_metadata,
746        };
747
748        Self::convert_from_gemini_response(converted, model)
749    }
750
751    #[cold]
752    pub(super) fn map_streaming_error(error: StreamingError) -> LLMError {
753        match error {
754            StreamingError::NetworkError { message, .. } => {
755                let formatted = error_display::format_llm_error(
756                    "Gemini",
757                    &format!("Network error: {}", message),
758                );
759                LLMError::Network {
760                    message: formatted,
761                    metadata: None,
762                }
763            }
764            StreamingError::ApiError {
765                status_code,
766                message,
767                ..
768            } => {
769                if status_code == 401 || status_code == 403 {
770                    let formatted = error_display::format_llm_error(
771                        "Gemini",
772                        &format!("HTTP {}: {}", status_code, message),
773                    );
774                    LLMError::Authentication {
775                        message: formatted,
776                        metadata: None,
777                    }
778                } else if status_code == 429 {
779                    LLMError::RateLimit { metadata: None }
780                } else {
781                    let formatted = error_display::format_llm_error(
782                        "Gemini",
783                        &format!("API error ({}): {}", status_code, message),
784                    );
785                    LLMError::Provider {
786                        message: formatted,
787                        metadata: None,
788                    }
789                }
790            }
791            StreamingError::ParseError { message, .. } => {
792                let formatted =
793                    error_display::format_llm_error("Gemini", &format!("Parse error: {}", message));
794                LLMError::Provider {
795                    message: formatted,
796                    metadata: None,
797                }
798            }
799            StreamingError::TimeoutError {
800                operation,
801                duration,
802            } => {
803                let formatted = error_display::format_llm_error(
804                    "Gemini",
805                    &format!(
806                        "Streaming timeout during {} after {:?}",
807                        operation, duration
808                    ),
809                );
810                LLMError::Network {
811                    message: formatted,
812                    metadata: None,
813                }
814            }
815            StreamingError::ContentError { message } => {
816                let formatted = error_display::format_llm_error(
817                    "Gemini",
818                    &format!("Content error: {}", message),
819                );
820                LLMError::Provider {
821                    message: formatted,
822                    metadata: None,
823                }
824            }
825            StreamingError::StreamingError { message, .. } => {
826                let formatted = error_display::format_llm_error(
827                    "Gemini",
828                    &format!("Streaming error: {}", message),
829                );
830                LLMError::Provider {
831                    message: formatted,
832                    metadata: None,
833                }
834            }
835        }
836    }
837}
838
839fn parts_from_message_content(content: &MessageContent) -> Vec<Part> {
840    match content {
841        MessageContent::Text(text) => {
842            if text.is_empty() {
843                Vec::new()
844            } else {
845                vec![Part::Text {
846                    text: text.clone(),
847                    thought_signature: None,
848                }]
849            }
850        }
851        MessageContent::Parts(parts) => {
852            let mut converted = Vec::new();
853            for part in parts {
854                match part {
855                    ContentPart::Text { text } => {
856                        if !text.is_empty() {
857                            converted.push(Part::Text {
858                                text: text.clone(),
859                                thought_signature: None,
860                            });
861                        }
862                    }
863                    ContentPart::Image {
864                        data, mime_type, ..
865                    } => {
866                        converted.push(Part::InlineData {
867                            inline_data: InlineData {
868                                mime_type: mime_type.clone(),
869                                data: data.clone(),
870                            },
871                        });
872                    }
873                    ContentPart::File {
874                        filename,
875                        file_id,
876                        file_url,
877                        ..
878                    } => {
879                        let fallback = filename
880                            .clone()
881                            .or_else(|| file_id.clone())
882                            .or_else(|| file_url.clone())
883                            .unwrap_or_else(|| "attached file".to_string());
884                        converted.push(Part::Text {
885                            text: format!("[File input not directly supported: {}]", fallback),
886                            thought_signature: None,
887                        });
888                    }
889                }
890            }
891            converted
892        }
893    }
894}
895
896fn build_interaction_content(content: &MessageContent) -> Vec<InteractionContent> {
897    match content {
898        MessageContent::Text(text) => {
899            if text.is_empty() {
900                Vec::new()
901            } else {
902                vec![InteractionContent::Text { text: text.clone() }]
903            }
904        }
905        MessageContent::Parts(parts) => {
906            let mut converted = Vec::new();
907            for part in parts {
908                match part {
909                    ContentPart::Text { text } => {
910                        if !text.is_empty() {
911                            converted.push(InteractionContent::Text { text: text.clone() });
912                        }
913                    }
914                    ContentPart::Image {
915                        data, mime_type, ..
916                    } => converted.push(InteractionContent::Image {
917                        data: data.clone(),
918                        mime_type: mime_type.clone(),
919                    }),
920                    ContentPart::File {
921                        filename,
922                        file_id,
923                        file_url,
924                        ..
925                    } => {
926                        let fallback = filename
927                            .clone()
928                            .or_else(|| file_id.clone())
929                            .or_else(|| file_url.clone())
930                            .unwrap_or_else(|| "attached file".to_string());
931                        converted.push(InteractionContent::Text {
932                            text: {
933                                let mut s = String::with_capacity(38 + fallback.len());
934                                s.push_str("[File input not directly supported: ");
935                                s.push_str(&fallback);
936                                s.push(']');
937                                s
938                            },
939                        });
940                    }
941                }
942            }
943            converted
944        }
945    }
946}
947
948fn build_message_parts(message: &Message, model: &str) -> Vec<Part> {
949    let mut parts = Vec::new();
950    if message.role != MessageRole::Tool {
951        parts.extend(parts_from_message_content(&message.content));
952    }
953
954    if message.role == MessageRole::Assistant
955        && let Some(tool_calls) = &message.tool_calls
956    {
957        let is_gemini3 = model.contains("gemini-3");
958        for tool_call in tool_calls {
959            if let Some(ref func) = tool_call.function {
960                let parsed_args = tool_call.parsed_arguments().unwrap_or_else(|_| json!({}));
961
962                let thought_signature = if is_gemini3 && tool_call.thought_signature.is_none() {
963                    tracing::trace!(
964                        function_name = %func.name,
965                        "Gemini 3: using skip_thought_signature_validator fallback"
966                    );
967                    Some("skip_thought_signature_validator".to_string())
968                } else {
969                    tool_call.thought_signature.clone()
970                };
971
972                parts.push(Part::FunctionCall {
973                    function_call: GeminiFunctionCall {
974                        name: func.name.clone(),
975                        args: parsed_args,
976                        id: Some(tool_call.id.clone()),
977                    },
978                    thought_signature,
979                });
980            }
981        }
982    }
983
984    parts
985}
986
987fn preserved_gemini_parts_from_message(message: &Message) -> Option<Vec<Part>> {
988    let details = message.reasoning_details.as_ref()?;
989    for detail in details {
990        let Some(text) = detail.as_str() else {
991            continue;
992        };
993        let Some(payload) = text.strip_prefix(GEMINI_PRESERVED_PARTS_PREFIX) else {
994            continue;
995        };
996        if let Ok(parts) = serde_json::from_str::<Vec<Part>>(payload) {
997            return Some(parts);
998        }
999    }
1000    None
1001}
1002
1003fn preserved_gemini_parts_detail(parts: &[Part]) -> Option<Vec<String>> {
1004    if !parts_require_roundtrip_history(parts) {
1005        return None;
1006    }
1007
1008    serde_json::to_string(parts)
1009        .ok()
1010        .map(|serialized| vec![format!("{GEMINI_PRESERVED_PARTS_PREFIX}{serialized}")])
1011}
1012
1013fn parts_require_roundtrip_history(parts: &[Part]) -> bool {
1014    parts.iter().any(|part| {
1015        part.thought_signature().is_some()
1016            || matches!(
1017                part,
1018                Part::ToolCall { .. }
1019                    | Part::ToolResponse { .. }
1020                    | Part::ExecutableCode { .. }
1021                    | Part::CodeExecutionResult { .. }
1022                    | Part::FunctionResponse { .. }
1023                    | Part::InlineData { .. }
1024            )
1025    })
1026}
1027
1028fn gemini_built_in_tool(tool: &ToolDefinition) -> Option<Tool> {
1029    match tool.tool_type.as_str() {
1030        "web_search" | "google_search" => Some(Tool {
1031            google_search: Some(tool.web_search.clone().unwrap_or_else(|| json!({}))),
1032            ..Tool::default()
1033        }),
1034        "google_maps" => Some(Tool {
1035            google_maps: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1036            ..Tool::default()
1037        }),
1038        "url_context" => Some(Tool {
1039            url_context: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1040            ..Tool::default()
1041        }),
1042        "file_search" => Some(Tool {
1043            file_search: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1044            ..Tool::default()
1045        }),
1046        "code_execution" => Some(Tool {
1047            code_execution: Some(tool.hosted_tool_config.clone().unwrap_or_else(|| json!({}))),
1048            ..Tool::default()
1049        }),
1050        other if other.starts_with("code_execution_") => Some(Tool {
1051            code_execution: Some(json!({})),
1052            ..Tool::default()
1053        }),
1054        _ => None,
1055    }
1056}
1057
1058fn gemini_interaction_built_in_tool(tool: &ToolDefinition) -> Option<InteractionTool> {
1059    let (tool_type, config) = match tool.tool_type.as_str() {
1060        "web_search" | "google_search" => ("google_search", tool.web_search.as_ref()),
1061        "google_maps" => ("google_maps", tool.hosted_tool_config.as_ref()),
1062        "url_context" => ("url_context", tool.hosted_tool_config.as_ref()),
1063        "file_search" => ("file_search", tool.hosted_tool_config.as_ref()),
1064        "code_execution" => ("code_execution", tool.hosted_tool_config.as_ref()),
1065        other if other.starts_with("code_execution_") => ("code_execution", None),
1066        _ => return None,
1067    };
1068
1069    Some(InteractionTool::built_in(tool_type, config))
1070}
1071
1072fn collect_gemini_tool_spec(definitions: Option<&[ToolDefinition]>) -> GeminiToolSpec {
1073    let Some(definitions) = definitions else {
1074        return GeminiToolSpec {
1075            generate_tools: None,
1076            interaction_tools: None,
1077            uses_server_side_tools: false,
1078            has_function_tools: false,
1079        };
1080    };
1081
1082    let mut generate_tools = Vec::new();
1083    let mut interaction_tools = Vec::new();
1084    let mut function_declarations = Vec::new();
1085    let mut seen = hashbrown::HashSet::new();
1086    let mut uses_server_side_tools = false;
1087    let mut has_function_tools = false;
1088
1089    for tool in definitions {
1090        if let Some(built_in_tool) = gemini_built_in_tool(tool) {
1091            uses_server_side_tools = true;
1092            generate_tools.push(built_in_tool);
1093        }
1094        if let Some(interaction_tool) = gemini_interaction_built_in_tool(tool) {
1095            interaction_tools.push(interaction_tool);
1096        }
1097
1098        let Some(func) = tool.function.as_ref() else {
1099            continue;
1100        };
1101        has_function_tools = true;
1102        let name = func.name.clone();
1103        if !seen.insert(name.clone()) {
1104            continue;
1105        }
1106
1107        let description = func.description.clone();
1108        let parameters = sanitize_function_parameters(func.parameters.clone());
1109        function_declarations.push(FunctionDeclaration {
1110            name: name.clone(),
1111            description: description.clone(),
1112            parameters: parameters.clone(),
1113        });
1114        interaction_tools.push(InteractionTool::function(name, description, parameters));
1115    }
1116
1117    if !function_declarations.is_empty() {
1118        generate_tools.push(Tool {
1119            function_declarations: Some(function_declarations),
1120            ..Tool::default()
1121        });
1122    }
1123
1124    GeminiToolSpec {
1125        generate_tools: (!generate_tools.is_empty()).then_some(generate_tools),
1126        interaction_tools: (!interaction_tools.is_empty()).then_some(interaction_tools),
1127        uses_server_side_tools,
1128        has_function_tools,
1129    }
1130}
1131
1132fn build_generation_config(provider: &GeminiProvider, request: &LLMRequest) -> GenerationConfig {
1133    let mut generation_config = GenerationConfig {
1134        max_output_tokens: request.max_tokens,
1135        temperature: request.temperature,
1136        top_p: request.top_p,
1137        top_k: request.top_k,
1138        presence_penalty: request.presence_penalty,
1139        frequency_penalty: request.frequency_penalty,
1140        stop_sequences: request.stop_sequences.clone(),
1141        ..Default::default()
1142    };
1143
1144    if let Some(format) = &request.output_format {
1145        generation_config.response_mime_type = Some("application/json".to_string());
1146        if format.is_object() {
1147            generation_config.response_schema = Some(format.clone());
1148        }
1149    }
1150
1151    if let Some(effort) = request.reasoning_effort
1152        && provider.supports_reasoning_effort(&request.model)
1153    {
1154        let is_gemini3_flash = request.model.contains("gemini-3-flash");
1155        let thinking_level = match effort {
1156            ReasoningEffortLevel::None => Some("low"),
1157            ReasoningEffortLevel::Minimal => {
1158                if is_gemini3_flash {
1159                    Some("minimal")
1160                } else {
1161                    Some("low")
1162                }
1163            }
1164            ReasoningEffortLevel::Low => Some("low"),
1165            ReasoningEffortLevel::Medium => {
1166                if is_gemini3_flash {
1167                    Some("medium")
1168                } else {
1169                    Some("high")
1170                }
1171            }
1172            ReasoningEffortLevel::High
1173            | ReasoningEffortLevel::XHigh
1174            | ReasoningEffortLevel::Max => Some("high"),
1175        };
1176
1177        if let Some(level) = thinking_level {
1178            generation_config.thinking_config = Some(ThinkingConfig {
1179                thinking_level: Some(level.to_string()),
1180            });
1181        }
1182    }
1183
1184    generation_config
1185}
1186
1187fn build_interaction_tool_choice(
1188    tool_choice: Option<&ToolChoice>,
1189    has_function_tools: bool,
1190    uses_server_side_tools: bool,
1191) -> Option<InteractionToolChoice> {
1192    if !has_function_tools {
1193        return None;
1194    }
1195
1196    let mut choice = match tool_choice {
1197        Some(ToolChoice::None) => InteractionToolChoice::new("none"),
1198        Some(ToolChoice::Any) => InteractionToolChoice::new("any"),
1199        Some(ToolChoice::Specific(spec)) => {
1200            let mut choice = InteractionToolChoice::new("validated");
1201            if spec.tool_type == "function" {
1202                choice.tools = Some(vec![spec.function.name.clone()]);
1203            }
1204            choice
1205        }
1206        _ => {
1207            if uses_server_side_tools {
1208                InteractionToolChoice::new("validated")
1209            } else {
1210                InteractionToolChoice::new("auto")
1211            }
1212        }
1213    };
1214
1215    if choice.tools.as_ref().is_some_and(|tools| tools.is_empty()) {
1216        choice.tools = None;
1217    }
1218
1219    Some(choice)
1220}
1221
1222fn build_interaction_input(request: &LLMRequest) -> Result<InteractionInput, LLMError> {
1223    let relevant_messages = if request.previous_response_id.is_some() {
1224        interaction_delta_messages(&request.messages)
1225    } else {
1226        request.messages.clone()
1227    };
1228    let turns = build_interaction_turns(&relevant_messages, &request.messages)?;
1229
1230    if request.previous_response_id.is_none() {
1231        if let [turn] = turns.as_slice()
1232            && turn.role == "user"
1233        {
1234            return Ok(match &turn.content {
1235                InteractionTurnContent::Text(text) => InteractionInput::Text(text.clone()),
1236                InteractionTurnContent::Content(content) => {
1237                    InteractionInput::Content(content.clone())
1238                }
1239            });
1240        }
1241        return Ok(InteractionInput::Turns(turns));
1242    }
1243
1244    if let [turn] = turns.as_slice()
1245        && turn.role == "user"
1246    {
1247        return Ok(match &turn.content {
1248            InteractionTurnContent::Text(text) => InteractionInput::Text(text.clone()),
1249            InteractionTurnContent::Content(content) => InteractionInput::Content(content.clone()),
1250        });
1251    }
1252
1253    Ok(InteractionInput::Turns(turns))
1254}
1255
1256fn interaction_delta_messages(messages: &[Message]) -> Vec<Message> {
1257    let start = messages
1258        .iter()
1259        .rposition(|message| message.role == MessageRole::Assistant)
1260        .map_or(0, |index| index.saturating_add(1));
1261    let delta = messages[start..].to_vec();
1262    if delta.is_empty() {
1263        messages.to_vec()
1264    } else {
1265        delta
1266    }
1267}
1268
1269fn build_interaction_turns(
1270    messages: &[Message],
1271    full_messages: &[Message],
1272) -> Result<Vec<InteractionTurn>, LLMError> {
1273    let mut call_map: HashMap<String, String> = HashMap::with_capacity(full_messages.len());
1274    for message in full_messages {
1275        if message.role == MessageRole::Assistant
1276            && let Some(tool_calls) = &message.tool_calls
1277        {
1278            for tool_call in tool_calls {
1279                if let Some(func) = &tool_call.function {
1280                    call_map.insert(tool_call.id.clone(), func.name.clone());
1281                }
1282            }
1283        }
1284    }
1285
1286    let mut turns = Vec::new();
1287    for message in messages {
1288        if message.role == MessageRole::System {
1289            continue;
1290        }
1291
1292        let mut content = if message.role == MessageRole::Tool {
1293            Vec::new()
1294        } else {
1295            build_interaction_content(&message.content)
1296        };
1297        if message.role == MessageRole::Assistant
1298            && let Some(tool_calls) = &message.tool_calls
1299        {
1300            for tool_call in tool_calls {
1301                if let Some(func) = &tool_call.function {
1302                    content.push(InteractionContent::FunctionCall {
1303                        id: tool_call.id.clone(),
1304                        name: func.name.clone(),
1305                        arguments: tool_call.parsed_arguments().unwrap_or(Value::Null),
1306                        signature: tool_call.thought_signature.clone(),
1307                    });
1308                }
1309            }
1310        }
1311        if message.role == MessageRole::Tool {
1312            let tool_call_id =
1313                message
1314                    .tool_call_id
1315                    .clone()
1316                    .ok_or_else(|| LLMError::InvalidRequest {
1317                        message: "Gemini interactions require tool_call_id for tool messages"
1318                            .to_string(),
1319                        metadata: None,
1320                    })?;
1321            content.push(InteractionContent::FunctionResult {
1322                call_id: tool_call_id.clone(),
1323                name: call_map.get(&tool_call_id).cloned(),
1324                result: interaction_result_from_message_content(&message.content),
1325                is_error: None,
1326                signature: None,
1327            });
1328        }
1329        if content.is_empty() {
1330            continue;
1331        }
1332
1333        let role = if message.role == MessageRole::Assistant {
1334            "model"
1335        } else {
1336            "user"
1337        };
1338        let content = match content.as_slice() {
1339            [InteractionContent::Text { text }] => InteractionTurnContent::Text(text.clone()),
1340            _ => InteractionTurnContent::Content(content),
1341        };
1342        turns.push(InteractionTurn {
1343            role: role.to_string(),
1344            content,
1345        });
1346    }
1347
1348    Ok(turns)
1349}
1350
1351fn interaction_result_from_message_content(content: &MessageContent) -> InteractionResult {
1352    match content {
1353        MessageContent::Text(text) => interaction_result_from_text(text),
1354        MessageContent::Parts(_) => {
1355            let parts = build_interaction_content(content);
1356            if let [InteractionContent::Text { text }] = parts.as_slice() {
1357                interaction_result_from_text(text)
1358            } else {
1359                InteractionResult::Content(parts)
1360            }
1361        }
1362    }
1363}
1364
1365fn interaction_result_from_text(text: &str) -> InteractionResult {
1366    let trimmed = text.trim();
1367    if trimmed.is_empty() {
1368        return InteractionResult::String(String::new());
1369    }
1370
1371    if let Ok(value) = serde_json::from_str::<Value>(trimmed) {
1372        if let Some(content) = interaction_result_content_array(&value) {
1373            return InteractionResult::Content(content);
1374        }
1375        if value.is_object() {
1376            return InteractionResult::Json(value);
1377        }
1378    }
1379
1380    InteractionResult::String(text.to_string())
1381}
1382
1383fn interaction_result_content_array(value: &Value) -> Option<Vec<InteractionContent>> {
1384    let items = value.as_array()?;
1385    let mut content = Vec::with_capacity(items.len());
1386    for item in items {
1387        let item_type = item.get("type")?.as_str()?;
1388        match item_type {
1389            "text" => content.push(InteractionContent::Text {
1390                text: item.get("text")?.as_str()?.to_string(),
1391            }),
1392            "image" => {
1393                let mime_type = item.get("mime_type")?.as_str()?.to_string();
1394                let data = item.get("data")?.as_str()?.to_string();
1395                content.push(InteractionContent::Image { data, mime_type });
1396            }
1397            _ => return None,
1398        }
1399    }
1400
1401    Some(content)
1402}
1403
1404fn interaction_object(payload: &Value) -> &Map<String, Value> {
1405    payload
1406        .get("interaction")
1407        .and_then(Value::as_object)
1408        .or_else(|| payload.as_object())
1409        .expect("stream payload should be an object")
1410}
1411
1412fn apply_interaction_delta(
1413    builder: &mut InteractionStreamOutputBuilder,
1414    delta: &Map<String, Value>,
1415    events: &mut Vec<LLMStreamEvent>,
1416) {
1417    let delta_type = delta
1418        .get("type")
1419        .and_then(Value::as_str)
1420        .unwrap_or_default();
1421
1422    match delta_type {
1423        "text" => {
1424            builder.output_type = "text".to_string();
1425            if let Some(text) = delta.get("text").and_then(Value::as_str) {
1426                builder.text.push_str(text);
1427                events.push(LLMStreamEvent::Token {
1428                    delta: text.to_string(),
1429                });
1430            }
1431        }
1432        "thought" => {
1433            builder.output_type = "thought".to_string();
1434            if let Some(text) = delta
1435                .get("thought")
1436                .and_then(Value::as_str)
1437                .or_else(|| delta.get("text").and_then(Value::as_str))
1438            {
1439                builder.summary.push_str(text);
1440                events.push(LLMStreamEvent::Reasoning {
1441                    delta: text.to_string(),
1442                });
1443            }
1444        }
1445        "thought_summary" => {
1446            builder.output_type = "thought".to_string();
1447            if let Some(text) = delta
1448                .get("content")
1449                .and_then(Value::as_object)
1450                .and_then(|content| content.get("text"))
1451                .and_then(Value::as_str)
1452                .or_else(|| delta.get("text").and_then(Value::as_str))
1453            {
1454                builder.summary.push_str(text);
1455                events.push(LLMStreamEvent::Reasoning {
1456                    delta: text.to_string(),
1457                });
1458            }
1459        }
1460        "thought_signature" => {
1461            builder.output_type = "thought".to_string();
1462            if let Some(signature) = delta.get("signature").and_then(Value::as_str) {
1463                builder.signature = Some(signature.to_string());
1464            }
1465        }
1466        "function_call" => {
1467            builder.output_type = "function_call".to_string();
1468            if let Some(id) = delta.get("id").and_then(Value::as_str) {
1469                builder.id = Some(id.to_string());
1470            }
1471            if let Some(name) = delta.get("name").and_then(Value::as_str) {
1472                builder.name = Some(name.to_string());
1473            }
1474            if let Some(arguments) = delta.get("arguments") {
1475                builder.arguments = Some(arguments.clone());
1476            }
1477            if let Some(signature) = delta.get("signature").and_then(Value::as_str) {
1478                builder.signature = Some(signature.to_string());
1479            }
1480        }
1481        _ => {}
1482    }
1483}