1use crate::attachment;
2use crate::retry::{RetryConfig, execute_with_retry, is_retryable_model_error};
3use adk_core::{
4 CacheCapable, CitationMetadata, CitationSource, Content, ErrorCategory, ErrorComponent,
5 FinishReason, Llm, LlmRequest, LlmResponse, LlmResponseStream, Part, Result, SchemaAdapter,
6 SchemaCache, UsageMetadata,
7};
8use adk_gemini::Gemini;
9use adk_gemini::schema_adapter::GeminiSchemaAdapter;
10use async_trait::async_trait;
11use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
12use futures::TryStreamExt;
13
14#[cfg(feature = "gemini-interactions")]
15use super::interactions_target::InteractionTarget;
16
17#[cfg(feature = "gemini-interactions")]
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum GeminiTransport {
29 #[default]
31 GenerateContent,
32 Interactions,
34}
35
36#[cfg(feature = "gemini-interactions")]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum BackgroundMode {
47 #[default]
51 AgentTargetsOnly,
52 Always,
54 Never,
56}
57
58#[cfg(feature = "gemini-interactions")]
69#[derive(Debug, Clone)]
70pub struct InteractionOptions {
71 pub store: bool,
73 pub stateful: bool,
76 pub background: BackgroundMode,
78 pub poll_interval: std::time::Duration,
80}
81
82#[cfg(feature = "gemini-interactions")]
83impl Default for InteractionOptions {
84 fn default() -> Self {
85 Self {
86 store: true,
87 stateful: true,
88 background: BackgroundMode::AgentTargetsOnly,
89 poll_interval: std::time::Duration::from_secs(1),
90 }
91 }
92}
93
94pub struct GeminiModel {
96 client: Gemini,
97 model_name: String,
98 retry_config: RetryConfig,
99 thinking_config: Option<adk_gemini::ThinkingConfig>,
105 #[cfg(feature = "gemini-interactions")]
109 transport: GeminiTransport,
110 #[cfg(feature = "gemini-interactions")]
113 interaction_target: Option<InteractionTarget>,
114 #[cfg(feature = "gemini-interactions")]
116 interaction_options: InteractionOptions,
117 #[cfg(feature = "gemini-vertex")]
120 vertex_rag_store: Option<adk_gemini::VertexRagStore>,
121 #[cfg(feature = "gemini-vertex")]
124 vertex_backend: bool,
125}
126
127pub(crate) fn gemini_error_to_adk(e: &adk_gemini::ClientError) -> adk_core::AdkError {
129 fn format_error_chain(e: &dyn std::error::Error) -> String {
130 let mut msg = e.to_string();
131 let mut source = e.source();
132 while let Some(s) = source {
133 msg.push_str(": ");
134 msg.push_str(&s.to_string());
135 source = s.source();
136 }
137 msg
138 }
139
140 let message = format_error_chain(e);
141 #[cfg(feature = "gemini-vertex")]
144 let transport_failure =
145 adk_gemini::backend::vertex::VertexBackend::is_transport_error(&message);
146 #[cfg(not(feature = "gemini-vertex"))]
147 let transport_failure = false;
148
149 let (category, code, status_code) = if message.contains("code 429")
152 || message.contains("RESOURCE_EXHAUSTED")
153 || message.contains("rate limit")
154 {
155 (ErrorCategory::RateLimited, "model.gemini.rate_limited", Some(429u16))
156 } else if message.contains("code 503") || message.contains("UNAVAILABLE") {
157 (ErrorCategory::Unavailable, "model.gemini.unavailable", Some(503))
158 } else if message.contains("code 529") || message.contains("OVERLOADED") {
159 (ErrorCategory::Unavailable, "model.gemini.overloaded", Some(529))
160 } else if message.contains("code 408")
161 || message.contains("DEADLINE_EXCEEDED")
162 || message.contains("TIMEOUT")
163 {
164 (ErrorCategory::Timeout, "model.gemini.timeout", Some(408))
165 } else if message.contains("code 401") || message.contains("Invalid API key") {
166 (ErrorCategory::Unauthorized, "model.gemini.unauthorized", Some(401))
167 } else if message.contains("code 400") {
168 (ErrorCategory::InvalidInput, "model.gemini.bad_request", Some(400))
169 } else if message.contains("code 404") {
170 (ErrorCategory::NotFound, "model.gemini.not_found", Some(404))
171 } else if message.contains("invalid generation config") {
172 (ErrorCategory::InvalidInput, "model.gemini.invalid_config", None)
173 } else if transport_failure {
174 (ErrorCategory::Unavailable, "model.gemini.unavailable", None)
175 } else {
176 (ErrorCategory::Internal, "model.gemini.internal", None)
177 };
178
179 let mut err = adk_core::AdkError::new(ErrorComponent::Model, category, code, message)
180 .with_provider("gemini");
181 if let Some(sc) = status_code {
182 err = err.with_upstream_status(sc);
183 }
184 err
185}
186
187#[cfg(feature = "gemini-interactions")]
198fn interaction_status_to_result(status: adk_gemini::interactions::InteractionStatus) -> Result<()> {
199 use adk_gemini::interactions::InteractionStatus;
200 match status {
201 InteractionStatus::Failed => Err(interaction_terminal_error(
202 "model.gemini.interactions.failed",
203 "the Gemini interaction terminated with status `failed`",
204 )),
205 InteractionStatus::BudgetExceeded => Err(interaction_terminal_error(
206 "model.gemini.interactions.budget_exceeded",
207 "the Gemini interaction terminated with status `budget_exceeded`",
208 )),
209 InteractionStatus::InProgress
210 | InteractionStatus::RequiresAction
211 | InteractionStatus::Completed
212 | InteractionStatus::Cancelled
213 | InteractionStatus::Incomplete => Ok(()),
214 }
215}
216
217#[cfg(feature = "gemini-interactions")]
219fn interaction_terminal_error(code: &'static str, message: &str) -> adk_core::AdkError {
220 adk_core::AdkError::new(
221 ErrorComponent::Model,
222 ErrorCategory::Internal,
223 code,
224 message.to_string(),
225 )
226 .with_provider("gemini")
227}
228
229fn env_non_empty(name: &str) -> Option<String> {
232 std::env::var(name).ok().filter(|v| !v.trim().is_empty())
233}
234
235fn env_flag_truthy(value: &str) -> bool {
238 let v = value.trim();
239 v == "1" || v.eq_ignore_ascii_case("true")
240}
241
242pub fn vertex_env_requested() -> bool {
257 if let Some(v) = env_non_empty("GOOGLE_GENAI_USE_ENTERPRISE") {
261 return env_flag_truthy(&v);
262 }
263 env_non_empty("GOOGLE_GENAI_USE_VERTEXAI").is_some_and(|v| env_flag_truthy(&v))
264}
265
266#[cfg(feature = "gemini-vertex")]
269fn vertex_target_from_env() -> Result<(String, String)> {
270 let project = env_non_empty("GOOGLE_CLOUD_PROJECT");
271 let location = env_non_empty("GOOGLE_CLOUD_LOCATION");
272 if let (Some(project), Some(location)) = (&project, &location) {
273 return Ok((project.clone(), location.clone()));
274 }
275 let mut missing = Vec::new();
276 if project.is_none() {
277 missing.push("GOOGLE_CLOUD_PROJECT");
278 }
279 if location.is_none() {
280 missing.push("GOOGLE_CLOUD_LOCATION");
281 }
282 Err(adk_core::AdkError::new(
283 ErrorComponent::Model,
284 ErrorCategory::InvalidInput,
285 "model.gemini.vertex_env_incomplete",
286 format!(
287 "Vertex AI backend selected via GOOGLE_GENAI_USE_ENTERPRISE/GOOGLE_GENAI_USE_VERTEXAI, but {} not set. Set the missing variable(s) to your Google Cloud project ID and region (e.g. us-central1).",
288 missing.join(" and ")
289 ),
290 )
291 .with_provider("gemini"))
292}
293
294impl GeminiModel {
295 fn gemini_part_thought_signature(value: &serde_json::Value) -> Option<String> {
296 value.get("thoughtSignature").and_then(serde_json::Value::as_str).map(str::to_string)
297 }
298
299 fn validate_request_contract(&self, req: &LlmRequest) -> Result<()> {
300 #[cfg(feature = "gemini-vertex")]
304 if self.vertex_rag_store.is_some() && !self.vertex_backend {
305 return Err(adk_core::AdkError::new(
306 ErrorComponent::Model,
307 ErrorCategory::Unsupported,
308 "model.gemini.rag_store_requires_vertex",
309 "Vertex RAG store grounding requires the Vertex AI backend. Construct the model with new_google_cloud/new_google_cloud_adc (or set GOOGLE_GENAI_USE_VERTEXAI with GOOGLE_CLOUD_PROJECT/GOOGLE_CLOUD_LOCATION) instead of an AI Studio API key.",
310 )
311 .with_provider("gemini"));
312 }
313
314 let model = self.model_name.strip_prefix("models/").unwrap_or(&self.model_name);
315 if !matches!(model, "gemini-3.6-flash" | "gemini-3.7-flash") {
316 return Ok(());
317 }
318
319 if let Some(config) = &req.config
320 && (config.temperature.is_some() || config.top_p.is_some() || config.top_k.is_some())
321 {
322 return Err(adk_core::AdkError::new(
323 ErrorComponent::Model,
324 ErrorCategory::InvalidInput,
325 "model.gemini.sampling_unsupported",
326 format!(
327 "{model} does not accept temperature, top_p, or top_k; remove explicit sampling parameters"
328 ),
329 )
330 .with_provider("gemini"));
331 }
332 if self.thinking_config.as_ref().is_some_and(|config| config.thinking_budget.is_some()) {
333 return Err(adk_core::AdkError::new(
334 ErrorComponent::Model,
335 ErrorCategory::InvalidInput,
336 "model.gemini.thinking_budget_unsupported",
337 format!(
338 "{model} uses thinking levels instead of token budgets; set thinking_level and clear thinking_budget"
339 ),
340 )
341 .with_provider("gemini"));
342 }
343 Ok(())
344 }
345
346 pub(crate) fn from_client(client: Gemini, model_name: String) -> Self {
352 Self {
353 client,
354 model_name,
355 retry_config: RetryConfig::default(),
356 thinking_config: None,
357 #[cfg(feature = "gemini-interactions")]
358 transport: GeminiTransport::GenerateContent,
359 #[cfg(feature = "gemini-interactions")]
360 interaction_target: None,
361 #[cfg(feature = "gemini-interactions")]
362 interaction_options: InteractionOptions::default(),
363 #[cfg(feature = "gemini-vertex")]
364 vertex_rag_store: None,
365 #[cfg(feature = "gemini-vertex")]
366 vertex_backend: false,
367 }
368 }
369
370 pub fn new(api_key: impl Into<String>, model: impl Into<String>) -> Result<Self> {
372 let model_name = model.into();
373 crate::catalog::warn_if_obsolete("gemini", &model_name);
374 let client = Gemini::with_model(api_key.into(), model_name.clone())
375 .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
376
377 Ok(Self::from_client(client, model_name))
378 }
379
380 pub fn new_with_base_url(
402 api_key: impl Into<String>,
403 model: impl Into<String>,
404 base_url: impl AsRef<str>,
405 ) -> Result<Self> {
406 let model_name = model.into();
407 let base_url = base_url
408 .as_ref()
409 .parse()
410 .map_err(|_| adk_core::AdkError::model("Invalid Gemini base URL"))?;
411 let client = Gemini::with_model_and_base_url(api_key.into(), model_name.clone(), base_url)
412 .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
413 Ok(Self::from_client(client, model_name))
414 }
415
416 #[cfg(feature = "gemini-vertex")]
420 pub fn new_google_cloud(
421 api_key: impl Into<String>,
422 project_id: impl AsRef<str>,
423 location: impl AsRef<str>,
424 model: impl Into<String>,
425 ) -> Result<Self> {
426 let model_name = model.into();
427 let client = Gemini::with_google_cloud_model(
428 api_key.into(),
429 project_id,
430 location,
431 model_name.clone(),
432 )
433 .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
434
435 let mut model = Self::from_client(client, model_name);
436 model.vertex_backend = true;
437 Ok(model)
438 }
439
440 #[cfg(feature = "gemini-vertex")]
464 pub fn new_google_cloud_endpoint(
465 api_key: Option<&str>,
466 project: &str,
467 location: &str,
468 model_name: &str,
469 endpoint: &str,
470 ) -> Result<Self> {
471 let mut builder = adk_gemini::GeminiBuilder::new(api_key.unwrap_or_default())
472 .with_model(model_name.to_owned())
473 .with_google_cloud(project, location)
474 .with_google_cloud_endpoint(endpoint);
475 if api_key.is_none() {
476 builder = builder.with_google_cloud_adc().map_err(|e| gemini_error_to_adk(&e))?;
477 }
478 let client = builder.build().map_err(|e| gemini_error_to_adk(&e))?;
479 let mut model = Self::from_client(client, model_name.to_owned());
480 model.vertex_backend = true;
481 Ok(model)
482 }
483
484 #[cfg(feature = "gemini-vertex")]
488 pub fn new_google_cloud_service_account(
489 service_account_json: &str,
490 project_id: impl AsRef<str>,
491 location: impl AsRef<str>,
492 model: impl Into<String>,
493 ) -> Result<Self> {
494 let model_name = model.into();
495 let client = Gemini::with_google_cloud_service_account_json(
496 service_account_json,
497 project_id.as_ref(),
498 location.as_ref(),
499 model_name.clone(),
500 )
501 .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
502
503 let mut model = Self::from_client(client, model_name);
504 model.vertex_backend = true;
505 Ok(model)
506 }
507
508 #[cfg(feature = "gemini-vertex")]
512 pub fn new_google_cloud_adc(
513 project_id: impl AsRef<str>,
514 location: impl AsRef<str>,
515 model: impl Into<String>,
516 ) -> Result<Self> {
517 let model_name = model.into();
518 let client = Gemini::with_google_cloud_adc_model(
519 project_id.as_ref(),
520 location.as_ref(),
521 model_name.clone(),
522 )
523 .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
524
525 let mut model = Self::from_client(client, model_name);
526 model.vertex_backend = true;
527 Ok(model)
528 }
529
530 #[cfg(feature = "gemini-vertex")]
534 pub fn new_google_cloud_wif(
535 wif_json: &str,
536 project_id: impl AsRef<str>,
537 location: impl AsRef<str>,
538 model: impl Into<String>,
539 ) -> Result<Self> {
540 let model_name = model.into();
541 let client = Gemini::with_google_cloud_wif_json(
542 wif_json,
543 project_id.as_ref(),
544 location.as_ref(),
545 model_name.clone(),
546 )
547 .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
548
549 let mut model = Self::from_client(client, model_name);
550 model.vertex_backend = true;
551 Ok(model)
552 }
553
554 pub fn from_env(model: &str) -> Result<Self> {
590 if vertex_env_requested() {
591 #[cfg(feature = "gemini-vertex")]
592 {
593 let (project, location) = vertex_target_from_env()?;
594 return Self::new_google_cloud_adc(project, location, model);
595 }
596 #[cfg(not(feature = "gemini-vertex"))]
597 {
598 return Err(adk_core::AdkError::new(
599 ErrorComponent::Model,
600 ErrorCategory::Unsupported,
601 "model.gemini.vertex_feature_missing",
602 "Vertex AI backend selected via GOOGLE_GENAI_USE_ENTERPRISE/GOOGLE_GENAI_USE_VERTEXAI, but the `gemini-vertex` feature is not compiled. Enable the `gemini-vertex` feature (or `gemini-agent-platform` on the adk-rust umbrella crate) to route Gemini traffic through Vertex AI.",
603 )
604 .with_provider("gemini"));
605 }
606 }
607
608 let Some(api_key) =
609 env_non_empty("GOOGLE_API_KEY").or_else(|| env_non_empty("GEMINI_API_KEY"))
610 else {
611 return Err(adk_core::AdkError::new(
612 ErrorComponent::Model,
613 ErrorCategory::InvalidInput,
614 "model.gemini.api_key_missing",
615 "No Gemini API key found. Set GOOGLE_API_KEY or GEMINI_API_KEY, or set GOOGLE_GENAI_USE_ENTERPRISE=true with GOOGLE_CLOUD_PROJECT/GOOGLE_CLOUD_LOCATION for Vertex AI.",
616 )
617 .with_provider("gemini"));
618 };
619 Self::new(api_key, model)
620 }
621
622 #[must_use]
624 pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
625 self.retry_config = retry_config;
626 self
627 }
628
629 pub fn set_retry_config(&mut self, retry_config: RetryConfig) {
631 self.retry_config = retry_config;
632 }
633
634 pub fn retry_config(&self) -> &RetryConfig {
636 &self.retry_config
637 }
638
639 #[must_use]
663 pub fn with_thinking_config(mut self, thinking_config: adk_gemini::ThinkingConfig) -> Self {
664 self.thinking_config = Some(thinking_config);
665 self
666 }
667
668 pub fn set_thinking_config(&mut self, thinking_config: adk_gemini::ThinkingConfig) {
670 self.thinking_config = Some(thinking_config);
671 }
672
673 pub fn thinking_config(&self) -> Option<&adk_gemini::ThinkingConfig> {
675 self.thinking_config.as_ref()
676 }
677
678 #[cfg(feature = "gemini-vertex")]
702 #[must_use]
703 pub fn with_vertex_rag_store(mut self, store: adk_gemini::VertexRagStore) -> Self {
704 self.vertex_rag_store = Some(store);
705 self
706 }
707
708 #[cfg(feature = "gemini-vertex")]
710 pub fn vertex_rag_store(&self) -> Option<&adk_gemini::VertexRagStore> {
711 self.vertex_rag_store.as_ref()
712 }
713
714 #[cfg(feature = "gemini-interactions")]
738 pub fn use_interactions_api(mut self, enabled: bool) -> Result<Self> {
739 if enabled {
740 let target = InteractionTarget::parse(&self.model_name)?;
741 self.transport = GeminiTransport::Interactions;
742 self.interaction_target = Some(target);
743 } else {
744 self.transport = GeminiTransport::GenerateContent;
745 self.interaction_target = None;
746 }
747 Ok(self)
748 }
749
750 #[cfg(feature = "gemini-interactions")]
757 #[must_use]
758 pub fn interaction_options(mut self, opts: InteractionOptions) -> Self {
759 self.interaction_options = opts;
760 self
761 }
762
763 #[cfg(feature = "gemini-interactions")]
767 pub fn transport(&self) -> GeminiTransport {
768 self.transport
769 }
770
771 #[cfg(feature = "gemini-interactions")]
775 pub fn interaction_options_ref(&self) -> &InteractionOptions {
776 &self.interaction_options
777 }
778
779 #[cfg(feature = "gemini-interactions")]
791 fn resolve_background(&self) -> bool {
792 match self.interaction_options.background {
793 BackgroundMode::Always => true,
794 BackgroundMode::Never => false,
795 BackgroundMode::AgentTargetsOnly => {
796 self.interaction_target.as_ref().is_some_and(InteractionTarget::is_agent)
797 }
798 }
799 }
800
801 fn convert_response(resp: &adk_gemini::GenerationResponse) -> Result<LlmResponse> {
802 let mut converted_parts: Vec<Part> = Vec::new();
803
804 if let Some(parts) = resp.candidates.first().and_then(|c| c.content.parts.as_ref()) {
806 for p in parts {
807 match p {
808 adk_gemini::Part::Text { text, thought, thought_signature } => {
809 if thought == &Some(true) {
810 converted_parts.push(Part::Thinking {
811 thinking: text.clone(),
812 signature: thought_signature.clone(),
813 });
814 } else {
815 converted_parts.push(Part::Text { text: text.clone() });
816 }
817 }
818 adk_gemini::Part::InlineData { inline_data } => {
819 let decoded =
820 BASE64_STANDARD.decode(&inline_data.data).map_err(|error| {
821 adk_core::AdkError::model(format!(
822 "failed to decode inline data from gemini response: {error}"
823 ))
824 })?;
825 converted_parts.push(Part::InlineData {
826 mime_type: inline_data.mime_type.clone(),
827 data: decoded,
828 uri: None,
829 annotations: None,
830 });
831 }
832 adk_gemini::Part::FunctionCall { function_call, thought_signature } => {
833 converted_parts.push(Part::FunctionCall {
834 name: function_call.name.clone(),
835 args: function_call.args.clone(),
836 id: function_call.id.clone(),
837 thought_signature: thought_signature.clone(),
838 });
839 }
840 adk_gemini::Part::FunctionResponse { function_response, .. } => {
841 converted_parts.push(Part::FunctionResponse {
842 function_response: adk_core::FunctionResponseData::new(
843 function_response.name.clone(),
844 function_response
845 .response
846 .clone()
847 .unwrap_or(serde_json::Value::Null),
848 ),
849 id: None,
850 annotations: None,
851 });
852 }
853 adk_gemini::Part::ToolCall { .. } | adk_gemini::Part::ExecutableCode { .. } => {
854 if let Ok(value) = serde_json::to_value(p) {
855 converted_parts.push(Part::ServerToolCall { server_tool_call: value });
856 }
857 }
858 adk_gemini::Part::ToolResponse { .. }
859 | adk_gemini::Part::CodeExecutionResult { .. } => {
860 let value = serde_json::to_value(p).unwrap_or(serde_json::Value::Null);
861 converted_parts
862 .push(Part::ServerToolResponse { server_tool_response: value });
863 }
864 adk_gemini::Part::FileData { file_data } => {
865 converted_parts.push(Part::FileData {
866 mime_type: file_data.mime_type.clone(),
867 file_uri: file_data.file_uri.clone(),
868 annotations: None,
869 });
870 }
871 }
872 }
873 }
874
875 let content = if converted_parts.is_empty() {
876 None
877 } else {
878 Some(Content { role: "model".to_string(), parts: converted_parts })
879 };
880 let tool_call_turn = content.as_ref().is_some_and(Content::has_function_calls);
881
882 let usage_metadata = resp.usage_metadata.as_ref().map(|u| UsageMetadata {
883 prompt_token_count: u.prompt_token_count.unwrap_or(0),
884 candidates_token_count: u
886 .candidates_token_count
887 .unwrap_or(0)
888 .saturating_add(u.thoughts_token_count.unwrap_or(0)),
889 total_token_count: u.total_token_count.unwrap_or(0),
890 thinking_token_count: u.thoughts_token_count,
891 cache_read_input_token_count: u.cached_content_token_count,
892 provider_usage: serde_json::to_value(u).ok(),
893 ..Default::default()
894 });
895
896 let finish_reason =
897 resp.candidates.first().and_then(|c| c.finish_reason.as_ref()).map(|fr| match fr {
898 adk_gemini::FinishReason::Stop => FinishReason::Stop,
899 adk_gemini::FinishReason::MaxTokens => FinishReason::MaxTokens,
900 adk_gemini::FinishReason::Safety => FinishReason::Safety,
901 adk_gemini::FinishReason::Recitation => FinishReason::Recitation,
902 _ => FinishReason::Other,
903 });
904
905 let citation_metadata =
906 resp.candidates.first().and_then(|c| c.citation_metadata.as_ref()).map(|meta| {
907 CitationMetadata {
908 citation_sources: meta
909 .citation_sources
910 .iter()
911 .map(|source| CitationSource {
912 uri: source.uri.clone(),
913 title: source.title.clone(),
914 start_index: source.start_index,
915 end_index: source.end_index,
916 license: source.license.clone(),
917 publication_date: source.publication_date.map(|d| d.to_string()),
918 })
919 .collect(),
920 }
921 });
922
923 let provider_metadata = resp
926 .candidates
927 .first()
928 .and_then(|c| c.grounding_metadata.as_ref())
929 .and_then(|g| serde_json::to_value(g).ok());
930
931 Ok(LlmResponse {
932 content,
933 usage_metadata,
934 finish_reason,
935 citation_metadata,
936 partial: false,
937 turn_complete: !tool_call_turn,
938 interrupted: false,
939 error_code: None,
940 error_message: None,
941 provider_metadata,
942 interaction_id: None,
943 })
944 }
945
946 fn gemini_function_response_payload(response: serde_json::Value) -> serde_json::Value {
947 match response {
948 serde_json::Value::Object(_) => response,
950 other => serde_json::json!({ "result": other }),
951 }
952 }
953
954 fn merge_object_value(
955 target: &mut serde_json::Map<String, serde_json::Value>,
956 value: serde_json::Value,
957 ) {
958 if let serde_json::Value::Object(object) = value {
959 for (key, value) in object {
960 target.insert(key, value);
961 }
962 }
963 }
964
965 fn build_gemini_tools(
966 tools: &std::collections::HashMap<String, serde_json::Value>,
967 adapter: &dyn SchemaAdapter,
968 cache: &SchemaCache,
969 ) -> Result<(Vec<adk_gemini::Tool>, adk_gemini::ToolConfig)> {
970 let mut gemini_tools = Vec::new();
971 let mut function_declarations = Vec::new();
972 let mut has_provider_native_tools = false;
973 let mut tool_config_json = serde_json::Map::new();
974
975 for (name, tool_decl) in tools {
976 if let Some(provider_tool) = tool_decl.get("x-adk-gemini-tool") {
977 let tool = serde_json::from_value::<adk_gemini::Tool>(provider_tool.clone())
978 .map_err(|error| {
979 adk_core::AdkError::model(format!(
980 "failed to deserialize Gemini native tool '{name}': {error}"
981 ))
982 })?;
983 has_provider_native_tools = true;
984 gemini_tools.push(tool);
985 } else {
986 let normalized_name = adapter.normalize_tool_name(name);
988
989 let schema =
992 tool_decl.get("parameters").cloned().unwrap_or_else(|| adapter.empty_schema());
993 let normalized_schema = cache.normalize(&schema);
994
995 let description =
997 tool_decl.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
998
999 let mut func_decl_json = serde_json::json!({
1000 "name": normalized_name.as_ref(),
1001 "description": description,
1002 "parameters": normalized_schema,
1003 });
1004
1005 if let Some(response) = tool_decl.get("response") {
1007 func_decl_json["response"] = cache.normalize(response);
1008 }
1009
1010 if let Some(behavior) = tool_decl.get("behavior") {
1012 func_decl_json["behavior"] = behavior.clone();
1013 }
1014
1015 let func_decl =
1016 serde_json::from_value::<adk_gemini::FunctionDeclaration>(func_decl_json)
1017 .map_err(|error| {
1018 adk_core::AdkError::model(format!(
1019 "failed to build Gemini function declaration for '{name}': {error}"
1020 ))
1021 })?;
1022 function_declarations.push(func_decl);
1023 }
1024
1025 if let Some(tool_config) = tool_decl.get("x-adk-gemini-tool-config") {
1026 Self::merge_object_value(&mut tool_config_json, tool_config.clone());
1027 }
1028 }
1029
1030 let has_function_declarations = !function_declarations.is_empty();
1031 if has_function_declarations {
1032 gemini_tools.push(adk_gemini::Tool::with_functions(function_declarations));
1033 }
1034
1035 if has_provider_native_tools {
1036 tool_config_json.insert(
1037 "includeServerSideToolInvocations".to_string(),
1038 serde_json::Value::Bool(true),
1039 );
1040 }
1041
1042 let tool_config = if tool_config_json.is_empty() {
1043 adk_gemini::ToolConfig::default()
1044 } else {
1045 serde_json::from_value::<adk_gemini::ToolConfig>(serde_json::Value::Object(
1046 tool_config_json,
1047 ))
1048 .map_err(|error| {
1049 adk_core::AdkError::model(format!(
1050 "failed to deserialize Gemini tool configuration: {error}"
1051 ))
1052 })?
1053 };
1054
1055 Ok((gemini_tools, tool_config))
1056 }
1057
1058 fn stream_chunks_from_response(
1059 mut response: LlmResponse,
1060 saw_partial_chunk: bool,
1061 ) -> (Vec<LlmResponse>, bool) {
1062 let is_final = response.finish_reason.is_some();
1063
1064 if !is_final {
1065 response.partial = true;
1066 response.turn_complete = false;
1067 return (vec![response], true);
1068 }
1069
1070 response.partial = false;
1071 response.turn_complete =
1072 response.content.as_ref().is_none_or(|content| !content.has_function_calls());
1073
1074 if saw_partial_chunk {
1075 return (vec![response], true);
1076 }
1077
1078 let synthetic_partial = LlmResponse {
1079 content: None,
1080 usage_metadata: None,
1081 finish_reason: None,
1082 citation_metadata: None,
1083 partial: true,
1084 turn_complete: false,
1085 interrupted: false,
1086 error_code: None,
1087 error_message: None,
1088 provider_metadata: None,
1089 interaction_id: None,
1090 };
1091
1092 (vec![synthetic_partial, response], true)
1093 }
1094
1095 async fn generate_content_internal(
1096 &self,
1097 req: LlmRequest,
1098 stream: bool,
1099 ) -> Result<LlmResponseStream> {
1100 let mut builder = self.client.generate_content();
1101
1102 let mut fn_call_signatures: std::collections::HashMap<String, String> =
1107 std::collections::HashMap::new();
1108 for content in &req.contents {
1109 if content.role == "model" {
1110 for part in &content.parts {
1111 if let Part::FunctionCall { name, thought_signature: Some(sig), .. } = part {
1112 fn_call_signatures.insert(name.clone(), sig.clone());
1113 }
1114 }
1115 }
1116 }
1117
1118 for content in &req.contents {
1120 match content.role.as_str() {
1121 "user" => {
1122 let mut gemini_parts = Vec::new();
1124 for part in &content.parts {
1125 match part {
1126 Part::Text { text } => {
1127 gemini_parts.push(adk_gemini::Part::Text {
1128 text: text.clone(),
1129 thought: None,
1130 thought_signature: None,
1131 });
1132 }
1133 Part::Thinking { thinking, signature } => {
1134 gemini_parts.push(adk_gemini::Part::Text {
1135 text: thinking.clone(),
1136 thought: Some(true),
1137 thought_signature: signature.clone(),
1138 });
1139 }
1140 Part::InlineData { data, mime_type, .. } => {
1141 let encoded = attachment::encode_base64(data);
1142 gemini_parts.push(adk_gemini::Part::InlineData {
1143 inline_data: adk_gemini::Blob {
1144 mime_type: mime_type.clone(),
1145 data: encoded,
1146 },
1147 });
1148 }
1149 Part::FileData { mime_type, file_uri, .. } => {
1150 gemini_parts.push(adk_gemini::Part::Text {
1151 text: attachment::file_attachment_to_text(mime_type, file_uri),
1152 thought: None,
1153 thought_signature: None,
1154 });
1155 }
1156 _ => {}
1157 }
1158 }
1159 if !gemini_parts.is_empty() {
1160 let user_content = adk_gemini::Content {
1161 role: Some(adk_gemini::Role::User),
1162 parts: Some(gemini_parts),
1163 };
1164 builder = builder.with_message(adk_gemini::Message {
1165 content: user_content,
1166 role: adk_gemini::Role::User,
1167 });
1168 }
1169 }
1170 "model" => {
1171 let mut gemini_parts = Vec::new();
1173 for part in &content.parts {
1174 match part {
1175 Part::Text { text } => {
1176 gemini_parts.push(adk_gemini::Part::Text {
1177 text: text.clone(),
1178 thought: None,
1179 thought_signature: None,
1180 });
1181 }
1182 Part::Thinking { thinking, signature } => {
1183 gemini_parts.push(adk_gemini::Part::Text {
1184 text: thinking.clone(),
1185 thought: Some(true),
1186 thought_signature: signature.clone(),
1187 });
1188 }
1189 Part::FunctionCall { name, args, thought_signature, id } => {
1190 gemini_parts.push(adk_gemini::Part::FunctionCall {
1191 function_call: adk_gemini::FunctionCall {
1192 name: name.clone(),
1193 args: args.clone(),
1194 id: id.clone(),
1195 thought_signature: None,
1196 },
1197 thought_signature: thought_signature.clone(),
1198 });
1199 }
1200 Part::ServerToolCall { server_tool_call } => {
1201 if let Ok(native_part) = serde_json::from_value::<adk_gemini::Part>(
1202 server_tool_call.clone(),
1203 ) {
1204 match native_part {
1205 adk_gemini::Part::ToolCall { .. }
1206 | adk_gemini::Part::ExecutableCode { .. } => {
1207 gemini_parts.push(native_part);
1208 continue;
1209 }
1210 _ => {}
1211 }
1212 }
1213
1214 gemini_parts.push(adk_gemini::Part::ToolCall {
1215 tool_call: server_tool_call.clone(),
1216 thought_signature: Self::gemini_part_thought_signature(
1217 server_tool_call,
1218 ),
1219 });
1220 }
1221 Part::ServerToolResponse { server_tool_response } => {
1222 if let Ok(native_part) = serde_json::from_value::<adk_gemini::Part>(
1223 server_tool_response.clone(),
1224 ) {
1225 match native_part {
1226 adk_gemini::Part::ToolResponse { .. }
1227 | adk_gemini::Part::CodeExecutionResult { .. } => {
1228 gemini_parts.push(native_part);
1229 continue;
1230 }
1231 _ => {}
1232 }
1233 }
1234
1235 gemini_parts.push(adk_gemini::Part::ToolResponse {
1236 tool_response: server_tool_response.clone(),
1237 thought_signature: Self::gemini_part_thought_signature(
1238 server_tool_response,
1239 ),
1240 });
1241 }
1242 _ => {}
1243 }
1244 }
1245 if !gemini_parts.is_empty() {
1246 let model_content = adk_gemini::Content {
1247 role: Some(adk_gemini::Role::Model),
1248 parts: Some(gemini_parts),
1249 };
1250 builder = builder.with_message(adk_gemini::Message {
1251 content: model_content,
1252 role: adk_gemini::Role::Model,
1253 });
1254 }
1255 }
1256 "function" => {
1257 let mut gemini_parts = Vec::new();
1260 for part in &content.parts {
1261 if let Part::FunctionResponse { function_response, id, .. } = part {
1262 let sig = fn_call_signatures.get(&function_response.name).cloned();
1263
1264 let mut fr_parts = Vec::new();
1266 for inline in &function_response.inline_data {
1267 let encoded = attachment::encode_base64(&inline.data);
1268 fr_parts.push(adk_gemini::FunctionResponsePart::InlineData {
1269 inline_data: adk_gemini::Blob {
1270 mime_type: inline.mime_type.clone(),
1271 data: encoded,
1272 },
1273 });
1274 }
1275 for file in &function_response.file_data {
1276 fr_parts.push(adk_gemini::FunctionResponsePart::FileData {
1277 file_data: adk_gemini::FileDataRef {
1278 mime_type: file.mime_type.clone(),
1279 file_uri: file.file_uri.clone(),
1280 },
1281 });
1282 }
1283
1284 let mut gemini_fr = adk_gemini::tools::FunctionResponse::new(
1285 &function_response.name,
1286 Self::gemini_function_response_payload(
1287 function_response.response.clone(),
1288 ),
1289 );
1290 gemini_fr.parts = fr_parts;
1291 gemini_fr.id = id.clone();
1294
1295 gemini_parts.push(adk_gemini::Part::FunctionResponse {
1296 function_response: gemini_fr,
1297 thought_signature: sig,
1298 });
1299 }
1300 }
1301 if !gemini_parts.is_empty() {
1302 let fn_content = adk_gemini::Content {
1303 role: Some(adk_gemini::Role::User),
1304 parts: Some(gemini_parts),
1305 };
1306 builder = builder.with_message(adk_gemini::Message {
1307 content: fn_content,
1308 role: adk_gemini::Role::User,
1309 });
1310 }
1311 }
1312 _ => {}
1313 }
1314 }
1315
1316 if let Some(config) = req.config {
1318 let has_schema = config.response_schema.is_some();
1319 let gen_config = adk_gemini::GenerationConfig {
1320 temperature: config.temperature,
1321 top_p: config.top_p,
1322 top_k: config.top_k,
1323 max_output_tokens: config.max_output_tokens,
1324 response_schema: config.response_schema,
1325 response_mime_type: if has_schema {
1326 Some("application/json".to_string())
1327 } else {
1328 None
1329 },
1330 thinking_config: self.thinking_config.clone(),
1331 ..Default::default()
1332 };
1333 builder = builder.with_generation_config(gen_config);
1334
1335 if let Some(ref name) = config.cached_content {
1337 let handle = self.client.get_cached_content(name);
1338 builder = builder.with_cached_content(&handle);
1339 }
1340 } else if self.thinking_config.is_some() {
1341 let gen_config = adk_gemini::GenerationConfig {
1344 thinking_config: self.thinking_config.clone(),
1345 ..Default::default()
1346 };
1347 builder = builder.with_generation_config(gen_config);
1348 }
1349
1350 if !req.tools.is_empty() {
1352 let adapter = self.schema_adapter();
1353 use std::sync::LazyLock;
1354 static SCHEMA_CACHE: LazyLock<SchemaCache> = LazyLock::new(|| {
1355 SchemaCache::for_adapter(std::sync::Arc::new(GeminiSchemaAdapter::new()))
1356 });
1357 let (gemini_tools, tool_config) =
1358 Self::build_gemini_tools(&req.tools, adapter, &SCHEMA_CACHE)?;
1359 for tool in gemini_tools {
1360 builder = builder.with_tool(tool);
1361 }
1362 if tool_config != adk_gemini::ToolConfig::default() {
1363 builder = builder.with_tool_config(tool_config);
1364 }
1365 }
1366
1367 #[cfg(feature = "gemini-vertex")]
1371 if let Some(store) = &self.vertex_rag_store {
1372 builder = builder.with_vertex_rag_store(store.clone());
1373 }
1374
1375 if stream {
1376 adk_telemetry::debug!("Executing streaming request");
1377 let response_stream = builder.execute_stream().await.map_err(|e| {
1378 adk_telemetry::error!(error = %e, "Model request failed");
1379 gemini_error_to_adk(&e)
1380 })?;
1381
1382 let mapped_stream = async_stream::stream! {
1383 let mut stream = response_stream;
1384 let mut saw_partial_chunk = false;
1385 while let Some(result) = stream.try_next().await.transpose() {
1386 match result {
1387 Ok(resp) => {
1388 match Self::convert_response(&resp) {
1389 Ok(llm_resp) => {
1390 let (chunks, next_saw_partial) =
1391 Self::stream_chunks_from_response(llm_resp, saw_partial_chunk);
1392 saw_partial_chunk = next_saw_partial;
1393 for chunk in chunks {
1394 yield Ok(chunk);
1395 }
1396 }
1397 Err(e) => {
1398 adk_telemetry::error!(error = %e, "Failed to convert response");
1399 yield Err(e);
1400 }
1401 }
1402 }
1403 Err(e) => {
1404 adk_telemetry::error!(error = %e, "Stream error");
1405 yield Err(gemini_error_to_adk(&e));
1406 }
1407 }
1408 }
1409 };
1410
1411 Ok(Box::pin(mapped_stream))
1412 } else {
1413 adk_telemetry::debug!("Executing blocking request");
1414 let response = builder.execute().await.map_err(|e| {
1415 adk_telemetry::error!(error = %e, "Model request failed");
1416 gemini_error_to_adk(&e)
1417 })?;
1418
1419 let llm_response = Self::convert_response(&response)?;
1420
1421 let stream = async_stream::stream! {
1422 yield Ok(llm_response);
1423 };
1424
1425 Ok(Box::pin(stream))
1426 }
1427 }
1428
1429 pub async fn create_cached_content(
1434 &self,
1435 system_instruction: &str,
1436 tools: &std::collections::HashMap<String, serde_json::Value>,
1437 ttl_seconds: u32,
1438 ) -> Result<String> {
1439 let mut cache_builder = self
1440 .client
1441 .create_cache()
1442 .with_system_instruction(system_instruction)
1443 .with_ttl(std::time::Duration::from_secs(u64::from(ttl_seconds)));
1444
1445 let adapter = self.schema_adapter();
1446 use std::sync::LazyLock;
1447 static SCHEMA_CACHE: LazyLock<SchemaCache> = LazyLock::new(|| {
1448 SchemaCache::for_adapter(std::sync::Arc::new(GeminiSchemaAdapter::new()))
1449 });
1450 let (gemini_tools, tool_config) = Self::build_gemini_tools(tools, adapter, &SCHEMA_CACHE)?;
1451 if !gemini_tools.is_empty() {
1452 cache_builder = cache_builder.with_tools(gemini_tools);
1453 }
1454 if tool_config != adk_gemini::ToolConfig::default() {
1455 cache_builder = cache_builder.with_tool_config(tool_config);
1456 }
1457
1458 let handle = cache_builder
1459 .execute()
1460 .await
1461 .map_err(|e| adk_core::AdkError::model(format!("cache creation failed: {e}")))?;
1462
1463 Ok(handle.name().to_string())
1464 }
1465
1466 pub async fn delete_cached_content(&self, name: &str) -> Result<()> {
1468 let handle = self.client.get_cached_content(name);
1469 handle
1470 .delete()
1471 .await
1472 .map_err(|(_, e)| adk_core::AdkError::model(format!("cache deletion failed: {e}")))?;
1473 Ok(())
1474 }
1475
1476 #[cfg(feature = "gemini-interactions")]
1504 async fn generate_interactions_once(&self, req: LlmRequest) -> Result<LlmResponse> {
1505 use super::interactions_convert;
1506
1507 let target = self.interaction_target.as_ref().ok_or_else(|| {
1510 adk_core::AdkError::new(
1511 ErrorComponent::Model,
1512 ErrorCategory::InvalidInput,
1513 "model.gemini.interactions.missing_target",
1514 "the Interactions transport is active but no validated target is configured",
1515 )
1516 .with_provider("gemini")
1517 })?;
1518
1519 let thinking_level = self.thinking_config.as_ref().and_then(|c| c.thinking_level);
1522
1523 let stateful = self.interaction_options.stateful;
1524 let store = self.interaction_options.store;
1525 let background = self.resolve_background();
1526
1527 let mut request =
1529 interactions_convert::build_request(&req, target, thinking_level, stateful, store)?;
1530 request.background = Some(background);
1531
1532 let interaction = match self.client.send_interaction(request.clone()).await {
1534 Ok(interaction) => interaction,
1535 Err(error) => {
1536 let mapped = gemini_error_to_adk(&error);
1537 if mapped.category == ErrorCategory::NotFound && req.previous_response_id.is_some()
1541 {
1542 let mut fallback = interactions_convert::build_request(
1543 &req,
1544 target,
1545 thinking_level,
1546 false,
1547 store,
1548 )?;
1549 fallback.background = Some(background);
1550 self.client
1551 .send_interaction(fallback)
1552 .await
1553 .map_err(|e| gemini_error_to_adk(&e))?
1554 } else {
1555 return Err(mapped);
1556 }
1557 }
1558 };
1559
1560 let final_interaction = if background {
1562 self.poll_interaction_to_completion(interaction).await?
1563 } else {
1564 interaction
1565 };
1566
1567 interaction_status_to_result(final_interaction.status)?;
1569
1570 Ok(interactions_convert::to_llm_response(&final_interaction))
1571 }
1572
1573 #[cfg(feature = "gemini-interactions")]
1604 async fn poll_interaction_to_completion(
1605 &self,
1606 interaction: adk_gemini::interactions::Interaction,
1607 ) -> Result<adk_gemini::interactions::Interaction> {
1608 const MAX_POLL_ATTEMPTS: u32 = 600;
1613
1614 let mut current = interaction;
1615 let mut attempts: u32 = 0;
1616 while !current.status.is_terminal() && !current.status.requires_action() {
1617 if attempts >= MAX_POLL_ATTEMPTS {
1618 return Err(adk_core::AdkError::new(
1619 ErrorComponent::Model,
1620 ErrorCategory::Timeout,
1621 "model.gemini.interactions.poll_timeout",
1622 format!(
1623 "the Gemini interaction did not reach a terminal or requires_action \
1624 state after {MAX_POLL_ATTEMPTS} poll attempts"
1625 ),
1626 )
1627 .with_provider("gemini"));
1628 }
1629 attempts += 1;
1630 tokio::time::sleep(self.interaction_options.poll_interval).await;
1631 current = self
1632 .client
1633 .get_interaction(¤t.id, false)
1634 .await
1635 .map_err(|e| gemini_error_to_adk(&e))?;
1636 }
1637 Ok(current)
1638 }
1639
1640 #[cfg(feature = "gemini-interactions")]
1665 async fn generate_interactions_stream(&self, req: LlmRequest) -> Result<LlmResponseStream> {
1666 use super::interactions_convert::{self, SseAccumulator, sse_event_to_chunk};
1667
1668 let target = self.interaction_target.as_ref().ok_or_else(|| {
1669 adk_core::AdkError::new(
1670 ErrorComponent::Model,
1671 ErrorCategory::InvalidInput,
1672 "model.gemini.interactions.missing_target",
1673 "the Interactions transport is active but no validated target is configured",
1674 )
1675 .with_provider("gemini")
1676 })?;
1677
1678 let thinking_level = self.thinking_config.as_ref().and_then(|c| c.thinking_level);
1679 let stateful = self.interaction_options.stateful;
1680 let store = self.interaction_options.store;
1681
1682 let mut request =
1683 interactions_convert::build_request(&req, target, thinking_level, stateful, store)?;
1684 request.background = Some(false);
1689
1690 let sse_stream = self
1691 .client
1692 .send_interaction_stream(request)
1693 .await
1694 .map_err(|e| gemini_error_to_adk(&e))?;
1695
1696 let mapped = async_stream::stream! {
1697 let mut sse_stream = sse_stream;
1698 let mut acc = SseAccumulator::new();
1699 while let Some(result) = sse_stream.try_next().await.transpose() {
1700 match result {
1701 Ok(event) => {
1702 if let Some(chunk) = sse_event_to_chunk(event, &mut acc) {
1703 yield chunk;
1704 }
1705 }
1706 Err(e) => {
1707 adk_telemetry::error!(error = %e, "Interaction stream error");
1708 yield Err(gemini_error_to_adk(&e));
1709 }
1710 }
1711 }
1712 };
1713
1714 Ok(Box::pin(mapped))
1715 }
1716}
1717
1718#[async_trait]
1719impl Llm for GeminiModel {
1720 fn name(&self) -> &str {
1721 &self.model_name
1722 }
1723
1724 fn schema_adapter(&self) -> &dyn SchemaAdapter {
1725 use std::sync::LazyLock;
1726 static ADAPTER: LazyLock<GeminiSchemaAdapter> = LazyLock::new(GeminiSchemaAdapter::new);
1727 &*ADAPTER
1728 }
1729
1730 #[cfg(feature = "gemini-interactions")]
1731 fn uses_interactions_api(&self) -> bool {
1732 self.transport == GeminiTransport::Interactions
1733 }
1734
1735 #[adk_telemetry::instrument(
1740 name = "model.generate_content",
1741 skip(self, req),
1742 fields(
1743 model.name = %self.model_name,
1744 stream = %stream,
1745 request.contents_count = %req.contents.len(),
1746 request.tools_count = %req.tools.len()
1747 )
1748 )]
1749 async fn generate_content(&self, req: LlmRequest, stream: bool) -> Result<LlmResponseStream> {
1750 adk_telemetry::info!("Generating content");
1751 self.validate_request_contract(&req)?;
1752 let usage_span = adk_telemetry::llm_generate_span("gemini", &self.model_name, stream);
1753
1754 #[cfg(feature = "gemini-interactions")]
1762 if self.transport == GeminiTransport::Interactions {
1763 if stream {
1772 let mapped =
1773 execute_with_retry(&self.retry_config, is_retryable_model_error, || {
1774 self.generate_interactions_stream(req.clone())
1775 })
1776 .await?;
1777 return Ok(crate::usage_tracking::with_usage_tracking(mapped, usage_span));
1778 }
1779 let response = execute_with_retry(&self.retry_config, is_retryable_model_error, || {
1780 self.generate_interactions_once(req.clone())
1781 })
1782 .await?;
1783 let single = async_stream::stream! {
1784 yield Ok(response);
1785 };
1786 return Ok(crate::usage_tracking::with_usage_tracking(Box::pin(single), usage_span));
1787 }
1788
1789 let result = execute_with_retry(&self.retry_config, is_retryable_model_error, || {
1790 self.generate_content_internal(req.clone(), stream)
1791 })
1792 .await?;
1793 Ok(crate::usage_tracking::with_usage_tracking(result, usage_span))
1794 }
1795}
1796
1797#[cfg(test)]
1798mod native_tool_tests {
1799 use super::*;
1800
1801 fn test_adapter() -> GeminiSchemaAdapter {
1802 GeminiSchemaAdapter::new()
1803 }
1804
1805 fn test_cache() -> SchemaCache {
1806 SchemaCache::for_adapter(std::sync::Arc::new(GeminiSchemaAdapter::new()))
1807 }
1808
1809 #[test]
1810 fn test_build_gemini_tools_supports_native_tool_metadata() {
1811 let mut tools = std::collections::HashMap::new();
1812 tools.insert(
1813 "google_search".to_string(),
1814 serde_json::json!({
1815 "x-adk-gemini-tool": {
1816 "google_search": {}
1817 }
1818 }),
1819 );
1820 tools.insert(
1821 "lookup_weather".to_string(),
1822 serde_json::json!({
1823 "name": "lookup_weather",
1824 "description": "lookup weather",
1825 "parameters": {
1826 "type": "object",
1827 "properties": {
1828 "city": { "type": "string" }
1829 }
1830 }
1831 }),
1832 );
1833
1834 let adapter = test_adapter();
1835 let cache = test_cache();
1836 let (gemini_tools, tool_config) = GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1837 .expect("tool conversion should succeed");
1838
1839 assert_eq!(gemini_tools.len(), 2);
1840 assert_eq!(tool_config.include_server_side_tool_invocations, Some(true));
1841 }
1842
1843 #[test]
1844 fn test_build_gemini_tools_sets_flag_for_builtin_only() {
1845 let mut tools = std::collections::HashMap::new();
1846 tools.insert(
1847 "google_search".to_string(),
1848 serde_json::json!({
1849 "x-adk-gemini-tool": {
1850 "google_search": {}
1851 }
1852 }),
1853 );
1854
1855 let adapter = test_adapter();
1856 let cache = test_cache();
1857 let (_gemini_tools, tool_config) =
1858 GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1859 .expect("tool conversion should succeed");
1860
1861 assert_eq!(
1862 tool_config.include_server_side_tool_invocations,
1863 Some(true),
1864 "includeServerSideToolInvocations should be set even with only built-in tools"
1865 );
1866 }
1867
1868 #[test]
1869 fn test_build_gemini_tools_no_flag_for_function_only() {
1870 let mut tools = std::collections::HashMap::new();
1871 tools.insert(
1872 "lookup_weather".to_string(),
1873 serde_json::json!({
1874 "name": "lookup_weather",
1875 "description": "lookup weather",
1876 "parameters": {
1877 "type": "object",
1878 "properties": {
1879 "city": { "type": "string" }
1880 }
1881 }
1882 }),
1883 );
1884
1885 let adapter = test_adapter();
1886 let cache = test_cache();
1887 let (_gemini_tools, tool_config) =
1888 GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1889 .expect("tool conversion should succeed");
1890
1891 assert_eq!(
1892 tool_config.include_server_side_tool_invocations, None,
1893 "includeServerSideToolInvocations should NOT be set for function-only tools"
1894 );
1895 }
1896
1897 #[test]
1898 fn test_build_gemini_tools_merges_native_tool_config() {
1899 let mut tools = std::collections::HashMap::new();
1900 tools.insert(
1901 "google_maps".to_string(),
1902 serde_json::json!({
1903 "x-adk-gemini-tool": {
1904 "google_maps": {
1905 "enable_widget": true
1906 }
1907 },
1908 "x-adk-gemini-tool-config": {
1909 "retrievalConfig": {
1910 "latLng": {
1911 "latitude": 1.23,
1912 "longitude": 4.56
1913 }
1914 }
1915 }
1916 }),
1917 );
1918
1919 let adapter = test_adapter();
1920 let cache = test_cache();
1921 let (_gemini_tools, tool_config) =
1922 GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1923 .expect("tool conversion should succeed");
1924
1925 assert_eq!(
1926 tool_config.retrieval_config,
1927 Some(serde_json::json!({
1928 "latLng": {
1929 "latitude": 1.23,
1930 "longitude": 4.56
1931 }
1932 }))
1933 );
1934 }
1935
1936 #[test]
1937 fn test_response_schema_is_normalized_like_parameters() {
1938 let mut tools = std::collections::HashMap::new();
1944 tools.insert(
1945 "read_file".to_string(),
1946 serde_json::json!({
1947 "name": "read_file",
1948 "description": "Read a file from the filesystem",
1949 "parameters": {
1950 "$schema": "http://json-schema.org/draft-07/schema#",
1951 "type": "object",
1952 "properties": {
1953 "path": { "type": "string", "description": "File path" }
1954 },
1955 "required": ["path"],
1956 "additionalProperties": false
1957 },
1958 "response": {
1959 "$schema": "http://json-schema.org/draft-07/schema#",
1960 "type": "object",
1961 "properties": {
1962 "content": { "type": "string", "description": "File contents" }
1963 },
1964 "required": ["content"],
1965 "additionalProperties": false
1966 }
1967 }),
1968 );
1969
1970 let adapter = test_adapter();
1971 let cache = test_cache();
1972 let (gemini_tools, _) = GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1973 .expect("tool conversion should succeed");
1974
1975 let func_tool = gemini_tools
1977 .iter()
1978 .find(|t| matches!(t, adk_gemini::Tool::Function { .. }))
1979 .expect("should have function declarations");
1980
1981 let decls = match func_tool {
1982 adk_gemini::Tool::Function { function_declarations } => function_declarations,
1983 _ => panic!("expected Function tool"),
1984 };
1985
1986 let decl = &decls[0];
1987 let decl_json = serde_json::to_value(decl).unwrap();
1988
1989 let params = &decl_json["parameters"];
1991 assert!(params.get("$schema").is_none(), "parameters.$schema should be stripped");
1992 assert!(
1993 params.get("additionalProperties").is_none(),
1994 "parameters.additionalProperties should be stripped"
1995 );
1996
1997 let response = &decl_json["response"];
1999 assert!(
2000 response.get("$schema").is_none(),
2001 "response.$schema should be stripped (was the bug: copied raw)"
2002 );
2003 assert!(
2004 response.get("additionalProperties").is_none(),
2005 "response.additionalProperties should be stripped (was the bug: copied raw)"
2006 );
2007 }
2008}
2009
2010#[async_trait]
2011impl CacheCapable for GeminiModel {
2012 async fn create_cache(
2013 &self,
2014 system_instruction: &str,
2015 tools: &std::collections::HashMap<String, serde_json::Value>,
2016 ttl_seconds: u32,
2017 ) -> Result<String> {
2018 self.create_cached_content(system_instruction, tools, ttl_seconds).await
2019 }
2020
2021 async fn delete_cache(&self, name: &str) -> Result<()> {
2022 self.delete_cached_content(name).await
2023 }
2024}
2025
2026#[cfg(test)]
2027mod tests {
2028 use super::*;
2029 use adk_core::AdkError;
2030 use std::{
2031 sync::{
2032 Arc,
2033 atomic::{AtomicU32, Ordering},
2034 },
2035 time::Duration,
2036 };
2037
2038 #[test]
2039 fn env_flag_truthy_accepts_one_and_case_insensitive_true() {
2040 for truthy in ["1", "true", "TRUE", "True", " true ", "tRuE"] {
2041 assert!(env_flag_truthy(truthy), "expected {truthy:?} to be truthy");
2042 }
2043 for falsy in ["0", "false", "FALSE", "yes", "on", "", "2", "vertex"] {
2044 assert!(!env_flag_truthy(falsy), "expected {falsy:?} to be falsy");
2045 }
2046 }
2047
2048 #[test]
2049 fn constructor_is_backward_compatible_and_sync() {
2050 fn accepts_sync_constructor<F>(_f: F)
2051 where
2052 F: Fn(&str, &str) -> Result<GeminiModel>,
2053 {
2054 }
2055
2056 accepts_sync_constructor(|api_key, model| GeminiModel::new(api_key, model));
2057 }
2058
2059 #[test]
2060 fn stream_chunks_from_response_injects_partial_before_lone_final_chunk() {
2061 let response = LlmResponse {
2062 content: Some(Content::new("model").with_text("hello")),
2063 usage_metadata: None,
2064 finish_reason: Some(FinishReason::Stop),
2065 citation_metadata: None,
2066 partial: false,
2067 turn_complete: true,
2068 interrupted: false,
2069 error_code: None,
2070 error_message: None,
2071 provider_metadata: None,
2072 interaction_id: None,
2073 };
2074
2075 let (chunks, saw_partial) = GeminiModel::stream_chunks_from_response(response, false);
2076 assert!(saw_partial);
2077 assert_eq!(chunks.len(), 2);
2078 assert!(chunks[0].partial);
2079 assert!(!chunks[0].turn_complete);
2080 assert!(chunks[0].content.is_none());
2081 assert!(!chunks[1].partial);
2082 assert!(chunks[1].turn_complete);
2083 }
2084
2085 #[test]
2086 fn stream_chunks_from_response_keeps_final_only_when_partial_already_seen() {
2087 let response = LlmResponse {
2088 content: Some(Content::new("model").with_text("done")),
2089 usage_metadata: None,
2090 finish_reason: Some(FinishReason::Stop),
2091 citation_metadata: None,
2092 partial: false,
2093 turn_complete: true,
2094 interrupted: false,
2095 error_code: None,
2096 error_message: None,
2097 provider_metadata: None,
2098 interaction_id: None,
2099 };
2100
2101 let (chunks, saw_partial) = GeminiModel::stream_chunks_from_response(response, true);
2102 assert!(saw_partial);
2103 assert_eq!(chunks.len(), 1);
2104 assert!(!chunks[0].partial);
2105 assert!(chunks[0].turn_complete);
2106 }
2107
2108 #[test]
2109 fn stream_chunks_from_response_keeps_tool_call_turn_open() {
2110 let response = LlmResponse {
2111 content: Some(Content {
2112 role: "model".to_string(),
2113 parts: vec![Part::FunctionCall {
2114 name: "get_weather".to_string(),
2115 args: serde_json::json!({"city": "Boston"}),
2116 id: Some("call-1".to_string()),
2117 thought_signature: None,
2118 }],
2119 }),
2120 finish_reason: Some(FinishReason::Stop),
2121 turn_complete: true,
2122 ..Default::default()
2123 };
2124
2125 let (chunks, _) = GeminiModel::stream_chunks_from_response(response, true);
2126
2127 assert_eq!(chunks.len(), 1);
2128 assert!(!chunks[0].partial);
2129 assert!(!chunks[0].turn_complete);
2130 }
2131
2132 #[tokio::test]
2133 async fn execute_with_retry_retries_retryable_errors() {
2134 let retry_config = RetryConfig::default()
2135 .with_max_retries(2)
2136 .with_initial_delay(Duration::from_millis(0))
2137 .with_max_delay(Duration::from_millis(0));
2138 let attempts = Arc::new(AtomicU32::new(0));
2139
2140 let result = execute_with_retry(&retry_config, is_retryable_model_error, || {
2141 let attempts = Arc::clone(&attempts);
2142 async move {
2143 let attempt = attempts.fetch_add(1, Ordering::SeqCst);
2144 if attempt < 2 {
2145 return Err(AdkError::model("code 429 RESOURCE_EXHAUSTED"));
2146 }
2147 Ok("ok")
2148 }
2149 })
2150 .await
2151 .expect("retry should eventually succeed");
2152
2153 assert_eq!(result, "ok");
2154 assert_eq!(attempts.load(Ordering::SeqCst), 3);
2155 }
2156
2157 #[tokio::test]
2158 async fn execute_with_retry_does_not_retry_non_retryable_errors() {
2159 let retry_config = RetryConfig::default()
2160 .with_max_retries(3)
2161 .with_initial_delay(Duration::from_millis(0))
2162 .with_max_delay(Duration::from_millis(0));
2163 let attempts = Arc::new(AtomicU32::new(0));
2164
2165 let error = execute_with_retry(&retry_config, is_retryable_model_error, || {
2166 let attempts = Arc::clone(&attempts);
2167 async move {
2168 attempts.fetch_add(1, Ordering::SeqCst);
2169 Err::<(), _>(AdkError::model("code 400 invalid request"))
2170 }
2171 })
2172 .await
2173 .expect_err("non-retryable error should return immediately");
2174
2175 assert!(error.is_model());
2176 assert_eq!(attempts.load(Ordering::SeqCst), 1);
2177 }
2178
2179 #[tokio::test]
2180 async fn execute_with_retry_respects_disabled_config() {
2181 let retry_config = RetryConfig::disabled().with_max_retries(10);
2182 let attempts = Arc::new(AtomicU32::new(0));
2183
2184 let error = execute_with_retry(&retry_config, is_retryable_model_error, || {
2185 let attempts = Arc::clone(&attempts);
2186 async move {
2187 attempts.fetch_add(1, Ordering::SeqCst);
2188 Err::<(), _>(AdkError::model("code 429 RESOURCE_EXHAUSTED"))
2189 }
2190 })
2191 .await
2192 .expect_err("disabled retries should return first error");
2193
2194 assert!(error.is_model());
2195 assert_eq!(attempts.load(Ordering::SeqCst), 1);
2196 }
2197
2198 #[test]
2199 fn convert_response_preserves_citation_metadata() {
2200 let response = adk_gemini::GenerationResponse {
2201 candidates: vec![adk_gemini::Candidate {
2202 content: adk_gemini::Content {
2203 role: Some(adk_gemini::Role::Model),
2204 parts: Some(vec![adk_gemini::Part::Text {
2205 text: "hello world".to_string(),
2206 thought: None,
2207 thought_signature: None,
2208 }]),
2209 },
2210 safety_ratings: None,
2211 citation_metadata: Some(adk_gemini::CitationMetadata {
2212 citation_sources: vec![adk_gemini::CitationSource {
2213 uri: Some("https://example.com".to_string()),
2214 title: Some("Example".to_string()),
2215 start_index: Some(0),
2216 end_index: Some(5),
2217 license: Some("CC-BY".to_string()),
2218 publication_date: None,
2219 }],
2220 }),
2221 grounding_metadata: None,
2222 finish_reason: Some(adk_gemini::FinishReason::Stop),
2223 index: Some(0),
2224 }],
2225 prompt_feedback: None,
2226 usage_metadata: None,
2227 model_version: None,
2228 response_id: None,
2229 };
2230
2231 let converted =
2232 GeminiModel::convert_response(&response).expect("conversion should succeed");
2233 let metadata = converted.citation_metadata.expect("citation metadata should be mapped");
2234 assert_eq!(metadata.citation_sources.len(), 1);
2235 assert_eq!(metadata.citation_sources[0].uri.as_deref(), Some("https://example.com"));
2236 assert_eq!(metadata.citation_sources[0].start_index, Some(0));
2237 assert_eq!(metadata.citation_sources[0].end_index, Some(5));
2238 }
2239
2240 #[test]
2241 fn convert_response_keeps_function_call_turn_open() {
2242 let response = adk_gemini::GenerationResponse {
2243 candidates: vec![adk_gemini::Candidate {
2244 content: adk_gemini::Content {
2245 role: Some(adk_gemini::Role::Model),
2246 parts: Some(vec![adk_gemini::Part::FunctionCall {
2247 function_call: adk_gemini::FunctionCall {
2248 name: "get_weather".to_string(),
2249 args: serde_json::json!({"city": "Boston"}),
2250 id: Some("call-1".to_string()),
2251 thought_signature: None,
2252 },
2253 thought_signature: None,
2254 }]),
2255 },
2256 safety_ratings: None,
2257 citation_metadata: None,
2258 grounding_metadata: None,
2259 finish_reason: Some(adk_gemini::FinishReason::Stop),
2260 index: Some(0),
2261 }],
2262 prompt_feedback: None,
2263 usage_metadata: None,
2264 model_version: None,
2265 response_id: None,
2266 };
2267
2268 let converted =
2269 GeminiModel::convert_response(&response).expect("conversion should succeed");
2270
2271 assert!(!converted.turn_complete);
2272 assert!(converted.content.is_some_and(|content| content.has_function_calls()));
2273 }
2274
2275 #[test]
2276 fn convert_response_handles_inline_data_from_model() {
2277 let image_bytes = vec![0x89, 0x50, 0x4E, 0x47];
2278 let encoded = crate::attachment::encode_base64(&image_bytes);
2279
2280 let response = adk_gemini::GenerationResponse {
2281 candidates: vec![adk_gemini::Candidate {
2282 content: adk_gemini::Content {
2283 role: Some(adk_gemini::Role::Model),
2284 parts: Some(vec![
2285 adk_gemini::Part::Text {
2286 text: "Here is the image".to_string(),
2287 thought: None,
2288 thought_signature: None,
2289 },
2290 adk_gemini::Part::InlineData {
2291 inline_data: adk_gemini::Blob {
2292 mime_type: "image/png".to_string(),
2293 data: encoded,
2294 },
2295 },
2296 ]),
2297 },
2298 safety_ratings: None,
2299 citation_metadata: None,
2300 grounding_metadata: None,
2301 finish_reason: Some(adk_gemini::FinishReason::Stop),
2302 index: Some(0),
2303 }],
2304 prompt_feedback: None,
2305 usage_metadata: None,
2306 model_version: None,
2307 response_id: None,
2308 };
2309
2310 let converted =
2311 GeminiModel::convert_response(&response).expect("conversion should succeed");
2312 let content = converted.content.expect("should have content");
2313 assert!(
2314 content
2315 .parts
2316 .iter()
2317 .any(|part| matches!(part, Part::Text { text } if text == "Here is the image"))
2318 );
2319 assert!(content.parts.iter().any(|part| {
2320 matches!(
2321 part,
2322 Part::InlineData { mime_type, data, .. }
2323 if mime_type == "image/png" && data.as_slice() == image_bytes.as_slice()
2324 )
2325 }));
2326 }
2327
2328 #[test]
2329 fn gemini_function_response_payload_preserves_objects() {
2330 let value = serde_json::json!({
2331 "documents": [
2332 { "id": "pricing", "score": 0.91 }
2333 ]
2334 });
2335
2336 let payload = GeminiModel::gemini_function_response_payload(value.clone());
2337
2338 assert_eq!(payload, value);
2339 }
2340
2341 #[test]
2342 fn gemini_function_response_payload_wraps_arrays() {
2343 let payload =
2344 GeminiModel::gemini_function_response_payload(serde_json::json!([{ "id": "pricing" }]));
2345
2346 assert_eq!(payload, serde_json::json!({ "result": [{ "id": "pricing" }] }));
2347 }
2348
2349 fn convert_function_response_to_gemini_fr(
2354 frd: &adk_core::FunctionResponseData,
2355 ) -> adk_gemini::tools::FunctionResponse {
2356 let mut fr_parts = Vec::new();
2357
2358 for inline in &frd.inline_data {
2359 let encoded = crate::attachment::encode_base64(&inline.data);
2360 fr_parts.push(adk_gemini::FunctionResponsePart::InlineData {
2361 inline_data: adk_gemini::Blob {
2362 mime_type: inline.mime_type.clone(),
2363 data: encoded,
2364 },
2365 });
2366 }
2367
2368 for file in &frd.file_data {
2369 fr_parts.push(adk_gemini::FunctionResponsePart::FileData {
2370 file_data: adk_gemini::FileDataRef {
2371 mime_type: file.mime_type.clone(),
2372 file_uri: file.file_uri.clone(),
2373 },
2374 });
2375 }
2376
2377 let mut gemini_fr = adk_gemini::tools::FunctionResponse::new(
2378 &frd.name,
2379 GeminiModel::gemini_function_response_payload(frd.response.clone()),
2380 );
2381 gemini_fr.parts = fr_parts;
2382 gemini_fr
2383 }
2384
2385 #[test]
2386 fn json_only_function_response_has_no_nested_parts() {
2387 let frd = adk_core::FunctionResponseData::new("tool", serde_json::json!({"ok": true}));
2388 let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2389 assert!(gemini_fr.parts.is_empty());
2390 let json = serde_json::to_string(&gemini_fr).unwrap();
2392 assert!(!json.contains("\"parts\""));
2393 }
2394
2395 #[test]
2396 fn function_response_with_inline_data_has_nested_parts() {
2397 let frd = adk_core::FunctionResponseData::with_inline_data(
2398 "chart",
2399 serde_json::json!({"status": "ok"}),
2400 vec![adk_core::InlineDataPart {
2401 mime_type: "image/png".to_string(),
2402 data: vec![0x89, 0x50, 0x4E, 0x47],
2403 uri: None,
2404 annotations: None,
2405 }],
2406 );
2407 let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2408 assert_eq!(gemini_fr.parts.len(), 1);
2409 match &gemini_fr.parts[0] {
2410 adk_gemini::FunctionResponsePart::InlineData { inline_data } => {
2411 assert_eq!(inline_data.mime_type, "image/png");
2412 let decoded = BASE64_STANDARD.decode(&inline_data.data).unwrap();
2413 assert_eq!(decoded, vec![0x89, 0x50, 0x4E, 0x47]);
2414 }
2415 other => panic!("expected InlineData, got {other:?}"),
2416 }
2417 }
2418
2419 #[test]
2420 fn function_response_with_file_data_has_nested_parts() {
2421 let frd = adk_core::FunctionResponseData::with_file_data(
2422 "doc",
2423 serde_json::json!({"ok": true}),
2424 vec![adk_core::FileDataPart {
2425 mime_type: "application/pdf".to_string(),
2426 file_uri: "gs://bucket/report.pdf".to_string(),
2427 annotations: None,
2428 }],
2429 );
2430 let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2431 assert_eq!(gemini_fr.parts.len(), 1);
2432 match &gemini_fr.parts[0] {
2433 adk_gemini::FunctionResponsePart::FileData { file_data } => {
2434 assert_eq!(file_data.mime_type, "application/pdf");
2435 assert_eq!(file_data.file_uri, "gs://bucket/report.pdf");
2436 }
2437 other => panic!("expected FileData, got {other:?}"),
2438 }
2439 }
2440
2441 #[test]
2442 fn function_response_with_both_inline_and_file_data_ordering() {
2443 let frd = adk_core::FunctionResponseData::with_multimodal(
2444 "multi",
2445 serde_json::json!({}),
2446 vec![
2447 adk_core::InlineDataPart {
2448 mime_type: "image/png".to_string(),
2449 data: vec![1, 2],
2450 uri: None,
2451 annotations: None,
2452 },
2453 adk_core::InlineDataPart {
2454 mime_type: "image/jpeg".to_string(),
2455 data: vec![3, 4],
2456 uri: None,
2457 annotations: None,
2458 },
2459 ],
2460 vec![adk_core::FileDataPart {
2461 mime_type: "application/pdf".to_string(),
2462 file_uri: "gs://b/f.pdf".to_string(),
2463 annotations: None,
2464 }],
2465 );
2466 let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2467 assert_eq!(gemini_fr.parts.len(), 3);
2469 assert!(matches!(&gemini_fr.parts[0], adk_gemini::FunctionResponsePart::InlineData { .. }));
2470 assert!(matches!(&gemini_fr.parts[1], adk_gemini::FunctionResponsePart::InlineData { .. }));
2471 assert!(matches!(&gemini_fr.parts[2], adk_gemini::FunctionResponsePart::FileData { .. }));
2472 }
2473}
2474
2475#[cfg(all(test, feature = "gemini-interactions"))]
2476mod interactions_transport_tests {
2477 use super::*;
2478 use adk_core::GenerateContentConfig;
2479
2480 #[test]
2485 fn default_interaction_options_match_api_posture() {
2486 let opts = InteractionOptions::default();
2487 assert!(opts.store, "store should default to true");
2488 assert!(opts.stateful, "stateful should default to true");
2489 assert_eq!(opts.background, BackgroundMode::AgentTargetsOnly);
2490 assert_eq!(opts.poll_interval, std::time::Duration::from_secs(1));
2491 }
2492
2493 #[test]
2494 fn new_model_defaults_to_generate_content_transport() {
2495 let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2496 .expect("constructing a Gemini model should not require network");
2497 assert_eq!(model.transport(), GeminiTransport::GenerateContent);
2498 }
2499
2500 #[test]
2501 fn use_interactions_api_enables_transport_for_allowlisted_model() {
2502 let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2503 .expect("construct model")
2504 .use_interactions_api(true)
2505 .expect("allowlisted model should enable the Interactions transport");
2506
2507 assert_eq!(model.transport(), GeminiTransport::Interactions);
2508 assert_eq!(
2509 model.interaction_target,
2510 Some(InteractionTarget::Model("gemini-2.5-flash".to_string()))
2511 );
2512 }
2513
2514 #[test]
2515 fn use_interactions_api_rejects_unsupported_model_with_invalid_input() {
2516 let result = GeminiModel::new("test-key", "gemini-2.0-flash")
2517 .expect("construct model")
2518 .use_interactions_api(true);
2519
2520 let err = match result {
2521 Ok(_) => panic!("unsupported model should be rejected"),
2522 Err(err) => err,
2523 };
2524 assert_eq!(err.category, adk_core::ErrorCategory::InvalidInput);
2525 assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2526 }
2527
2528 #[test]
2529 fn use_interactions_api_false_reverts_to_generate_content() {
2530 let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2531 .expect("construct model")
2532 .use_interactions_api(true)
2533 .expect("enable interactions")
2534 .use_interactions_api(false)
2535 .expect("disabling should always succeed");
2536
2537 assert_eq!(model.transport(), GeminiTransport::GenerateContent);
2538 assert_eq!(model.interaction_target, None);
2539 }
2540
2541 #[test]
2542 fn interaction_options_override_is_stored() {
2543 let opts = InteractionOptions {
2544 store: false,
2545 stateful: false,
2546 background: BackgroundMode::Always,
2547 poll_interval: std::time::Duration::from_millis(250),
2548 };
2549 let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2550 .expect("construct model")
2551 .interaction_options(opts.clone());
2552
2553 let stored = model.interaction_options_ref();
2554 assert_eq!(stored.store, opts.store);
2555 assert_eq!(stored.stateful, opts.stateful);
2556 assert_eq!(stored.background, opts.background);
2557 assert_eq!(stored.poll_interval, opts.poll_interval);
2558 }
2559
2560 #[test]
2561 fn resolve_background_honors_background_mode() {
2562 let always = GeminiModel::new("test-key", "gemini-2.5-flash")
2564 .expect("construct model")
2565 .use_interactions_api(true)
2566 .expect("enable")
2567 .interaction_options(InteractionOptions {
2568 background: BackgroundMode::Always,
2569 ..InteractionOptions::default()
2570 });
2571 assert!(always.resolve_background());
2572
2573 let never = GeminiModel::new("test-key", "gemini-2.5-flash")
2575 .expect("construct model")
2576 .use_interactions_api(true)
2577 .expect("enable")
2578 .interaction_options(InteractionOptions {
2579 background: BackgroundMode::Never,
2580 ..InteractionOptions::default()
2581 });
2582 assert!(!never.resolve_background());
2583
2584 let model_target = GeminiModel::new("test-key", "gemini-2.5-flash")
2586 .expect("construct model")
2587 .use_interactions_api(true)
2588 .expect("enable");
2589 assert!(!model_target.resolve_background());
2590
2591 let agent_target = GeminiModel::new("test-key", "deep-research-preview-04-2026")
2593 .expect("construct model")
2594 .use_interactions_api(true)
2595 .expect("enable");
2596 assert!(agent_target.resolve_background());
2597 }
2598
2599 #[test]
2602 fn terminal_failed_status_maps_to_error() {
2603 use adk_gemini::interactions::InteractionStatus;
2604
2605 let err = interaction_status_to_result(InteractionStatus::Failed)
2606 .expect_err("a failed interaction must surface an error");
2607 assert_eq!(err.category, adk_core::ErrorCategory::Internal);
2608 assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2609 assert_eq!(err.code, "model.gemini.interactions.failed");
2610 }
2611
2612 #[test]
2615 fn terminal_budget_exceeded_status_maps_to_error() {
2616 use adk_gemini::interactions::InteractionStatus;
2617
2618 let err = interaction_status_to_result(InteractionStatus::BudgetExceeded)
2619 .expect_err("a budget_exceeded interaction must surface an error");
2620 assert_eq!(err.category, adk_core::ErrorCategory::Internal);
2621 assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2622 assert_eq!(err.code, "model.gemini.interactions.budget_exceeded");
2623 }
2624
2625 #[test]
2628 fn non_failure_statuses_are_ok() {
2629 use adk_gemini::interactions::InteractionStatus;
2630
2631 for status in [
2632 InteractionStatus::InProgress,
2633 InteractionStatus::RequiresAction,
2634 InteractionStatus::Completed,
2635 InteractionStatus::Cancelled,
2636 InteractionStatus::Incomplete,
2637 ] {
2638 assert!(
2639 interaction_status_to_result(status).is_ok(),
2640 "status {status:?} should not map to an error"
2641 );
2642 }
2643 }
2644
2645 #[test]
2648 fn failed_interaction_resource_surfaces_error() {
2649 use adk_gemini::interactions::{Interaction, InteractionStatus};
2650
2651 let interaction = Interaction {
2652 id: "v1_failed".to_string(),
2653 model: Some("gemini-2.5-flash".to_string()),
2654 agent: None,
2655 status: InteractionStatus::Failed,
2656 steps: Vec::new(),
2657 usage: None,
2658 created: None,
2659 updated: None,
2660 environment_id: None,
2661 };
2662
2663 let err = interaction_status_to_result(interaction.status)
2664 .expect_err("failed interaction must surface an error");
2665 assert_eq!(err.category, adk_core::ErrorCategory::Internal);
2666 assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2667 }
2668 #[test]
2669 fn gemini_37_rejects_sampling_before_network_io() {
2670 let model = GeminiModel::new("test-key", "gemini-3.7-flash").expect("construct model");
2671 let mut request = LlmRequest::new("gemini-3.7-flash", Vec::new());
2672 request.config =
2673 Some(GenerateContentConfig { temperature: Some(0.2), ..Default::default() });
2674
2675 let error = model
2676 .validate_request_contract(&request)
2677 .expect_err("sampling must be rejected locally");
2678 assert_eq!(error.code, "model.gemini.sampling_unsupported");
2679 }
2680
2681 #[test]
2682 fn gemini_37_rejects_token_budget_thinking() {
2683 let model = GeminiModel::new("test-key", "gemini-3.7-flash")
2684 .expect("construct model")
2685 .with_thinking_config(adk_gemini::ThinkingConfig {
2686 thinking_budget: Some(1024),
2687 include_thoughts: None,
2688 thinking_level: None,
2689 });
2690
2691 let error = model
2692 .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2693 .expect_err("token budgets must be rejected locally");
2694 assert_eq!(error.code, "model.gemini.thinking_budget_unsupported");
2695 }
2696}
2697
2698#[cfg(all(test, feature = "gemini-vertex"))]
2699mod vertex_rag_tests {
2700 use super::*;
2701 use adk_gemini::VertexRagStore;
2702
2703 fn test_store() -> VertexRagStore {
2704 VertexRagStore::corpus("projects/p/locations/us-central1/ragCorpora/123").with_top_k(5)
2705 }
2706
2707 fn vertex_model() -> GeminiModel {
2708 GeminiModel::new_google_cloud("test-key", "p", "us-central1", "gemini-3.7-flash")
2709 .expect("construct vertex model")
2710 }
2711
2712 #[tokio::test]
2715 async fn vertex_backend_accepts_and_stores_the_rag_store() {
2716 let model = vertex_model().with_vertex_rag_store(test_store());
2717
2718 model
2719 .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2720 .expect("vertex backend must accept the rag store");
2721
2722 assert_eq!(model.vertex_rag_store(), Some(&test_store()));
2726 }
2727
2728 #[test]
2729 fn studio_backend_rejects_rag_store_before_network_io() {
2730 let model = GeminiModel::new("test-key", "gemini-3.7-flash")
2731 .expect("construct studio model")
2732 .with_vertex_rag_store(test_store());
2733
2734 let error = model
2735 .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2736 .expect_err("studio backend must reject the rag store locally");
2737 assert_eq!(error.code, "model.gemini.rag_store_requires_vertex");
2738 assert_eq!(error.category, adk_core::ErrorCategory::Unsupported);
2739 assert_eq!(error.details.provider.as_deref(), Some("gemini"));
2740 }
2741
2742 #[test]
2743 fn no_rag_store_means_no_gate() {
2744 let model = GeminiModel::new("test-key", "gemini-3.7-flash").expect("construct model");
2745 assert_eq!(model.vertex_rag_store(), None);
2746 model
2747 .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2748 .expect("no store configured, nothing to reject");
2749 }
2750}
2751
2752#[cfg(all(test, feature = "gemini-vertex"))]
2753mod vertex_transport_tests {
2754 use super::*;
2755
2756 #[test]
2757 fn vertex_transport_failures_are_retryable() {
2758 let error = adk_gemini::ClientError::Io {
2759 source: std::io::Error::other(
2760 "the transport reports an error: client error (SendRequest): http2 error",
2761 ),
2762 };
2763 let mapped = gemini_error_to_adk(&error);
2764 assert_eq!(
2765 (mapped.category, mapped.code, mapped.is_retryable()),
2766 (ErrorCategory::Unavailable, "model.gemini.unavailable", true)
2767 );
2768
2769 let denied =
2770 adk_gemini::ClientError::Io { source: std::io::Error::other("permission denied") };
2771 assert_eq!(gemini_error_to_adk(&denied).category, ErrorCategory::Internal);
2772 }
2773}