1use super::wire::{GenerationConfig, InlineData, StreamingError, ThinkingConfig};
2use super::*;
3use crate::config::constants::models;
4use crate::llm::error_display;
5use crate::llm::provider::LLMError;
6use crate::llm::provider::{ContentPart, MessageContent, ToolDefinition};
7use crate::llm::providers::common::{
8 collect_history_system_directives, merge_system_prompt_with_history_directives,
9};
10use crate::prompts::system::default_system_prompt;
11use serde_json::Map;
12use std::collections::BTreeMap;
13
14const GEMINI_PRESERVED_PARTS_PREFIX: &str = "__vtcode_gemini_parts__:";
15
16struct GeminiToolSpec {
17 generate_tools: Option<Vec<Tool>>,
18 interaction_tools: Option<Vec<InteractionTool>>,
19 uses_server_side_tools: bool,
20 has_function_tools: bool,
21}
22
23#[derive(Debug, Clone, Default)]
24pub(super) struct InteractionStreamOutputBuilder {
25 pub output_type: String,
26 pub text: String,
27 pub summary: String,
28 pub id: Option<String>,
29 pub name: Option<String>,
30 pub arguments: Option<Value>,
31 pub signature: Option<String>,
32}
33
34impl InteractionStreamOutputBuilder {
35 fn into_output(self) -> InteractionOutput {
36 InteractionOutput {
37 output_type: self.output_type,
38 text: (!self.text.is_empty()).then_some(self.text),
39 id: self.id,
40 name: self.name,
41 arguments: self.arguments,
42 signature: self.signature,
43 function_call: None,
44 summary: (!self.summary.is_empty()).then_some(self.summary),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Default)]
50pub(super) struct InteractionStreamState {
51 pub interaction_id: Option<String>,
52 pub status: Option<String>,
53 pub outputs: BTreeMap<usize, InteractionStreamOutputBuilder>,
54 pub usage: Option<wire::interactions::InteractionUsage>,
55 pub completed: bool,
56}
57
58impl GeminiProvider {
59 const HISTORY_DIRECTIVES_SECTION_HEADER: &str = "[History Directives]";
60
61 pub(super) fn is_gemini_3_pro_model(model: &str) -> bool {
62 model.contains("gemini-3") && model.contains("pro") && !model.contains("flash")
63 }
64
65 pub fn supports_caching(model: &str) -> bool {
67 models::google::CACHING_MODELS.contains(&model)
68 }
69
70 pub fn supports_code_execution(model: &str) -> bool {
72 models::google::CODE_EXECUTION_MODELS.contains(&model)
73 }
74
75 pub fn max_input_tokens(model: &str) -> usize {
77 if model.contains("gemini-3.1") {
78 1_048_576 } else if model.contains("3") || model.contains("1.5-pro") {
80 2_097_152 } else {
82 1_048_576 }
84 }
85
86 pub fn max_output_tokens(model: &str) -> usize {
88 if model.contains("3") {
89 65_536 } else {
91 8_192 }
93 }
94
95 pub fn supports_extended_thinking(model: &str) -> bool {
98 model.contains("gemini-3-flash")
99 }
100
101 pub fn supported_thinking_levels(model: &str) -> Vec<&'static str> {
104 if model.contains("gemini-3-flash") {
105 vec!["minimal", "low", "medium", "high"]
107 } else if model.contains("gemini-3") {
108 vec!["low", "high"]
110 } else {
111 vec!["low", "high"]
113 }
114 }
115 pub(super) fn apply_stream_delta(accumulator: &mut String, chunk: &str) -> Option<String> {
116 if chunk.is_empty() {
117 return None;
118 }
119
120 if chunk.starts_with(accumulator.as_str()) {
121 let delta = &chunk[accumulator.len()..];
122 if delta.is_empty() {
123 return None;
124 }
125 accumulator.clear();
126 accumulator.push_str(chunk);
127 return Some(delta.to_string());
128 }
129
130 if accumulator.starts_with(chunk) {
131 accumulator.clear();
132 accumulator.push_str(chunk);
133 return None;
134 }
135
136 accumulator.push_str(chunk);
137 Some(chunk.to_string())
138 }
139
140 pub(super) fn convert_to_gemini_request(
141 &self,
142 request: &LLMRequest,
143 ) -> Result<GenerateContentRequest, LLMError> {
144 if self.prompt_cache_enabled
145 && matches!(
146 self.prompt_cache_settings.mode,
147 GeminiPromptCacheMode::Explicit
148 )
149 {
150 }
154
155 let mut call_map: HashMap<String, String> = HashMap::with_capacity(request.messages.len());
156 for message in &request.messages {
157 if message.role == MessageRole::Assistant
158 && let Some(tool_calls) = &message.tool_calls
159 {
160 for tool_call in tool_calls {
161 if let Some(ref func) = tool_call.function {
162 call_map.insert(tool_call.id.clone(), func.name.clone());
163 }
164 }
165 }
166 }
167
168 let mut contents: Vec<Content> = Vec::with_capacity(request.messages.len());
169 let history_system_directives = collect_history_system_directives(request);
170 for message in &request.messages {
171 if message.role == MessageRole::System {
172 continue;
173 }
174
175 let mut parts: Vec<Part> = preserved_gemini_parts_from_message(message)
176 .unwrap_or_else(|| build_message_parts(message, request.model.as_str()));
177
178 if message.role == MessageRole::Tool {
179 if let Some(tool_call_id) = &message.tool_call_id {
180 let func_name = call_map
181 .get(tool_call_id)
182 .cloned()
183 .unwrap_or_else(|| tool_call_id.clone());
184 let response_text = serde_json::from_str::<Value>(&message.content.as_text())
185 .map(|value| {
186 serde_json::to_string_pretty(&value)
187 .unwrap_or_else(|_| message.content.as_text().into_owned())
188 })
189 .unwrap_or_else(|_| message.content.as_text().into_owned());
190
191 let response_payload = json!({
192 "name": func_name.clone(),
193 "content": [{
194 "text": response_text
195 }]
196 });
197
198 parts.push(Part::FunctionResponse {
199 function_response: FunctionResponse {
200 name: func_name,
201 response: response_payload,
202 id: Some(tool_call_id.clone()),
203 },
204 thought_signature: None, });
206 } else if !message.content.is_empty() {
207 parts.push(Part::Text {
208 text: message.content.as_text().into_owned(),
209 thought_signature: None,
210 });
211 }
212 }
213
214 if !parts.is_empty() {
215 contents.push(Content {
216 role: message.role.as_gemini_str().to_string(),
217 parts,
218 });
219 }
220 }
221
222 let tool_spec = collect_gemini_tool_spec(request.tools.as_deref().map(|v| v.as_slice()));
223 let tools = tool_spec.generate_tools;
224 let uses_server_side_tools = tool_spec.uses_server_side_tools;
225
226 let generation_config = build_generation_config(self, request);
227
228 if let Some(temp) = request.temperature {
230 if Self::is_gemini_3_pro_model(&request.model) && temp < 1.0 {
231 tracing::warn!(
232 "When using Gemini 3 Pro with temperature values below 1.0, be aware that this may cause looping or degraded performance on complex tasks. Consider using 1.0 or higher for optimal results."
233 );
234 }
235 }
236
237 let has_tools = request
238 .tools
239 .as_ref()
240 .map(|defs| !defs.is_empty())
241 .unwrap_or(false);
242 let has_function_tools = tool_spec.has_function_tools;
243 let tool_config = if has_tools || request.tool_choice.is_some() {
244 let function_calling_config = if has_function_tools {
245 Some(match request.tool_choice.as_ref() {
246 Some(ToolChoice::None) => FunctionCallingConfig::none(),
247 Some(ToolChoice::Any) => FunctionCallingConfig::any(),
248 Some(ToolChoice::Specific(spec)) => {
249 let mut config = if uses_server_side_tools {
250 FunctionCallingConfig::validated()
251 } else {
252 FunctionCallingConfig::any()
253 };
254 if spec.tool_type == "function" {
255 config.allowed_function_names = Some(vec![spec.function.name.clone()]);
256 }
257 config
258 }
259 _ => {
260 if uses_server_side_tools {
261 FunctionCallingConfig::validated()
262 } else {
263 FunctionCallingConfig::auto()
264 }
265 }
266 })
267 } else {
268 None
269 };
270
271 Some(ToolConfig {
272 function_calling_config,
273 include_server_side_tool_invocations: uses_server_side_tools.then_some(true),
274 })
275 } else {
276 None
277 };
278
279 Ok(GenerateContentRequest {
280 contents,
281 tools,
282 tool_config,
283 system_instruction: {
284 let base_system_prompt = request
285 .system_prompt
286 .as_ref()
287 .map(|prompt| prompt.as_str())
288 .or_else(|| self.prompt_cache_enabled.then_some(default_system_prompt()));
289 let merged_system_prompt = merge_system_prompt_with_history_directives(
290 base_system_prompt,
291 &history_system_directives,
292 Self::HISTORY_DIRECTIVES_SECTION_HEADER,
293 );
294
295 if self.prompt_cache_enabled
296 && matches!(
297 self.prompt_cache_settings.mode,
298 GeminiPromptCacheMode::Explicit
299 )
300 {
301 if let Some(ttl) = self.prompt_cache_settings.explicit_ttl_seconds {
302 merged_system_prompt.map(|text| SystemInstruction::with_ttl(text, ttl))
303 } else {
304 merged_system_prompt.map(SystemInstruction::new)
305 }
306 } else if request.system_prompt.is_some()
307 || self.prompt_cache_enabled
308 || !history_system_directives.is_empty()
309 {
310 merged_system_prompt.map(SystemInstruction::new)
311 } else {
312 None
313 }
314 },
315 generation_config: Some(generation_config.into()),
316 })
317 }
318
319 pub(super) fn should_use_interactions(&self, request: &LLMRequest) -> bool {
320 if request.previous_response_id.is_some() {
321 return true;
322 }
323
324 request.model.contains("gemini-3")
325 && collect_gemini_tool_spec(request.tools.as_deref().map(|v| v.as_slice()))
326 .uses_server_side_tools
327 }
328
329 pub(super) fn convert_to_interaction_request(
330 &self,
331 request: &LLMRequest,
332 ) -> Result<InteractionRequest, LLMError> {
333 let history_system_directives = collect_history_system_directives(request);
334 let base_system_prompt = request
335 .system_prompt
336 .as_ref()
337 .map(|prompt| prompt.as_str())
338 .or_else(|| self.prompt_cache_enabled.then_some(default_system_prompt()));
339 let merged_system_prompt = merge_system_prompt_with_history_directives(
340 base_system_prompt,
341 &history_system_directives,
342 Self::HISTORY_DIRECTIVES_SECTION_HEADER,
343 );
344
345 let tool_spec = collect_gemini_tool_spec(request.tools.as_deref().map(|v| v.as_slice()));
346 let generation_config = build_generation_config(self, request);
347 let interaction_input = build_interaction_input(request)?;
348
349 Ok(InteractionRequest {
350 model: request.model.clone(),
351 input: interaction_input,
352 tools: tool_spec.interaction_tools,
353 system_instruction: merged_system_prompt,
354 response_format: request.output_format.clone(),
355 response_mime_type: request
356 .output_format
357 .as_ref()
358 .map(|_| "application/json".to_string()),
359 stream: request.stream.then_some(true),
360 store: request.response_store,
361 generation_config: Some(generation_config.into()),
362 tool_choice: build_interaction_tool_choice(
363 request.tool_choice.as_ref(),
364 tool_spec.has_function_tools,
365 tool_spec.uses_server_side_tools,
366 ),
367 previous_interaction_id: request.previous_response_id.clone(),
368 })
369 }
370
371 pub(super) fn convert_from_gemini_response(
372 response: GenerateContentResponse,
373 model: String,
374 ) -> Result<LLMResponse, LLMError> {
375 let mut candidates = response.candidates.into_iter();
376 let candidate = candidates.next().ok_or_else(|| {
377 let formatted_error =
378 error_display::format_llm_error("Gemini", "No candidate in response");
379 LLMError::Provider {
380 message: formatted_error,
381 metadata: None,
382 }
383 })?;
384
385 if candidate.content.parts.is_empty() {
386 return Ok(LLMResponse {
387 content: Some(String::new()),
388 tool_calls: None,
389 model,
390 usage: None,
391 finish_reason: FinishReason::Stop,
392 reasoning: None,
393 reasoning_details: None,
394 tool_references: Vec::new(),
395 request_id: None,
396 organization_id: None,
397 compaction: None,
398 });
399 }
400
401 let raw_parts = candidate.content.parts.clone();
402 let mut text_content = String::new();
403 let mut tool_calls = Vec::new();
404 let mut last_text_thought_signature: Option<String> = None;
408
409 for part in candidate.content.parts {
410 match part {
411 Part::Text {
412 text,
413 thought_signature,
414 } => {
415 text_content.push_str(&text);
416 if thought_signature.is_some() {
417 last_text_thought_signature = thought_signature;
418 }
419 }
420 Part::InlineData { .. } => {}
421 Part::FunctionCall {
422 function_call,
423 thought_signature,
424 } => {
425 let call_id = function_call
426 .id
427 .clone()
428 .unwrap_or_else(|| format!("call_{}", tool_calls.len()));
429
430 let effective_signature =
432 thought_signature.or(last_text_thought_signature.clone());
433
434 tool_calls.push(ToolCall {
435 id: call_id,
436 call_type: "function".to_string(),
437 function: Some(FunctionCall {
438 namespace: None,
439 name: function_call.name,
440 arguments: serde_json::to_string(&function_call.args)
441 .unwrap_or_else(|_| "{}".to_string()),
442 }),
443 text: None,
444 thought_signature: effective_signature,
445 });
446 }
447 Part::FunctionResponse { .. } => {}
448 Part::ToolCall { .. } => {}
449 Part::ToolResponse { .. } => {}
450 Part::ExecutableCode { .. } => {}
451 Part::CodeExecutionResult { .. } => {}
452 Part::CacheControl { .. } => {}
453 }
454 }
455
456 let finish_reason = match candidate.finish_reason.as_deref() {
457 Some("STOP") => FinishReason::Stop,
458 Some("MAX_TOKENS") => FinishReason::Length,
459 Some("SAFETY") => FinishReason::ContentFilter,
460 Some("FUNCTION_CALL") => FinishReason::ToolCalls,
461 Some(other) => FinishReason::Error(other.to_string()),
462 None => FinishReason::Stop,
463 };
464
465 let (cleaned_content, extracted_reasoning) = if !text_content.is_empty() {
466 let (reasoning_segments, cleaned) =
467 crate::llm::providers::split_reasoning_from_text(&text_content);
468 let final_reasoning = if reasoning_segments.is_empty() {
469 None
470 } else {
471 let combined_reasoning: Vec<String> =
472 reasoning_segments.into_iter().map(|s| s.text).collect();
473 let combined_reasoning = combined_reasoning.join("\n");
474 if combined_reasoning.trim().is_empty() {
475 None
476 } else {
477 Some(combined_reasoning)
478 }
479 };
480 let final_content = cleaned.unwrap_or_else(|| text_content.clone());
481 (
482 if final_content.trim().is_empty() {
483 None
484 } else {
485 Some(final_content)
486 },
487 final_reasoning,
488 )
489 } else {
490 (None, None)
491 };
492
493 Ok(LLMResponse {
494 content: cleaned_content,
495 tool_calls: if tool_calls.is_empty() {
496 None
497 } else {
498 Some(tool_calls)
499 },
500 model,
501 usage: None,
502 finish_reason,
503 reasoning: extracted_reasoning,
504 reasoning_details: preserved_gemini_parts_detail(&raw_parts),
505 tool_references: Vec::new(),
506 request_id: None,
507 organization_id: None,
508 compaction: None,
509 })
510 }
511
512 pub(super) fn convert_from_interaction_response(
513 response: Interaction,
514 model: String,
515 ) -> Result<LLMResponse, LLMError> {
516 let mut text_content = String::new();
517 let mut tool_calls = Vec::new();
518 let mut thought_summaries = Vec::new();
519 let mut thought_details = Vec::new();
520
521 for output in response.outputs {
522 match output.output_type.as_str() {
523 "text" => {
524 if let Some(text) = output.text {
525 text_content.push_str(&text);
526 }
527 }
528 "thought" => {
529 let summary = output.summary.or(output.text).unwrap_or_default();
530 if !summary.trim().is_empty() {
531 thought_summaries.push(summary.clone());
532 }
533 thought_details.push(
534 json!({
535 "type": "thought",
536 "signature": output.signature,
537 "summary": summary,
538 })
539 .to_string(),
540 );
541 }
542 "function_call" => {
543 let (name, arguments, id, signature) =
544 if let Some(function_call) = output.function_call {
545 (
546 function_call.name,
547 function_call.arguments,
548 function_call.id.or(output.id),
549 function_call.signature.or(output.signature),
550 )
551 } else {
552 (
553 output.name.unwrap_or_default(),
554 output.arguments.unwrap_or(Value::Null),
555 output.id,
556 output.signature,
557 )
558 };
559
560 let call_id = id.unwrap_or_else(|| format!("call_{}", tool_calls.len()));
561
562 tool_calls.push(ToolCall {
563 id: call_id,
564 call_type: "function".to_string(),
565 function: Some(FunctionCall {
566 namespace: None,
567 name,
568 arguments: serde_json::to_string(&arguments)
569 .unwrap_or_else(|_| "{}".to_string()),
570 }),
571 text: None,
572 thought_signature: signature,
573 });
574 }
575 _ => {}
576 }
577 }
578
579 let finish_reason = if tool_calls.is_empty() {
580 FinishReason::Stop
581 } else {
582 FinishReason::ToolCalls
583 };
584 let (reasoning_segments, cleaned) =
585 crate::llm::providers::split_reasoning_from_text(&text_content);
586 let extracted_reasoning = if reasoning_segments.is_empty() {
587 None
588 } else {
589 Some(
590 reasoning_segments
591 .into_iter()
592 .map(|segment| segment.text)
593 .collect::<Vec<_>>()
594 .join("\n"),
595 )
596 .filter(|value| !value.trim().is_empty())
597 };
598 let content = cleaned
599 .or_else(|| (!text_content.trim().is_empty()).then_some(text_content))
600 .filter(|value| !value.trim().is_empty());
601 let reasoning = if thought_summaries.is_empty() {
602 extracted_reasoning
603 } else {
604 Some(thought_summaries.join("\n"))
605 };
606 let reasoning_details = if thought_details.is_empty() {
607 None
608 } else {
609 Some(thought_details)
610 };
611
612 Ok(LLMResponse {
613 content,
614 tool_calls: (!tool_calls.is_empty()).then_some(tool_calls),
615 model,
616 usage: response.usage.map(|usage| vtcode_commons::llm::Usage {
617 prompt_tokens: usage.total_input_tokens.unwrap_or_default(),
618 completion_tokens: usage.total_output_tokens.unwrap_or_default(),
619 total_tokens: usage.total_tokens.unwrap_or_default(),
620 cached_prompt_tokens: usage.total_cached_tokens,
621 cache_creation_tokens: None,
622 cache_read_tokens: usage.total_cached_tokens,
623 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}