Skip to main content

adk_model/gemini/
client.rs

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/// Which Gemini wire API a [`GeminiModel`] uses.
18///
19/// Defaults to [`GeminiTransport::GenerateContent`], the classic
20/// `models/{model}:generateContent` endpoint. Selecting
21/// [`GeminiTransport::Interactions`] (via [`GeminiModel::use_interactions_api`])
22/// routes requests through the Interactions API (Beta): a stateful, step-based
23/// transport that drives the same [`adk_core::Llm`] contract.
24///
25/// Only compiled when the `gemini-interactions` feature is enabled.
26#[cfg(feature = "gemini-interactions")]
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub enum GeminiTransport {
29    /// The classic `models/{model}:generateContent` API (default).
30    #[default]
31    GenerateContent,
32    /// The Interactions API (Beta): stateful, step-based.
33    Interactions,
34}
35
36/// Background-execution policy for the Interactions transport.
37///
38/// Controls whether interactions run with `background=true`. The default,
39/// [`BackgroundMode::AgentTargetsOnly`], keeps low-latency chat turns
40/// foreground while letting long-running agent targets (e.g. Deep Research)
41/// run in the background.
42///
43/// Only compiled when the `gemini-interactions` feature is enabled.
44#[cfg(feature = "gemini-interactions")]
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum BackgroundMode {
47    /// `background=true` for agent targets, `false` for model targets
48    /// (default). Keeps chat turns low-latency while letting Deep Research and
49    /// other long-running agents run in the background.
50    #[default]
51    AgentTargetsOnly,
52    /// Always run interactions with `background=true`.
53    Always,
54    /// Never run interactions in the background.
55    Never,
56}
57
58/// Faithful-to-API options for the Interactions transport.
59///
60/// The defaults mirror the Interactions API's intended posture: interactions
61/// are stored (`store = true`), stateful continuation via
62/// `previous_interaction_id` is enabled (`stateful = true`), background
63/// execution applies to agent targets only
64/// ([`BackgroundMode::AgentTargetsOnly`]), and background interactions are
65/// polled once per second.
66///
67/// Only compiled when the `gemini-interactions` feature is enabled.
68#[cfg(feature = "gemini-interactions")]
69#[derive(Debug, Clone)]
70pub struct InteractionOptions {
71    /// Whether interactions are stored server-side. Default: `true`.
72    pub store: bool,
73    /// Whether to continue conversations statefully via
74    /// `previous_interaction_id`. Default: `true`.
75    pub stateful: bool,
76    /// Background-execution policy. Default: [`BackgroundMode::AgentTargetsOnly`].
77    pub background: BackgroundMode,
78    /// Poll interval for background interactions. Default: 1 second.
79    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
94/// Gemini model client wrapping the `adk-gemini` crate for the `Llm` trait.
95pub struct GeminiModel {
96    client: Gemini,
97    model_name: String,
98    retry_config: RetryConfig,
99    /// Default thinking configuration applied to every request.
100    ///
101    /// Controls the model's reasoning effort. For Gemini 3 series, use
102    /// `ThinkingLevel` (Low/Medium/High). For Gemini 2.5 series, use
103    /// `thinking_budget` (token count).
104    thinking_config: Option<adk_gemini::ThinkingConfig>,
105    /// Selected wire transport. Defaults to
106    /// [`GeminiTransport::GenerateContent`]; set to
107    /// [`GeminiTransport::Interactions`] via [`GeminiModel::use_interactions_api`].
108    #[cfg(feature = "gemini-interactions")]
109    transport: GeminiTransport,
110    /// The validated Interactions destination, populated when the Interactions
111    /// transport is enabled. `None` for the generateContent transport.
112    #[cfg(feature = "gemini-interactions")]
113    interaction_target: Option<InteractionTarget>,
114    /// Faithful-to-API options for the Interactions transport.
115    #[cfg(feature = "gemini-interactions")]
116    interaction_options: InteractionOptions,
117    /// Server-side RAG grounding declaration attached to every request.
118    /// Vertex AI backend only; see [`GeminiModel::with_vertex_rag_store`].
119    #[cfg(feature = "gemini-vertex")]
120    vertex_rag_store: Option<adk_gemini::VertexRagStore>,
121    /// Whether the wrapped client targets the Vertex AI backend (as opposed
122    /// to AI Studio). Set by the `new_google_cloud*` constructors.
123    #[cfg(feature = "gemini-vertex")]
124    vertex_backend: bool,
125}
126
127/// Convert a Gemini client error to a structured `AdkError` with proper category and retry hints.
128fn 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
142    // Extract status code from BadResponse variant via Display output
143    // BadResponse format: "bad response from server; code {code}; description: ..."
144    let (category, code, status_code) = if message.contains("code 429")
145        || message.contains("RESOURCE_EXHAUSTED")
146        || message.contains("rate limit")
147    {
148        (ErrorCategory::RateLimited, "model.gemini.rate_limited", Some(429u16))
149    } else if message.contains("code 503") || message.contains("UNAVAILABLE") {
150        (ErrorCategory::Unavailable, "model.gemini.unavailable", Some(503))
151    } else if message.contains("code 529") || message.contains("OVERLOADED") {
152        (ErrorCategory::Unavailable, "model.gemini.overloaded", Some(529))
153    } else if message.contains("code 408")
154        || message.contains("DEADLINE_EXCEEDED")
155        || message.contains("TIMEOUT")
156    {
157        (ErrorCategory::Timeout, "model.gemini.timeout", Some(408))
158    } else if message.contains("code 401") || message.contains("Invalid API key") {
159        (ErrorCategory::Unauthorized, "model.gemini.unauthorized", Some(401))
160    } else if message.contains("code 400") {
161        (ErrorCategory::InvalidInput, "model.gemini.bad_request", Some(400))
162    } else if message.contains("code 404") {
163        (ErrorCategory::NotFound, "model.gemini.not_found", Some(404))
164    } else if message.contains("invalid generation config") {
165        (ErrorCategory::InvalidInput, "model.gemini.invalid_config", None)
166    } else {
167        (ErrorCategory::Internal, "model.gemini.internal", None)
168    };
169
170    let mut err = adk_core::AdkError::new(ErrorComponent::Model, category, code, message)
171        .with_provider("gemini");
172    if let Some(sc) = status_code {
173        err = err.with_upstream_status(sc);
174    }
175    err
176}
177
178/// Maps a terminal [`InteractionStatus`](adk_gemini::interactions::InteractionStatus)
179/// to a `Result`, surfacing the API's failure states as errors.
180///
181/// The Interactions API can terminate an interaction in `failed` or
182/// `budget_exceeded` (Requirements 7.7 / 9.2). This helper converts those two
183/// states into an [`adk_core::AdkError`] (provider `"gemini"`, category
184/// [`ErrorCategory::Internal`]) and treats every other status as success — the
185/// caller has already decided to read the interaction's content for non-failure
186/// states. Factored out as a free function so the terminal-status mapping is
187/// unit-testable without a network round-trip.
188#[cfg(feature = "gemini-interactions")]
189fn interaction_status_to_result(status: adk_gemini::interactions::InteractionStatus) -> Result<()> {
190    use adk_gemini::interactions::InteractionStatus;
191    match status {
192        InteractionStatus::Failed => Err(interaction_terminal_error(
193            "model.gemini.interactions.failed",
194            "the Gemini interaction terminated with status `failed`",
195        )),
196        InteractionStatus::BudgetExceeded => Err(interaction_terminal_error(
197            "model.gemini.interactions.budget_exceeded",
198            "the Gemini interaction terminated with status `budget_exceeded`",
199        )),
200        InteractionStatus::InProgress
201        | InteractionStatus::RequiresAction
202        | InteractionStatus::Completed
203        | InteractionStatus::Cancelled
204        | InteractionStatus::Incomplete => Ok(()),
205    }
206}
207
208/// Builds the [`adk_core::AdkError`] for a terminal Interactions failure status.
209#[cfg(feature = "gemini-interactions")]
210fn interaction_terminal_error(code: &'static str, message: &str) -> adk_core::AdkError {
211    adk_core::AdkError::new(
212        ErrorComponent::Model,
213        ErrorCategory::Internal,
214        code,
215        message.to_string(),
216    )
217    .with_provider("gemini")
218}
219
220/// Returns the value of the environment variable `name` when it is set and
221/// non-empty (after trimming whitespace).
222fn env_non_empty(name: &str) -> Option<String> {
223    std::env::var(name).ok().filter(|v| !v.trim().is_empty())
224}
225
226/// Returns `true` when an opt-in flag value selects the Vertex AI backend:
227/// `1` or a case-insensitive `true`.
228fn env_flag_truthy(value: &str) -> bool {
229    let v = value.trim();
230    v == "1" || v.eq_ignore_ascii_case("true")
231}
232
233/// Reports whether the environment opts in to the Vertex AI backend.
234///
235/// Consults `GOOGLE_GENAI_USE_ENTERPRISE` first and, when that variable is
236/// unset or empty, the deprecated `GOOGLE_GENAI_USE_VERTEXAI`. A flag is
237/// truthy when its value is `1` or a case-insensitive `true`.
238/// `GOOGLE_GENAI_USE_ENTERPRISE` takes precedence when both are set.
239///
240/// # Example
241///
242/// ```rust,no_run
243/// if adk_model::gemini::vertex_env_requested() {
244///     // route Gemini traffic through Vertex AI
245/// }
246/// ```
247pub fn vertex_env_requested() -> bool {
248    // `GOOGLE_GENAI_USE_VERTEXAI` is the deprecated name adk-python still
249    // honors. Deliberately no deprecation warning for it —
250    // google/adk-python#6168 documents the churn such a warning caused.
251    if let Some(v) = env_non_empty("GOOGLE_GENAI_USE_ENTERPRISE") {
252        return env_flag_truthy(&v);
253    }
254    env_non_empty("GOOGLE_GENAI_USE_VERTEXAI").is_some_and(|v| env_flag_truthy(&v))
255}
256
257/// Reads `GOOGLE_CLOUD_PROJECT` and `GOOGLE_CLOUD_LOCATION`, erroring with a
258/// message that names whichever variable is missing.
259#[cfg(feature = "gemini-vertex")]
260fn vertex_target_from_env() -> Result<(String, String)> {
261    let project = env_non_empty("GOOGLE_CLOUD_PROJECT");
262    let location = env_non_empty("GOOGLE_CLOUD_LOCATION");
263    if let (Some(project), Some(location)) = (&project, &location) {
264        return Ok((project.clone(), location.clone()));
265    }
266    let mut missing = Vec::new();
267    if project.is_none() {
268        missing.push("GOOGLE_CLOUD_PROJECT");
269    }
270    if location.is_none() {
271        missing.push("GOOGLE_CLOUD_LOCATION");
272    }
273    Err(adk_core::AdkError::new(
274        ErrorComponent::Model,
275        ErrorCategory::InvalidInput,
276        "model.gemini.vertex_env_incomplete",
277        format!(
278            "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).",
279            missing.join(" and ")
280        ),
281    )
282    .with_provider("gemini"))
283}
284
285impl GeminiModel {
286    fn gemini_part_thought_signature(value: &serde_json::Value) -> Option<String> {
287        value.get("thoughtSignature").and_then(serde_json::Value::as_str).map(str::to_string)
288    }
289
290    fn validate_request_contract(&self, req: &LlmRequest) -> Result<()> {
291        // Server-side RAG grounding is a Vertex AI capability; reject it on
292        // the AI Studio backend before any network IO (mirrors the request-
293        // time gating the Vertex backend applies to Studio-only fields).
294        #[cfg(feature = "gemini-vertex")]
295        if self.vertex_rag_store.is_some() && !self.vertex_backend {
296            return Err(adk_core::AdkError::new(
297                ErrorComponent::Model,
298                ErrorCategory::Unsupported,
299                "model.gemini.rag_store_requires_vertex",
300                "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.",
301            )
302            .with_provider("gemini"));
303        }
304
305        let model = self.model_name.strip_prefix("models/").unwrap_or(&self.model_name);
306        if !matches!(model, "gemini-3.6-flash" | "gemini-3.7-flash") {
307            return Ok(());
308        }
309
310        if let Some(config) = &req.config
311            && (config.temperature.is_some() || config.top_p.is_some() || config.top_k.is_some())
312        {
313            return Err(adk_core::AdkError::new(
314                ErrorComponent::Model,
315                ErrorCategory::InvalidInput,
316                "model.gemini.sampling_unsupported",
317                format!(
318                    "{model} does not accept temperature, top_p, or top_k; remove explicit sampling parameters"
319                ),
320            )
321            .with_provider("gemini"));
322        }
323        if self.thinking_config.as_ref().is_some_and(|config| config.thinking_budget.is_some()) {
324            return Err(adk_core::AdkError::new(
325                ErrorComponent::Model,
326                ErrorCategory::InvalidInput,
327                "model.gemini.thinking_budget_unsupported",
328                format!(
329                    "{model} uses thinking levels instead of token budgets; set thinking_level and clear thinking_budget"
330                ),
331            )
332            .with_provider("gemini"));
333        }
334        Ok(())
335    }
336
337    /// Builds a `GeminiModel` from a constructed client and model name with all
338    /// configurable fields defaulted.
339    ///
340    /// Centralizing struct construction here keeps the cfg-gated Interactions
341    /// fields out of every public constructor's `Self { .. }` literal.
342    fn from_client(client: Gemini, model_name: String) -> Self {
343        Self {
344            client,
345            model_name,
346            retry_config: RetryConfig::default(),
347            thinking_config: None,
348            #[cfg(feature = "gemini-interactions")]
349            transport: GeminiTransport::GenerateContent,
350            #[cfg(feature = "gemini-interactions")]
351            interaction_target: None,
352            #[cfg(feature = "gemini-interactions")]
353            interaction_options: InteractionOptions::default(),
354            #[cfg(feature = "gemini-vertex")]
355            vertex_rag_store: None,
356            #[cfg(feature = "gemini-vertex")]
357            vertex_backend: false,
358        }
359    }
360
361    /// Create a new Gemini model client with an API key and model name.
362    pub fn new(api_key: impl Into<String>, model: impl Into<String>) -> Result<Self> {
363        let model_name = model.into();
364        crate::catalog::warn_if_obsolete("gemini", &model_name);
365        let client = Gemini::with_model(api_key.into(), model_name.clone())
366            .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
367
368        Ok(Self::from_client(client, model_name))
369    }
370
371    /// Create a Gemini model via Vertex AI with API key auth.
372    ///
373    /// Requires `gemini-vertex` feature.
374    #[cfg(feature = "gemini-vertex")]
375    pub fn new_google_cloud(
376        api_key: impl Into<String>,
377        project_id: impl AsRef<str>,
378        location: impl AsRef<str>,
379        model: impl Into<String>,
380    ) -> Result<Self> {
381        let model_name = model.into();
382        let client = Gemini::with_google_cloud_model(
383            api_key.into(),
384            project_id,
385            location,
386            model_name.clone(),
387        )
388        .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
389
390        let mut model = Self::from_client(client, model_name);
391        model.vertex_backend = true;
392        Ok(model)
393    }
394
395    /// Create a Gemini model via Vertex AI with service account JSON.
396    ///
397    /// Requires `gemini-vertex` feature.
398    #[cfg(feature = "gemini-vertex")]
399    pub fn new_google_cloud_service_account(
400        service_account_json: &str,
401        project_id: impl AsRef<str>,
402        location: impl AsRef<str>,
403        model: impl Into<String>,
404    ) -> Result<Self> {
405        let model_name = model.into();
406        let client = Gemini::with_google_cloud_service_account_json(
407            service_account_json,
408            project_id.as_ref(),
409            location.as_ref(),
410            model_name.clone(),
411        )
412        .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
413
414        let mut model = Self::from_client(client, model_name);
415        model.vertex_backend = true;
416        Ok(model)
417    }
418
419    /// Create a Gemini model via Vertex AI with Application Default Credentials.
420    ///
421    /// Requires `gemini-vertex` feature.
422    #[cfg(feature = "gemini-vertex")]
423    pub fn new_google_cloud_adc(
424        project_id: impl AsRef<str>,
425        location: impl AsRef<str>,
426        model: impl Into<String>,
427    ) -> Result<Self> {
428        let model_name = model.into();
429        let client = Gemini::with_google_cloud_adc_model(
430            project_id.as_ref(),
431            location.as_ref(),
432            model_name.clone(),
433        )
434        .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
435
436        let mut model = Self::from_client(client, model_name);
437        model.vertex_backend = true;
438        Ok(model)
439    }
440
441    /// Create a Gemini model via Vertex AI with Workload Identity Federation.
442    ///
443    /// Requires `gemini-vertex` feature.
444    #[cfg(feature = "gemini-vertex")]
445    pub fn new_google_cloud_wif(
446        wif_json: &str,
447        project_id: impl AsRef<str>,
448        location: impl AsRef<str>,
449        model: impl Into<String>,
450    ) -> Result<Self> {
451        let model_name = model.into();
452        let client = Gemini::with_google_cloud_wif_json(
453            wif_json,
454            project_id.as_ref(),
455            location.as_ref(),
456            model_name.clone(),
457        )
458        .map_err(|e| adk_core::AdkError::model(e.to_string()))?;
459
460        let mut model = Self::from_client(client, model_name);
461        model.vertex_backend = true;
462        Ok(model)
463    }
464
465    /// Create a Gemini model from environment variables.
466    ///
467    /// When `GOOGLE_GENAI_USE_ENTERPRISE` or `GOOGLE_GENAI_USE_VERTEXAI` is
468    /// truthy (`1` or a case-insensitive `true`), builds a Vertex AI client
469    /// with Application Default Credentials from `GOOGLE_CLOUD_PROJECT` and
470    /// `GOOGLE_CLOUD_LOCATION` — this path requires the `gemini-vertex`
471    /// feature. Otherwise builds a Gemini API (AI Studio) client from
472    /// `GOOGLE_API_KEY` or `GEMINI_API_KEY`.
473    ///
474    /// `GOOGLE_GENAI_USE_ENTERPRISE` takes precedence when both flags are set.
475    ///
476    /// The Vertex path builds Application Default Credentials, which requires
477    /// a Tokio runtime to be current — call from async code or under
478    /// `#[tokio::main]`.
479    ///
480    /// # Errors
481    ///
482    /// - A Vertex flag is truthy but `GOOGLE_CLOUD_PROJECT` or
483    ///   `GOOGLE_CLOUD_LOCATION` is not set.
484    /// - A Vertex flag is truthy but the `gemini-vertex` feature is not
485    ///   compiled.
486    /// - No Vertex flag is truthy and neither `GOOGLE_API_KEY` nor
487    ///   `GEMINI_API_KEY` is set.
488    ///
489    /// # Example
490    ///
491    /// ```rust,no_run
492    /// use adk_model::gemini::GeminiModel;
493    ///
494    /// # #[tokio::main]
495    /// # async fn main() -> adk_core::Result<()> {
496    /// let model = GeminiModel::from_env("gemini-2.5-flash")?;
497    /// # Ok(())
498    /// # }
499    /// ```
500    pub fn from_env(model: &str) -> Result<Self> {
501        if vertex_env_requested() {
502            #[cfg(feature = "gemini-vertex")]
503            {
504                let (project, location) = vertex_target_from_env()?;
505                return Self::new_google_cloud_adc(project, location, model);
506            }
507            #[cfg(not(feature = "gemini-vertex"))]
508            {
509                return Err(adk_core::AdkError::new(
510                    ErrorComponent::Model,
511                    ErrorCategory::Unsupported,
512                    "model.gemini.vertex_feature_missing",
513                    "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.",
514                )
515                .with_provider("gemini"));
516            }
517        }
518
519        let Some(api_key) =
520            env_non_empty("GOOGLE_API_KEY").or_else(|| env_non_empty("GEMINI_API_KEY"))
521        else {
522            return Err(adk_core::AdkError::new(
523                ErrorComponent::Model,
524                ErrorCategory::InvalidInput,
525                "model.gemini.api_key_missing",
526                "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.",
527            )
528            .with_provider("gemini"));
529        };
530        Self::new(api_key, model)
531    }
532
533    /// Set the retry configuration (builder pattern).
534    #[must_use]
535    pub fn with_retry_config(mut self, retry_config: RetryConfig) -> Self {
536        self.retry_config = retry_config;
537        self
538    }
539
540    /// Set the retry configuration (mutable reference).
541    pub fn set_retry_config(&mut self, retry_config: RetryConfig) {
542        self.retry_config = retry_config;
543    }
544
545    /// Returns the current retry configuration.
546    pub fn retry_config(&self) -> &RetryConfig {
547        &self.retry_config
548    }
549
550    /// Set the default thinking configuration applied to every request.
551    ///
552    /// Controls the model's reasoning effort. For Gemini 3 series models,
553    /// use `ThinkingLevel` (Low/Medium/High). For Gemini 2.5 series, use
554    /// `thinking_budget` (token count).
555    ///
556    /// # Example
557    ///
558    /// ```rust,ignore
559    /// use adk_gemini::{ThinkingConfig, ThinkingLevel};
560    ///
561    /// // Gemini 3 — level-based thinking
562    /// let model = GeminiModel::new(api_key, "gemini-3.1-pro-preview")?
563    ///     .with_thinking_config(
564    ///         ThinkingConfig::new().with_thinking_level(ThinkingLevel::Low)
565    ///     );
566    ///
567    /// // Gemini 2.5 — budget-based thinking
568    /// let model = GeminiModel::new(api_key, "gemini-2.5-flash")?
569    ///     .with_thinking_config(
570    ///         ThinkingConfig::new().with_thinking_budget(2048)
571    ///     );
572    /// ```
573    #[must_use]
574    pub fn with_thinking_config(mut self, thinking_config: adk_gemini::ThinkingConfig) -> Self {
575        self.thinking_config = Some(thinking_config);
576        self
577    }
578
579    /// Set the thinking configuration (mutable reference variant).
580    pub fn set_thinking_config(&mut self, thinking_config: adk_gemini::ThinkingConfig) {
581        self.thinking_config = Some(thinking_config);
582    }
583
584    /// Returns the current thinking configuration, if set.
585    pub fn thinking_config(&self) -> Option<&adk_gemini::ThinkingConfig> {
586        self.thinking_config.as_ref()
587    }
588
589    /// Ground every request in a Vertex AI RAG store (server-side retrieval).
590    ///
591    /// Declares a `retrieval` tool on each outgoing `generateContent` request
592    /// so Vertex AI retrieves grounding context from the referenced RAG
593    /// corpora before generating. Requires a Vertex AI backend — requests on
594    /// an AI Studio model fail with the structured error
595    /// `model.gemini.rag_store_requires_vertex` before any network IO.
596    ///
597    /// # Example
598    ///
599    /// ```rust,no_run
600    /// use adk_gemini::VertexRagStore;
601    /// use adk_model::gemini::GeminiModel;
602    ///
603    /// # fn main() -> adk_core::Result<()> {
604    /// let store = VertexRagStore::corpus("projects/p/locations/us-central1/ragCorpora/123")
605    ///     .with_top_k(5);
606    /// let model =
607    ///     GeminiModel::new_google_cloud("api-key", "p", "us-central1", "gemini-3.7-flash")?
608    ///         .with_vertex_rag_store(store);
609    /// # Ok(())
610    /// # }
611    /// ```
612    #[cfg(feature = "gemini-vertex")]
613    #[must_use]
614    pub fn with_vertex_rag_store(mut self, store: adk_gemini::VertexRagStore) -> Self {
615        self.vertex_rag_store = Some(store);
616        self
617    }
618
619    /// Returns the configured Vertex RAG store, if set.
620    #[cfg(feature = "gemini-vertex")]
621    pub fn vertex_rag_store(&self) -> Option<&adk_gemini::VertexRagStore> {
622        self.vertex_rag_store.as_ref()
623    }
624
625    /// Enable (or disable) the Interactions API transport.
626    ///
627    /// When enabling, the model's configured model id is validated against the
628    /// Interactions allowlist via [`InteractionTarget::parse`]. On success the
629    /// transport switches to [`GeminiTransport::Interactions`] and the
630    /// validated target is stored. Disabling reverts to
631    /// [`GeminiTransport::GenerateContent`] and clears the stored target.
632    ///
633    /// Requires the `gemini-interactions` feature.
634    ///
635    /// # Errors
636    ///
637    /// Returns an [`adk_core::AdkError`] with category `InvalidInput` when
638    /// enabling the transport for a model id outside the Interactions
639    /// allowlist (Requirement 2.4). The error message names the supported
640    /// model and agent targets.
641    ///
642    /// # Example
643    ///
644    /// ```rust,ignore
645    /// let model = GeminiModel::new(api_key, "gemini-2.5-flash")?
646    ///     .use_interactions_api(true)?;
647    /// ```
648    #[cfg(feature = "gemini-interactions")]
649    pub fn use_interactions_api(mut self, enabled: bool) -> Result<Self> {
650        if enabled {
651            let target = InteractionTarget::parse(&self.model_name)?;
652            self.transport = GeminiTransport::Interactions;
653            self.interaction_target = Some(target);
654        } else {
655            self.transport = GeminiTransport::GenerateContent;
656            self.interaction_target = None;
657        }
658        Ok(self)
659    }
660
661    /// Override the Interactions transport options (store, stateful, background
662    /// mode, poll interval).
663    ///
664    /// Requires the `gemini-interactions` feature. The defaults (see
665    /// [`InteractionOptions::default`]) mirror the Interactions API's intended
666    /// posture; override only when a different behavior is required.
667    #[cfg(feature = "gemini-interactions")]
668    #[must_use]
669    pub fn interaction_options(mut self, opts: InteractionOptions) -> Self {
670        self.interaction_options = opts;
671        self
672    }
673
674    /// Returns the currently selected wire transport.
675    ///
676    /// Requires the `gemini-interactions` feature.
677    #[cfg(feature = "gemini-interactions")]
678    pub fn transport(&self) -> GeminiTransport {
679        self.transport
680    }
681
682    /// Returns the configured Interactions options.
683    ///
684    /// Requires the `gemini-interactions` feature.
685    #[cfg(feature = "gemini-interactions")]
686    pub fn interaction_options_ref(&self) -> &InteractionOptions {
687        &self.interaction_options
688    }
689
690    /// Resolves whether background execution should be used for the configured
691    /// Interactions target, honoring [`BackgroundMode`].
692    ///
693    /// Returns `true` when the mode is [`BackgroundMode::Always`], `false` when
694    /// [`BackgroundMode::Never`], and (for [`BackgroundMode::AgentTargetsOnly`])
695    /// `true` only when the configured target is an agent target. When no
696    /// Interactions target is configured, agent-targets-only resolves to
697    /// `false`.
698    ///
699    /// Used by the non-streaming/background Interactions path (task 7.3); kept
700    /// here so the transport state and its policy resolution live together.
701    #[cfg(feature = "gemini-interactions")]
702    fn resolve_background(&self) -> bool {
703        match self.interaction_options.background {
704            BackgroundMode::Always => true,
705            BackgroundMode::Never => false,
706            BackgroundMode::AgentTargetsOnly => {
707                self.interaction_target.as_ref().is_some_and(InteractionTarget::is_agent)
708            }
709        }
710    }
711
712    fn convert_response(resp: &adk_gemini::GenerationResponse) -> Result<LlmResponse> {
713        let mut converted_parts: Vec<Part> = Vec::new();
714
715        // Convert content parts
716        if let Some(parts) = resp.candidates.first().and_then(|c| c.content.parts.as_ref()) {
717            for p in parts {
718                match p {
719                    adk_gemini::Part::Text { text, thought, thought_signature } => {
720                        if thought == &Some(true) {
721                            converted_parts.push(Part::Thinking {
722                                thinking: text.clone(),
723                                signature: thought_signature.clone(),
724                            });
725                        } else {
726                            converted_parts.push(Part::Text { text: text.clone() });
727                        }
728                    }
729                    adk_gemini::Part::InlineData { inline_data } => {
730                        let decoded =
731                            BASE64_STANDARD.decode(&inline_data.data).map_err(|error| {
732                                adk_core::AdkError::model(format!(
733                                    "failed to decode inline data from gemini response: {error}"
734                                ))
735                            })?;
736                        converted_parts.push(Part::InlineData {
737                            mime_type: inline_data.mime_type.clone(),
738                            data: decoded,
739                            uri: None,
740                            annotations: None,
741                        });
742                    }
743                    adk_gemini::Part::FunctionCall { function_call, thought_signature } => {
744                        converted_parts.push(Part::FunctionCall {
745                            name: function_call.name.clone(),
746                            args: function_call.args.clone(),
747                            id: function_call.id.clone(),
748                            thought_signature: thought_signature.clone(),
749                        });
750                    }
751                    adk_gemini::Part::FunctionResponse { function_response, .. } => {
752                        converted_parts.push(Part::FunctionResponse {
753                            function_response: adk_core::FunctionResponseData::new(
754                                function_response.name.clone(),
755                                function_response
756                                    .response
757                                    .clone()
758                                    .unwrap_or(serde_json::Value::Null),
759                            ),
760                            id: None,
761                            annotations: None,
762                        });
763                    }
764                    adk_gemini::Part::ToolCall { .. } | adk_gemini::Part::ExecutableCode { .. } => {
765                        if let Ok(value) = serde_json::to_value(p) {
766                            converted_parts.push(Part::ServerToolCall { server_tool_call: value });
767                        }
768                    }
769                    adk_gemini::Part::ToolResponse { .. }
770                    | adk_gemini::Part::CodeExecutionResult { .. } => {
771                        let value = serde_json::to_value(p).unwrap_or(serde_json::Value::Null);
772                        converted_parts
773                            .push(Part::ServerToolResponse { server_tool_response: value });
774                    }
775                    adk_gemini::Part::FileData { file_data } => {
776                        converted_parts.push(Part::FileData {
777                            mime_type: file_data.mime_type.clone(),
778                            file_uri: file_data.file_uri.clone(),
779                            annotations: None,
780                        });
781                    }
782                }
783            }
784        }
785
786        // Add grounding metadata as text if present (required for Google Search grounding compliance)
787        if let Some(grounding) = resp.candidates.first().and_then(|c| c.grounding_metadata.as_ref())
788        {
789            if let Some(queries) = &grounding.web_search_queries
790                && !queries.is_empty()
791            {
792                let search_info = format!("\n\n🔍 **Searched:** {}", queries.join(", "));
793                converted_parts.push(Part::Text { text: search_info });
794            }
795            if let Some(chunks) = &grounding.grounding_chunks {
796                let sources: Vec<String> = chunks
797                    .iter()
798                    .filter_map(|c| {
799                        c.web.as_ref().and_then(|w| match (&w.title, &w.uri) {
800                            (Some(title), Some(uri)) => Some(format!("[{}]({})", title, uri)),
801                            (Some(title), None) => Some(title.clone()),
802                            (None, Some(uri)) => Some(uri.to_string()),
803                            (None, None) => None,
804                        })
805                    })
806                    .collect();
807                if !sources.is_empty() {
808                    let sources_info = format!("\n📚 **Sources:** {}", sources.join(" | "));
809                    converted_parts.push(Part::Text { text: sources_info });
810                }
811            }
812        }
813
814        let content = if converted_parts.is_empty() {
815            None
816        } else {
817            Some(Content { role: "model".to_string(), parts: converted_parts })
818        };
819        let tool_call_turn = content.as_ref().is_some_and(Content::has_function_calls);
820
821        let usage_metadata = resp.usage_metadata.as_ref().map(|u| UsageMetadata {
822            prompt_token_count: u.prompt_token_count.unwrap_or(0),
823            candidates_token_count: u.candidates_token_count.unwrap_or(0),
824            total_token_count: u.total_token_count.unwrap_or(0),
825            thinking_token_count: u.thoughts_token_count,
826            cache_read_input_token_count: u.cached_content_token_count,
827            ..Default::default()
828        });
829
830        let finish_reason =
831            resp.candidates.first().and_then(|c| c.finish_reason.as_ref()).map(|fr| match fr {
832                adk_gemini::FinishReason::Stop => FinishReason::Stop,
833                adk_gemini::FinishReason::MaxTokens => FinishReason::MaxTokens,
834                adk_gemini::FinishReason::Safety => FinishReason::Safety,
835                adk_gemini::FinishReason::Recitation => FinishReason::Recitation,
836                _ => FinishReason::Other,
837            });
838
839        let citation_metadata =
840            resp.candidates.first().and_then(|c| c.citation_metadata.as_ref()).map(|meta| {
841                CitationMetadata {
842                    citation_sources: meta
843                        .citation_sources
844                        .iter()
845                        .map(|source| CitationSource {
846                            uri: source.uri.clone(),
847                            title: source.title.clone(),
848                            start_index: source.start_index,
849                            end_index: source.end_index,
850                            license: source.license.clone(),
851                            publication_date: source.publication_date.map(|d| d.to_string()),
852                        })
853                        .collect(),
854                }
855            });
856
857        // Serialize grounding metadata into provider_metadata so consumers
858        // can access structured grounding data (search queries, sources, supports).
859        let provider_metadata = resp
860            .candidates
861            .first()
862            .and_then(|c| c.grounding_metadata.as_ref())
863            .and_then(|g| serde_json::to_value(g).ok());
864
865        Ok(LlmResponse {
866            content,
867            usage_metadata,
868            finish_reason,
869            citation_metadata,
870            partial: false,
871            turn_complete: !tool_call_turn,
872            interrupted: false,
873            error_code: None,
874            error_message: None,
875            provider_metadata,
876            interaction_id: None,
877        })
878    }
879
880    fn gemini_function_response_payload(response: serde_json::Value) -> serde_json::Value {
881        match response {
882            // Gemini functionResponse.response must be a JSON object.
883            serde_json::Value::Object(_) => response,
884            other => serde_json::json!({ "result": other }),
885        }
886    }
887
888    fn merge_object_value(
889        target: &mut serde_json::Map<String, serde_json::Value>,
890        value: serde_json::Value,
891    ) {
892        if let serde_json::Value::Object(object) = value {
893            for (key, value) in object {
894                target.insert(key, value);
895            }
896        }
897    }
898
899    fn build_gemini_tools(
900        tools: &std::collections::HashMap<String, serde_json::Value>,
901        adapter: &dyn SchemaAdapter,
902        cache: &SchemaCache,
903    ) -> Result<(Vec<adk_gemini::Tool>, adk_gemini::ToolConfig)> {
904        let mut gemini_tools = Vec::new();
905        let mut function_declarations = Vec::new();
906        let mut has_provider_native_tools = false;
907        let mut tool_config_json = serde_json::Map::new();
908
909        for (name, tool_decl) in tools {
910            if let Some(provider_tool) = tool_decl.get("x-adk-gemini-tool") {
911                let tool = serde_json::from_value::<adk_gemini::Tool>(provider_tool.clone())
912                    .map_err(|error| {
913                        adk_core::AdkError::model(format!(
914                            "failed to deserialize Gemini native tool '{name}': {error}"
915                        ))
916                    })?;
917                has_provider_native_tools = true;
918                gemini_tools.push(tool);
919            } else {
920                // Normalize tool name via the schema adapter
921                let normalized_name = adapter.normalize_tool_name(name);
922
923                // Get the parameters schema from the declaration, or use the
924                // adapter's empty_schema fallback when none is provided.
925                let schema =
926                    tool_decl.get("parameters").cloned().unwrap_or_else(|| adapter.empty_schema());
927                let normalized_schema = cache.normalize(&schema);
928
929                // Build the FunctionDeclaration with normalized values
930                let description =
931                    tool_decl.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
932
933                let mut func_decl_json = serde_json::json!({
934                    "name": normalized_name.as_ref(),
935                    "description": description,
936                    "parameters": normalized_schema,
937                });
938
939                // Preserve response schema if present (normalized like parameters)
940                if let Some(response) = tool_decl.get("response") {
941                    func_decl_json["response"] = cache.normalize(response);
942                }
943
944                // Preserve behavior if present
945                if let Some(behavior) = tool_decl.get("behavior") {
946                    func_decl_json["behavior"] = behavior.clone();
947                }
948
949                let func_decl =
950                    serde_json::from_value::<adk_gemini::FunctionDeclaration>(func_decl_json)
951                        .map_err(|error| {
952                            adk_core::AdkError::model(format!(
953                                "failed to build Gemini function declaration for '{name}': {error}"
954                            ))
955                        })?;
956                function_declarations.push(func_decl);
957            }
958
959            if let Some(tool_config) = tool_decl.get("x-adk-gemini-tool-config") {
960                Self::merge_object_value(&mut tool_config_json, tool_config.clone());
961            }
962        }
963
964        let has_function_declarations = !function_declarations.is_empty();
965        if has_function_declarations {
966            gemini_tools.push(adk_gemini::Tool::with_functions(function_declarations));
967        }
968
969        if has_provider_native_tools {
970            tool_config_json.insert(
971                "includeServerSideToolInvocations".to_string(),
972                serde_json::Value::Bool(true),
973            );
974        }
975
976        let tool_config = if tool_config_json.is_empty() {
977            adk_gemini::ToolConfig::default()
978        } else {
979            serde_json::from_value::<adk_gemini::ToolConfig>(serde_json::Value::Object(
980                tool_config_json,
981            ))
982            .map_err(|error| {
983                adk_core::AdkError::model(format!(
984                    "failed to deserialize Gemini tool configuration: {error}"
985                ))
986            })?
987        };
988
989        Ok((gemini_tools, tool_config))
990    }
991
992    fn stream_chunks_from_response(
993        mut response: LlmResponse,
994        saw_partial_chunk: bool,
995    ) -> (Vec<LlmResponse>, bool) {
996        let is_final = response.finish_reason.is_some();
997
998        if !is_final {
999            response.partial = true;
1000            response.turn_complete = false;
1001            return (vec![response], true);
1002        }
1003
1004        response.partial = false;
1005        response.turn_complete =
1006            response.content.as_ref().is_none_or(|content| !content.has_function_calls());
1007
1008        if saw_partial_chunk {
1009            return (vec![response], true);
1010        }
1011
1012        let synthetic_partial = LlmResponse {
1013            content: None,
1014            usage_metadata: None,
1015            finish_reason: None,
1016            citation_metadata: None,
1017            partial: true,
1018            turn_complete: false,
1019            interrupted: false,
1020            error_code: None,
1021            error_message: None,
1022            provider_metadata: None,
1023            interaction_id: None,
1024        };
1025
1026        (vec![synthetic_partial, response], true)
1027    }
1028
1029    async fn generate_content_internal(
1030        &self,
1031        req: LlmRequest,
1032        stream: bool,
1033    ) -> Result<LlmResponseStream> {
1034        let mut builder = self.client.generate_content();
1035
1036        // Build a map of function_name → thought_signature from FunctionCall parts
1037        // in model content. Gemini 3.x requires thought_signature on FunctionResponse
1038        // parts when thinking is active, but adk_core::Part::FunctionResponse doesn't
1039        // carry it (it's Gemini-specific). We recover it here at the provider boundary.
1040        let mut fn_call_signatures: std::collections::HashMap<String, String> =
1041            std::collections::HashMap::new();
1042        for content in &req.contents {
1043            if content.role == "model" {
1044                for part in &content.parts {
1045                    if let Part::FunctionCall { name, thought_signature: Some(sig), .. } = part {
1046                        fn_call_signatures.insert(name.clone(), sig.clone());
1047                    }
1048                }
1049            }
1050        }
1051
1052        // Add contents using proper builder methods
1053        for content in &req.contents {
1054            match content.role.as_str() {
1055                "user" => {
1056                    // For user messages, build gemini Content with potentially multiple parts
1057                    let mut gemini_parts = Vec::new();
1058                    for part in &content.parts {
1059                        match part {
1060                            Part::Text { text } => {
1061                                gemini_parts.push(adk_gemini::Part::Text {
1062                                    text: text.clone(),
1063                                    thought: None,
1064                                    thought_signature: None,
1065                                });
1066                            }
1067                            Part::Thinking { thinking, signature } => {
1068                                gemini_parts.push(adk_gemini::Part::Text {
1069                                    text: thinking.clone(),
1070                                    thought: Some(true),
1071                                    thought_signature: signature.clone(),
1072                                });
1073                            }
1074                            Part::InlineData { data, mime_type, .. } => {
1075                                let encoded = attachment::encode_base64(data);
1076                                gemini_parts.push(adk_gemini::Part::InlineData {
1077                                    inline_data: adk_gemini::Blob {
1078                                        mime_type: mime_type.clone(),
1079                                        data: encoded,
1080                                    },
1081                                });
1082                            }
1083                            Part::FileData { mime_type, file_uri, .. } => {
1084                                gemini_parts.push(adk_gemini::Part::Text {
1085                                    text: attachment::file_attachment_to_text(mime_type, file_uri),
1086                                    thought: None,
1087                                    thought_signature: None,
1088                                });
1089                            }
1090                            _ => {}
1091                        }
1092                    }
1093                    if !gemini_parts.is_empty() {
1094                        let user_content = adk_gemini::Content {
1095                            role: Some(adk_gemini::Role::User),
1096                            parts: Some(gemini_parts),
1097                        };
1098                        builder = builder.with_message(adk_gemini::Message {
1099                            content: user_content,
1100                            role: adk_gemini::Role::User,
1101                        });
1102                    }
1103                }
1104                "model" => {
1105                    // For model messages, build gemini Content
1106                    let mut gemini_parts = Vec::new();
1107                    for part in &content.parts {
1108                        match part {
1109                            Part::Text { text } => {
1110                                gemini_parts.push(adk_gemini::Part::Text {
1111                                    text: text.clone(),
1112                                    thought: None,
1113                                    thought_signature: None,
1114                                });
1115                            }
1116                            Part::Thinking { thinking, signature } => {
1117                                gemini_parts.push(adk_gemini::Part::Text {
1118                                    text: thinking.clone(),
1119                                    thought: Some(true),
1120                                    thought_signature: signature.clone(),
1121                                });
1122                            }
1123                            Part::FunctionCall { name, args, thought_signature, id } => {
1124                                gemini_parts.push(adk_gemini::Part::FunctionCall {
1125                                    function_call: adk_gemini::FunctionCall {
1126                                        name: name.clone(),
1127                                        args: args.clone(),
1128                                        id: id.clone(),
1129                                        thought_signature: None,
1130                                    },
1131                                    thought_signature: thought_signature.clone(),
1132                                });
1133                            }
1134                            Part::ServerToolCall { server_tool_call } => {
1135                                if let Ok(native_part) = serde_json::from_value::<adk_gemini::Part>(
1136                                    server_tool_call.clone(),
1137                                ) {
1138                                    match native_part {
1139                                        adk_gemini::Part::ToolCall { .. }
1140                                        | adk_gemini::Part::ExecutableCode { .. } => {
1141                                            gemini_parts.push(native_part);
1142                                            continue;
1143                                        }
1144                                        _ => {}
1145                                    }
1146                                }
1147
1148                                gemini_parts.push(adk_gemini::Part::ToolCall {
1149                                    tool_call: server_tool_call.clone(),
1150                                    thought_signature: Self::gemini_part_thought_signature(
1151                                        server_tool_call,
1152                                    ),
1153                                });
1154                            }
1155                            Part::ServerToolResponse { server_tool_response } => {
1156                                if let Ok(native_part) = serde_json::from_value::<adk_gemini::Part>(
1157                                    server_tool_response.clone(),
1158                                ) {
1159                                    match native_part {
1160                                        adk_gemini::Part::ToolResponse { .. }
1161                                        | adk_gemini::Part::CodeExecutionResult { .. } => {
1162                                            gemini_parts.push(native_part);
1163                                            continue;
1164                                        }
1165                                        _ => {}
1166                                    }
1167                                }
1168
1169                                gemini_parts.push(adk_gemini::Part::ToolResponse {
1170                                    tool_response: server_tool_response.clone(),
1171                                    thought_signature: Self::gemini_part_thought_signature(
1172                                        server_tool_response,
1173                                    ),
1174                                });
1175                            }
1176                            _ => {}
1177                        }
1178                    }
1179                    if !gemini_parts.is_empty() {
1180                        let model_content = adk_gemini::Content {
1181                            role: Some(adk_gemini::Role::Model),
1182                            parts: Some(gemini_parts),
1183                        };
1184                        builder = builder.with_message(adk_gemini::Message {
1185                            content: model_content,
1186                            role: adk_gemini::Role::Model,
1187                        });
1188                    }
1189                }
1190                "function" => {
1191                    // For function responses, build content directly to attach thought_signature
1192                    // recovered from the preceding FunctionCall (Gemini 3.x requirement)
1193                    let mut gemini_parts = Vec::new();
1194                    for part in &content.parts {
1195                        if let Part::FunctionResponse { function_response, id, .. } = part {
1196                            let sig = fn_call_signatures.get(&function_response.name).cloned();
1197
1198                            // Build nested FunctionResponsePart entries for multimodal data
1199                            let mut fr_parts = Vec::new();
1200                            for inline in &function_response.inline_data {
1201                                let encoded = attachment::encode_base64(&inline.data);
1202                                fr_parts.push(adk_gemini::FunctionResponsePart::InlineData {
1203                                    inline_data: adk_gemini::Blob {
1204                                        mime_type: inline.mime_type.clone(),
1205                                        data: encoded,
1206                                    },
1207                                });
1208                            }
1209                            for file in &function_response.file_data {
1210                                fr_parts.push(adk_gemini::FunctionResponsePart::FileData {
1211                                    file_data: adk_gemini::FileDataRef {
1212                                        mime_type: file.mime_type.clone(),
1213                                        file_uri: file.file_uri.clone(),
1214                                    },
1215                                });
1216                            }
1217
1218                            let mut gemini_fr = adk_gemini::tools::FunctionResponse::new(
1219                                &function_response.name,
1220                                Self::gemini_function_response_payload(
1221                                    function_response.response.clone(),
1222                                ),
1223                            );
1224                            gemini_fr.parts = fr_parts;
1225                            // Echo the call id so Gemini 3.x strict response matching
1226                            // (id + name + count) can correlate this response.
1227                            gemini_fr.id = id.clone();
1228
1229                            gemini_parts.push(adk_gemini::Part::FunctionResponse {
1230                                function_response: gemini_fr,
1231                                thought_signature: sig,
1232                            });
1233                        }
1234                    }
1235                    if !gemini_parts.is_empty() {
1236                        let fn_content = adk_gemini::Content {
1237                            role: Some(adk_gemini::Role::User),
1238                            parts: Some(gemini_parts),
1239                        };
1240                        builder = builder.with_message(adk_gemini::Message {
1241                            content: fn_content,
1242                            role: adk_gemini::Role::User,
1243                        });
1244                    }
1245                }
1246                _ => {}
1247            }
1248        }
1249
1250        // Add generation config
1251        if let Some(config) = req.config {
1252            let has_schema = config.response_schema.is_some();
1253            let gen_config = adk_gemini::GenerationConfig {
1254                temperature: config.temperature,
1255                top_p: config.top_p,
1256                top_k: config.top_k,
1257                max_output_tokens: config.max_output_tokens,
1258                response_schema: config.response_schema,
1259                response_mime_type: if has_schema {
1260                    Some("application/json".to_string())
1261                } else {
1262                    None
1263                },
1264                thinking_config: self.thinking_config.clone(),
1265                ..Default::default()
1266            };
1267            builder = builder.with_generation_config(gen_config);
1268
1269            // Attach cached content reference if provided
1270            if let Some(ref name) = config.cached_content {
1271                let handle = self.client.get_cached_content(name);
1272                builder = builder.with_cached_content(&handle);
1273            }
1274        } else if self.thinking_config.is_some() {
1275            // No generation config from the request, but we have a default
1276            // thinking config — apply it in an otherwise-default gen config.
1277            let gen_config = adk_gemini::GenerationConfig {
1278                thinking_config: self.thinking_config.clone(),
1279                ..Default::default()
1280            };
1281            builder = builder.with_generation_config(gen_config);
1282        }
1283
1284        // Add tools
1285        if !req.tools.is_empty() {
1286            let adapter = self.schema_adapter();
1287            use std::sync::LazyLock;
1288            static SCHEMA_CACHE: LazyLock<SchemaCache> = LazyLock::new(|| {
1289                SchemaCache::for_adapter(std::sync::Arc::new(GeminiSchemaAdapter::new()))
1290            });
1291            let (gemini_tools, tool_config) =
1292                Self::build_gemini_tools(&req.tools, adapter, &SCHEMA_CACHE)?;
1293            for tool in gemini_tools {
1294                builder = builder.with_tool(tool);
1295            }
1296            if tool_config != adk_gemini::ToolConfig::default() {
1297                builder = builder.with_tool_config(tool_config);
1298            }
1299        }
1300
1301        // Declare server-side RAG grounding; the Vertex backend injects it
1302        // as a `retrieval` tool at send time (validate_request_contract has
1303        // already rejected the Studio backend at this point).
1304        #[cfg(feature = "gemini-vertex")]
1305        if let Some(store) = &self.vertex_rag_store {
1306            builder = builder.with_vertex_rag_store(store.clone());
1307        }
1308
1309        if stream {
1310            adk_telemetry::debug!("Executing streaming request");
1311            let response_stream = builder.execute_stream().await.map_err(|e| {
1312                adk_telemetry::error!(error = %e, "Model request failed");
1313                gemini_error_to_adk(&e)
1314            })?;
1315
1316            let mapped_stream = async_stream::stream! {
1317                let mut stream = response_stream;
1318                let mut saw_partial_chunk = false;
1319                while let Some(result) = stream.try_next().await.transpose() {
1320                    match result {
1321                        Ok(resp) => {
1322                            match Self::convert_response(&resp) {
1323                                Ok(llm_resp) => {
1324                                    let (chunks, next_saw_partial) =
1325                                        Self::stream_chunks_from_response(llm_resp, saw_partial_chunk);
1326                                    saw_partial_chunk = next_saw_partial;
1327                                    for chunk in chunks {
1328                                        yield Ok(chunk);
1329                                    }
1330                                }
1331                                Err(e) => {
1332                                    adk_telemetry::error!(error = %e, "Failed to convert response");
1333                                    yield Err(e);
1334                                }
1335                            }
1336                        }
1337                        Err(e) => {
1338                            adk_telemetry::error!(error = %e, "Stream error");
1339                            yield Err(gemini_error_to_adk(&e));
1340                        }
1341                    }
1342                }
1343            };
1344
1345            Ok(Box::pin(mapped_stream))
1346        } else {
1347            adk_telemetry::debug!("Executing blocking request");
1348            let response = builder.execute().await.map_err(|e| {
1349                adk_telemetry::error!(error = %e, "Model request failed");
1350                gemini_error_to_adk(&e)
1351            })?;
1352
1353            let llm_response = Self::convert_response(&response)?;
1354
1355            let stream = async_stream::stream! {
1356                yield Ok(llm_response);
1357            };
1358
1359            Ok(Box::pin(stream))
1360        }
1361    }
1362
1363    /// Create a cached content resource with the given system instruction, tools, and TTL.
1364    ///
1365    /// Returns the cache name (e.g., "cachedContents/abc123") on success.
1366    /// The cache is created using the model configured on this `GeminiModel` instance.
1367    pub async fn create_cached_content(
1368        &self,
1369        system_instruction: &str,
1370        tools: &std::collections::HashMap<String, serde_json::Value>,
1371        ttl_seconds: u32,
1372    ) -> Result<String> {
1373        let mut cache_builder = self
1374            .client
1375            .create_cache()
1376            .with_system_instruction(system_instruction)
1377            .with_ttl(std::time::Duration::from_secs(u64::from(ttl_seconds)));
1378
1379        let adapter = self.schema_adapter();
1380        use std::sync::LazyLock;
1381        static SCHEMA_CACHE: LazyLock<SchemaCache> = LazyLock::new(|| {
1382            SchemaCache::for_adapter(std::sync::Arc::new(GeminiSchemaAdapter::new()))
1383        });
1384        let (gemini_tools, tool_config) = Self::build_gemini_tools(tools, adapter, &SCHEMA_CACHE)?;
1385        if !gemini_tools.is_empty() {
1386            cache_builder = cache_builder.with_tools(gemini_tools);
1387        }
1388        if tool_config != adk_gemini::ToolConfig::default() {
1389            cache_builder = cache_builder.with_tool_config(tool_config);
1390        }
1391
1392        let handle = cache_builder
1393            .execute()
1394            .await
1395            .map_err(|e| adk_core::AdkError::model(format!("cache creation failed: {e}")))?;
1396
1397        Ok(handle.name().to_string())
1398    }
1399
1400    /// Delete a cached content resource by name.
1401    pub async fn delete_cached_content(&self, name: &str) -> Result<()> {
1402        let handle = self.client.get_cached_content(name);
1403        handle
1404            .delete()
1405            .await
1406            .map_err(|(_, e)| adk_core::AdkError::model(format!("cache deletion failed: {e}")))?;
1407        Ok(())
1408    }
1409
1410    /// Drives a single turn through the Interactions API (Beta), non-streaming.
1411    ///
1412    /// This is the Interactions counterpart to
1413    /// [`generate_content_internal`](Self::generate_content_internal). It builds
1414    /// a [`CreateInteractionRequest`](adk_gemini::interactions::CreateInteractionRequest)
1415    /// from `req` via [`interactions_convert::build_request`], sends it, polls to
1416    /// completion when running in the background, maps terminal failure statuses
1417    /// to errors, and converts the final interaction into a single
1418    /// [`LlmResponse`].
1419    ///
1420    /// Behavior of note:
1421    ///
1422    /// - **Background completion (Requirement 7.5).** When `background` is set
1423    ///   and the first response is neither terminal nor awaiting a tool result,
1424    ///   the interaction is polled via
1425    ///   [`get_interaction`](adk_gemini::Gemini::get_interaction) every
1426    ///   `poll_interval` until it reaches a terminal or `requires_action` state.
1427    /// - **Stale continuation fallback (Requirement 4.4).** If the initial send
1428    ///   fails with a `NotFound` error *and* the request carried a
1429    ///   `previous_response_id`, the request is transparently rebuilt without
1430    ///   stateful continuation (full transcript, no `previous_interaction_id`)
1431    ///   and re-sent once. The original `NotFound` is not surfaced.
1432    /// - **Terminal failure (Requirements 7.7 / 9.2).** A final `failed` /
1433    ///   `budget_exceeded` status becomes an [`adk_core::AdkError`].
1434    ///
1435    /// The streaming counterpart is
1436    /// [`generate_interactions_stream`](Self::generate_interactions_stream).
1437    #[cfg(feature = "gemini-interactions")]
1438    async fn generate_interactions_once(&self, req: LlmRequest) -> Result<LlmResponse> {
1439        use super::interactions_convert;
1440
1441        // The Interactions target is always populated when the transport is
1442        // active (set by `use_interactions_api`); guard defensively.
1443        let target = self.interaction_target.as_ref().ok_or_else(|| {
1444            adk_core::AdkError::new(
1445                ErrorComponent::Model,
1446                ErrorCategory::InvalidInput,
1447                "model.gemini.interactions.missing_target",
1448                "the Interactions transport is active but no validated target is configured",
1449            )
1450            .with_provider("gemini")
1451        })?;
1452
1453        // Resolve the thinking level (Gemini 3 level-based reasoning). Budget-only
1454        // configs (Gemini 2.5) carry no level, so this stays `None` for them.
1455        let thinking_level = self.thinking_config.as_ref().and_then(|c| c.thinking_level);
1456
1457        let stateful = self.interaction_options.stateful;
1458        let store = self.interaction_options.store;
1459        let background = self.resolve_background();
1460
1461        // Build the request and stamp the background flag.
1462        let mut request =
1463            interactions_convert::build_request(&req, target, thinking_level, stateful, store)?;
1464        request.background = Some(background);
1465
1466        // Send, with a transparent transcript fallback for a stale continuation id.
1467        let interaction = match self.client.send_interaction(request.clone()).await {
1468            Ok(interaction) => interaction,
1469            Err(error) => {
1470                let mapped = gemini_error_to_adk(&error);
1471                // Requirement 4.4: a rejected `previous_interaction_id` (retention
1472                // expiry) maps to NotFound. Rebuild statelessly (full transcript,
1473                // no continuation id) and retry once, without surfacing the error.
1474                if mapped.category == ErrorCategory::NotFound && req.previous_response_id.is_some()
1475                {
1476                    let mut fallback = interactions_convert::build_request(
1477                        &req,
1478                        target,
1479                        thinking_level,
1480                        /* stateful */ false,
1481                        store,
1482                    )?;
1483                    fallback.background = Some(background);
1484                    self.client
1485                        .send_interaction(fallback)
1486                        .await
1487                        .map_err(|e| gemini_error_to_adk(&e))?
1488                } else {
1489                    return Err(mapped);
1490                }
1491            }
1492        };
1493
1494        // Background completion: poll until terminal or awaiting a tool result.
1495        let final_interaction = if background {
1496            self.poll_interaction_to_completion(interaction).await?
1497        } else {
1498            interaction
1499        };
1500
1501        // Requirements 7.7 / 9.2: surface terminal failure statuses as errors.
1502        interaction_status_to_result(final_interaction.status)?;
1503
1504        Ok(interactions_convert::to_llm_response(&final_interaction))
1505    }
1506
1507    /// Polls a background interaction until it reaches a terminal or
1508    /// `requires_action` state, honoring the configured `poll_interval`
1509    /// (Requirement 7.5).
1510    ///
1511    /// Returns immediately when the interaction is already terminal or awaiting
1512    /// a tool result. Otherwise it sleeps for `poll_interval` and re-fetches via
1513    /// [`get_interaction`](adk_gemini::Gemini::get_interaction) (with
1514    /// `include_input = false`) until one of those states is reached, or until a
1515    /// bounded safeguard (`MAX_POLL_ATTEMPTS`) trips.
1516    ///
1517    /// ## Cancellation (Requirement 7.6)
1518    ///
1519    /// True invocation-driven cancellation (calling
1520    /// [`cancel_interaction`](adk_gemini::Gemini::cancel_interaction) when the
1521    /// caller cancels) is **not reachable from this layer**: the [`Llm`] trait's
1522    /// [`generate_content`](Llm::generate_content) signature receives only an
1523    /// [`LlmRequest`] and a `stream` flag — it has no `InvocationContext` or
1524    /// cancellation token. Cancellation is handled by the runner at the
1525    /// event-stream boundary: when a run is cancelled the runner stops consuming
1526    /// the agent's event stream, which drops this future and cancels its
1527    /// `await` points (the in-flight `sleep` / `get_interaction`) cooperatively.
1528    /// The polled interaction is left running server-side; reviving it to issue
1529    /// an explicit `cancel_interaction` would require threading the cancellation
1530    /// token through the trait, which is intentionally out of scope here (the
1531    /// trait is transport-only and shared by every provider).
1532    ///
1533    /// The `MAX_POLL_ATTEMPTS` bound is a safeguard against an interaction that
1534    /// never reaches a terminal/`requires_action` state (e.g. a server-side
1535    /// stall): rather than looping forever it returns a [`ErrorCategory::Timeout`]
1536    /// error so the call fails fast instead of hanging.
1537    #[cfg(feature = "gemini-interactions")]
1538    async fn poll_interaction_to_completion(
1539        &self,
1540        interaction: adk_gemini::interactions::Interaction,
1541    ) -> Result<adk_gemini::interactions::Interaction> {
1542        /// Upper bound on poll iterations before giving up, guarding against an
1543        /// interaction that never settles. With the default 1s `poll_interval`
1544        /// this is ~10 minutes; shorter intervals trade latency for a tighter
1545        /// wall-clock cap. Deep Research agents complete well within this.
1546        const MAX_POLL_ATTEMPTS: u32 = 600;
1547
1548        let mut current = interaction;
1549        let mut attempts: u32 = 0;
1550        while !current.status.is_terminal() && !current.status.requires_action() {
1551            if attempts >= MAX_POLL_ATTEMPTS {
1552                return Err(adk_core::AdkError::new(
1553                    ErrorComponent::Model,
1554                    ErrorCategory::Timeout,
1555                    "model.gemini.interactions.poll_timeout",
1556                    format!(
1557                        "the Gemini interaction did not reach a terminal or requires_action \
1558                         state after {MAX_POLL_ATTEMPTS} poll attempts"
1559                    ),
1560                )
1561                .with_provider("gemini"));
1562            }
1563            attempts += 1;
1564            tokio::time::sleep(self.interaction_options.poll_interval).await;
1565            current = self
1566                .client
1567                .get_interaction(&current.id, false)
1568                .await
1569                .map_err(|e| gemini_error_to_adk(&e))?;
1570        }
1571        Ok(current)
1572    }
1573
1574    /// Drives a single turn through the Interactions API (Beta) as an SSE
1575    /// stream, yielding partial→final [`LlmResponse`] chunks (Requirement 7.4).
1576    ///
1577    /// This is the streaming counterpart to
1578    /// [`generate_interactions_once`](Self::generate_interactions_once). It
1579    /// builds the request the same way, forces non-background completion
1580    /// (streaming and background polling are mutually exclusive completion
1581    /// modes — SSE delivers the turn incrementally, so `background` is set to
1582    /// `false`), opens the SSE stream via
1583    /// [`send_interaction_stream`](adk_gemini::Gemini::send_interaction_stream),
1584    /// and folds each [`InteractionSseEvent`](adk_gemini::interactions::InteractionSseEvent)
1585    /// into chunks via [`interactions_convert::sse_event_to_chunk`].
1586    ///
1587    /// Stream setup (target resolution, request building, opening the SSE
1588    /// connection) is fallible and returns `Err` synchronously, so
1589    /// `execute_with_retry` (which wraps this in `generate_content`) can retry
1590    /// transient setup failures. Errors that occur *after* the stream starts
1591    /// are yielded into the stream and not retried, mirroring the
1592    /// generateContent streaming path.
1593    ///
1594    /// The stale-continuation fallback used by the non-streaming path is not
1595    /// applied here: a `NotFound` on stream setup surfaces as a normal error
1596    /// (the streaming path is opt-in and lower-level; callers that need
1597    /// transparent retention fallback use the default non-streaming path).
1598    #[cfg(feature = "gemini-interactions")]
1599    async fn generate_interactions_stream(&self, req: LlmRequest) -> Result<LlmResponseStream> {
1600        use super::interactions_convert::{self, SseAccumulator, sse_event_to_chunk};
1601
1602        let target = self.interaction_target.as_ref().ok_or_else(|| {
1603            adk_core::AdkError::new(
1604                ErrorComponent::Model,
1605                ErrorCategory::InvalidInput,
1606                "model.gemini.interactions.missing_target",
1607                "the Interactions transport is active but no validated target is configured",
1608            )
1609            .with_provider("gemini")
1610        })?;
1611
1612        let thinking_level = self.thinking_config.as_ref().and_then(|c| c.thinking_level);
1613        let stateful = self.interaction_options.stateful;
1614        let store = self.interaction_options.store;
1615
1616        let mut request =
1617            interactions_convert::build_request(&req, target, thinking_level, stateful, store)?;
1618        // Streaming uses SSE for incremental completion, not background polling;
1619        // the two are distinct completion modes (Requirement 7.4 vs 7.5). Force
1620        // foreground so the server streams the turn rather than returning a
1621        // background handle.
1622        request.background = Some(false);
1623
1624        let sse_stream = self
1625            .client
1626            .send_interaction_stream(request)
1627            .await
1628            .map_err(|e| gemini_error_to_adk(&e))?;
1629
1630        let mapped = async_stream::stream! {
1631            let mut sse_stream = sse_stream;
1632            let mut acc = SseAccumulator::new();
1633            while let Some(result) = sse_stream.try_next().await.transpose() {
1634                match result {
1635                    Ok(event) => {
1636                        if let Some(chunk) = sse_event_to_chunk(event, &mut acc) {
1637                            yield chunk;
1638                        }
1639                    }
1640                    Err(e) => {
1641                        adk_telemetry::error!(error = %e, "Interaction stream error");
1642                        yield Err(gemini_error_to_adk(&e));
1643                    }
1644                }
1645            }
1646        };
1647
1648        Ok(Box::pin(mapped))
1649    }
1650}
1651
1652#[async_trait]
1653impl Llm for GeminiModel {
1654    fn name(&self) -> &str {
1655        &self.model_name
1656    }
1657
1658    fn schema_adapter(&self) -> &dyn SchemaAdapter {
1659        use std::sync::LazyLock;
1660        static ADAPTER: LazyLock<GeminiSchemaAdapter> = LazyLock::new(GeminiSchemaAdapter::new);
1661        &*ADAPTER
1662    }
1663
1664    #[cfg(feature = "gemini-interactions")]
1665    fn uses_interactions_api(&self) -> bool {
1666        self.transport == GeminiTransport::Interactions
1667    }
1668
1669    // Named distinctly from the agent layer's `call_llm` span (which carries
1670    // the gcp.vertex.agent.* attributes and is what trace exporters capture):
1671    // this is the model-transport layer, so traces show one `call_llm` per
1672    // LLM call instead of an identical nested pair.
1673    #[adk_telemetry::instrument(
1674        name = "model.generate_content",
1675        skip(self, req),
1676        fields(
1677            model.name = %self.model_name,
1678            stream = %stream,
1679            request.contents_count = %req.contents.len(),
1680            request.tools_count = %req.tools.len()
1681        )
1682    )]
1683    async fn generate_content(&self, req: LlmRequest, stream: bool) -> Result<LlmResponseStream> {
1684        adk_telemetry::info!("Generating content");
1685        self.validate_request_contract(&req)?;
1686        let usage_span = adk_telemetry::llm_generate_span("gemini", &self.model_name, stream);
1687
1688        // Dispatch on the configured transport. The default `GenerateContent`
1689        // path is unchanged; the Interactions path (task 7.3/7.4) is only
1690        // reachable when `use_interactions_api` switched the transport.
1691        //
1692        // Retries only cover request setup/execution. Stream failures after the
1693        // stream starts are yielded to the caller and are not replayed
1694        // automatically.
1695        #[cfg(feature = "gemini-interactions")]
1696        if self.transport == GeminiTransport::Interactions {
1697            // Streaming and non-streaming are distinct completion modes. The
1698            // streaming path consumes the Interactions SSE stream and yields
1699            // partial→final chunks; the non-streaming path sends a single
1700            // request and (optionally) polls a background interaction to
1701            // completion. `execute_with_retry` wraps only request *setup* in
1702            // both cases — once a stream starts, its mid-flight errors are
1703            // surfaced to the caller rather than replayed (mirroring the
1704            // generateContent streaming path).
1705            if stream {
1706                let mapped =
1707                    execute_with_retry(&self.retry_config, is_retryable_model_error, || {
1708                        self.generate_interactions_stream(req.clone())
1709                    })
1710                    .await?;
1711                return Ok(crate::usage_tracking::with_usage_tracking(mapped, usage_span));
1712            }
1713            let response = execute_with_retry(&self.retry_config, is_retryable_model_error, || {
1714                self.generate_interactions_once(req.clone())
1715            })
1716            .await?;
1717            let single = async_stream::stream! {
1718                yield Ok(response);
1719            };
1720            return Ok(crate::usage_tracking::with_usage_tracking(Box::pin(single), usage_span));
1721        }
1722
1723        let result = execute_with_retry(&self.retry_config, is_retryable_model_error, || {
1724            self.generate_content_internal(req.clone(), stream)
1725        })
1726        .await?;
1727        Ok(crate::usage_tracking::with_usage_tracking(result, usage_span))
1728    }
1729}
1730
1731#[cfg(test)]
1732mod native_tool_tests {
1733    use super::*;
1734
1735    fn test_adapter() -> GeminiSchemaAdapter {
1736        GeminiSchemaAdapter::new()
1737    }
1738
1739    fn test_cache() -> SchemaCache {
1740        SchemaCache::for_adapter(std::sync::Arc::new(GeminiSchemaAdapter::new()))
1741    }
1742
1743    #[test]
1744    fn test_build_gemini_tools_supports_native_tool_metadata() {
1745        let mut tools = std::collections::HashMap::new();
1746        tools.insert(
1747            "google_search".to_string(),
1748            serde_json::json!({
1749                "x-adk-gemini-tool": {
1750                    "google_search": {}
1751                }
1752            }),
1753        );
1754        tools.insert(
1755            "lookup_weather".to_string(),
1756            serde_json::json!({
1757                "name": "lookup_weather",
1758                "description": "lookup weather",
1759                "parameters": {
1760                    "type": "object",
1761                    "properties": {
1762                        "city": { "type": "string" }
1763                    }
1764                }
1765            }),
1766        );
1767
1768        let adapter = test_adapter();
1769        let cache = test_cache();
1770        let (gemini_tools, tool_config) = GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1771            .expect("tool conversion should succeed");
1772
1773        assert_eq!(gemini_tools.len(), 2);
1774        assert_eq!(tool_config.include_server_side_tool_invocations, Some(true));
1775    }
1776
1777    #[test]
1778    fn test_build_gemini_tools_sets_flag_for_builtin_only() {
1779        let mut tools = std::collections::HashMap::new();
1780        tools.insert(
1781            "google_search".to_string(),
1782            serde_json::json!({
1783                "x-adk-gemini-tool": {
1784                    "google_search": {}
1785                }
1786            }),
1787        );
1788
1789        let adapter = test_adapter();
1790        let cache = test_cache();
1791        let (_gemini_tools, tool_config) =
1792            GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1793                .expect("tool conversion should succeed");
1794
1795        assert_eq!(
1796            tool_config.include_server_side_tool_invocations,
1797            Some(true),
1798            "includeServerSideToolInvocations should be set even with only built-in tools"
1799        );
1800    }
1801
1802    #[test]
1803    fn test_build_gemini_tools_no_flag_for_function_only() {
1804        let mut tools = std::collections::HashMap::new();
1805        tools.insert(
1806            "lookup_weather".to_string(),
1807            serde_json::json!({
1808                "name": "lookup_weather",
1809                "description": "lookup weather",
1810                "parameters": {
1811                    "type": "object",
1812                    "properties": {
1813                        "city": { "type": "string" }
1814                    }
1815                }
1816            }),
1817        );
1818
1819        let adapter = test_adapter();
1820        let cache = test_cache();
1821        let (_gemini_tools, tool_config) =
1822            GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1823                .expect("tool conversion should succeed");
1824
1825        assert_eq!(
1826            tool_config.include_server_side_tool_invocations, None,
1827            "includeServerSideToolInvocations should NOT be set for function-only tools"
1828        );
1829    }
1830
1831    #[test]
1832    fn test_build_gemini_tools_merges_native_tool_config() {
1833        let mut tools = std::collections::HashMap::new();
1834        tools.insert(
1835            "google_maps".to_string(),
1836            serde_json::json!({
1837                "x-adk-gemini-tool": {
1838                    "google_maps": {
1839                        "enable_widget": true
1840                    }
1841                },
1842                "x-adk-gemini-tool-config": {
1843                    "retrievalConfig": {
1844                        "latLng": {
1845                            "latitude": 1.23,
1846                            "longitude": 4.56
1847                        }
1848                    }
1849                }
1850            }),
1851        );
1852
1853        let adapter = test_adapter();
1854        let cache = test_cache();
1855        let (_gemini_tools, tool_config) =
1856            GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1857                .expect("tool conversion should succeed");
1858
1859        assert_eq!(
1860            tool_config.retrieval_config,
1861            Some(serde_json::json!({
1862                "latLng": {
1863                    "latitude": 1.23,
1864                    "longitude": 4.56
1865                }
1866            }))
1867        );
1868    }
1869
1870    #[test]
1871    fn test_response_schema_is_normalized_like_parameters() {
1872        // Regression test: MCP tools provide an output_schema that becomes the
1873        // `response` field in the tool declaration. Gemini rejects JSON-Schema
1874        // dialect fields ($schema, additionalProperties) in the response schema.
1875        // This test verifies that the response schema is normalized the same way
1876        // parameters are — stripping unsupported keywords.
1877        let mut tools = std::collections::HashMap::new();
1878        tools.insert(
1879            "read_file".to_string(),
1880            serde_json::json!({
1881                "name": "read_file",
1882                "description": "Read a file from the filesystem",
1883                "parameters": {
1884                    "$schema": "http://json-schema.org/draft-07/schema#",
1885                    "type": "object",
1886                    "properties": {
1887                        "path": { "type": "string", "description": "File path" }
1888                    },
1889                    "required": ["path"],
1890                    "additionalProperties": false
1891                },
1892                "response": {
1893                    "$schema": "http://json-schema.org/draft-07/schema#",
1894                    "type": "object",
1895                    "properties": {
1896                        "content": { "type": "string", "description": "File contents" }
1897                    },
1898                    "required": ["content"],
1899                    "additionalProperties": false
1900                }
1901            }),
1902        );
1903
1904        let adapter = test_adapter();
1905        let cache = test_cache();
1906        let (gemini_tools, _) = GeminiModel::build_gemini_tools(&tools, &adapter, &cache)
1907            .expect("tool conversion should succeed");
1908
1909        // Find the function declaration
1910        let func_tool = gemini_tools
1911            .iter()
1912            .find(|t| matches!(t, adk_gemini::Tool::Function { .. }))
1913            .expect("should have function declarations");
1914
1915        let decls = match func_tool {
1916            adk_gemini::Tool::Function { function_declarations } => function_declarations,
1917            _ => panic!("expected Function tool"),
1918        };
1919
1920        let decl = &decls[0];
1921        let decl_json = serde_json::to_value(decl).unwrap();
1922
1923        // Parameters should be normalized (no $schema, no additionalProperties)
1924        let params = &decl_json["parameters"];
1925        assert!(params.get("$schema").is_none(), "parameters.$schema should be stripped");
1926        assert!(
1927            params.get("additionalProperties").is_none(),
1928            "parameters.additionalProperties should be stripped"
1929        );
1930
1931        // Response should ALSO be normalized (this was the bug)
1932        let response = &decl_json["response"];
1933        assert!(
1934            response.get("$schema").is_none(),
1935            "response.$schema should be stripped (was the bug: copied raw)"
1936        );
1937        assert!(
1938            response.get("additionalProperties").is_none(),
1939            "response.additionalProperties should be stripped (was the bug: copied raw)"
1940        );
1941    }
1942}
1943
1944#[async_trait]
1945impl CacheCapable for GeminiModel {
1946    async fn create_cache(
1947        &self,
1948        system_instruction: &str,
1949        tools: &std::collections::HashMap<String, serde_json::Value>,
1950        ttl_seconds: u32,
1951    ) -> Result<String> {
1952        self.create_cached_content(system_instruction, tools, ttl_seconds).await
1953    }
1954
1955    async fn delete_cache(&self, name: &str) -> Result<()> {
1956        self.delete_cached_content(name).await
1957    }
1958}
1959
1960#[cfg(test)]
1961mod tests {
1962    use super::*;
1963    use adk_core::AdkError;
1964    use std::{
1965        sync::{
1966            Arc,
1967            atomic::{AtomicU32, Ordering},
1968        },
1969        time::Duration,
1970    };
1971
1972    #[test]
1973    fn env_flag_truthy_accepts_one_and_case_insensitive_true() {
1974        for truthy in ["1", "true", "TRUE", "True", " true ", "tRuE"] {
1975            assert!(env_flag_truthy(truthy), "expected {truthy:?} to be truthy");
1976        }
1977        for falsy in ["0", "false", "FALSE", "yes", "on", "", "2", "vertex"] {
1978            assert!(!env_flag_truthy(falsy), "expected {falsy:?} to be falsy");
1979        }
1980    }
1981
1982    #[test]
1983    fn constructor_is_backward_compatible_and_sync() {
1984        fn accepts_sync_constructor<F>(_f: F)
1985        where
1986            F: Fn(&str, &str) -> Result<GeminiModel>,
1987        {
1988        }
1989
1990        accepts_sync_constructor(|api_key, model| GeminiModel::new(api_key, model));
1991    }
1992
1993    #[test]
1994    fn stream_chunks_from_response_injects_partial_before_lone_final_chunk() {
1995        let response = LlmResponse {
1996            content: Some(Content::new("model").with_text("hello")),
1997            usage_metadata: None,
1998            finish_reason: Some(FinishReason::Stop),
1999            citation_metadata: None,
2000            partial: false,
2001            turn_complete: true,
2002            interrupted: false,
2003            error_code: None,
2004            error_message: None,
2005            provider_metadata: None,
2006            interaction_id: None,
2007        };
2008
2009        let (chunks, saw_partial) = GeminiModel::stream_chunks_from_response(response, false);
2010        assert!(saw_partial);
2011        assert_eq!(chunks.len(), 2);
2012        assert!(chunks[0].partial);
2013        assert!(!chunks[0].turn_complete);
2014        assert!(chunks[0].content.is_none());
2015        assert!(!chunks[1].partial);
2016        assert!(chunks[1].turn_complete);
2017    }
2018
2019    #[test]
2020    fn stream_chunks_from_response_keeps_final_only_when_partial_already_seen() {
2021        let response = LlmResponse {
2022            content: Some(Content::new("model").with_text("done")),
2023            usage_metadata: None,
2024            finish_reason: Some(FinishReason::Stop),
2025            citation_metadata: None,
2026            partial: false,
2027            turn_complete: true,
2028            interrupted: false,
2029            error_code: None,
2030            error_message: None,
2031            provider_metadata: None,
2032            interaction_id: None,
2033        };
2034
2035        let (chunks, saw_partial) = GeminiModel::stream_chunks_from_response(response, true);
2036        assert!(saw_partial);
2037        assert_eq!(chunks.len(), 1);
2038        assert!(!chunks[0].partial);
2039        assert!(chunks[0].turn_complete);
2040    }
2041
2042    #[test]
2043    fn stream_chunks_from_response_keeps_tool_call_turn_open() {
2044        let response = LlmResponse {
2045            content: Some(Content {
2046                role: "model".to_string(),
2047                parts: vec![Part::FunctionCall {
2048                    name: "get_weather".to_string(),
2049                    args: serde_json::json!({"city": "Boston"}),
2050                    id: Some("call-1".to_string()),
2051                    thought_signature: None,
2052                }],
2053            }),
2054            finish_reason: Some(FinishReason::Stop),
2055            turn_complete: true,
2056            ..Default::default()
2057        };
2058
2059        let (chunks, _) = GeminiModel::stream_chunks_from_response(response, true);
2060
2061        assert_eq!(chunks.len(), 1);
2062        assert!(!chunks[0].partial);
2063        assert!(!chunks[0].turn_complete);
2064    }
2065
2066    #[tokio::test]
2067    async fn execute_with_retry_retries_retryable_errors() {
2068        let retry_config = RetryConfig::default()
2069            .with_max_retries(2)
2070            .with_initial_delay(Duration::from_millis(0))
2071            .with_max_delay(Duration::from_millis(0));
2072        let attempts = Arc::new(AtomicU32::new(0));
2073
2074        let result = execute_with_retry(&retry_config, is_retryable_model_error, || {
2075            let attempts = Arc::clone(&attempts);
2076            async move {
2077                let attempt = attempts.fetch_add(1, Ordering::SeqCst);
2078                if attempt < 2 {
2079                    return Err(AdkError::model("code 429 RESOURCE_EXHAUSTED"));
2080                }
2081                Ok("ok")
2082            }
2083        })
2084        .await
2085        .expect("retry should eventually succeed");
2086
2087        assert_eq!(result, "ok");
2088        assert_eq!(attempts.load(Ordering::SeqCst), 3);
2089    }
2090
2091    #[tokio::test]
2092    async fn execute_with_retry_does_not_retry_non_retryable_errors() {
2093        let retry_config = RetryConfig::default()
2094            .with_max_retries(3)
2095            .with_initial_delay(Duration::from_millis(0))
2096            .with_max_delay(Duration::from_millis(0));
2097        let attempts = Arc::new(AtomicU32::new(0));
2098
2099        let error = execute_with_retry(&retry_config, is_retryable_model_error, || {
2100            let attempts = Arc::clone(&attempts);
2101            async move {
2102                attempts.fetch_add(1, Ordering::SeqCst);
2103                Err::<(), _>(AdkError::model("code 400 invalid request"))
2104            }
2105        })
2106        .await
2107        .expect_err("non-retryable error should return immediately");
2108
2109        assert!(error.is_model());
2110        assert_eq!(attempts.load(Ordering::SeqCst), 1);
2111    }
2112
2113    #[tokio::test]
2114    async fn execute_with_retry_respects_disabled_config() {
2115        let retry_config = RetryConfig::disabled().with_max_retries(10);
2116        let attempts = Arc::new(AtomicU32::new(0));
2117
2118        let error = execute_with_retry(&retry_config, is_retryable_model_error, || {
2119            let attempts = Arc::clone(&attempts);
2120            async move {
2121                attempts.fetch_add(1, Ordering::SeqCst);
2122                Err::<(), _>(AdkError::model("code 429 RESOURCE_EXHAUSTED"))
2123            }
2124        })
2125        .await
2126        .expect_err("disabled retries should return first error");
2127
2128        assert!(error.is_model());
2129        assert_eq!(attempts.load(Ordering::SeqCst), 1);
2130    }
2131
2132    #[test]
2133    fn convert_response_preserves_citation_metadata() {
2134        let response = adk_gemini::GenerationResponse {
2135            candidates: vec![adk_gemini::Candidate {
2136                content: adk_gemini::Content {
2137                    role: Some(adk_gemini::Role::Model),
2138                    parts: Some(vec![adk_gemini::Part::Text {
2139                        text: "hello world".to_string(),
2140                        thought: None,
2141                        thought_signature: None,
2142                    }]),
2143                },
2144                safety_ratings: None,
2145                citation_metadata: Some(adk_gemini::CitationMetadata {
2146                    citation_sources: vec![adk_gemini::CitationSource {
2147                        uri: Some("https://example.com".to_string()),
2148                        title: Some("Example".to_string()),
2149                        start_index: Some(0),
2150                        end_index: Some(5),
2151                        license: Some("CC-BY".to_string()),
2152                        publication_date: None,
2153                    }],
2154                }),
2155                grounding_metadata: None,
2156                finish_reason: Some(adk_gemini::FinishReason::Stop),
2157                index: Some(0),
2158            }],
2159            prompt_feedback: None,
2160            usage_metadata: None,
2161            model_version: None,
2162            response_id: None,
2163        };
2164
2165        let converted =
2166            GeminiModel::convert_response(&response).expect("conversion should succeed");
2167        let metadata = converted.citation_metadata.expect("citation metadata should be mapped");
2168        assert_eq!(metadata.citation_sources.len(), 1);
2169        assert_eq!(metadata.citation_sources[0].uri.as_deref(), Some("https://example.com"));
2170        assert_eq!(metadata.citation_sources[0].start_index, Some(0));
2171        assert_eq!(metadata.citation_sources[0].end_index, Some(5));
2172    }
2173
2174    #[test]
2175    fn convert_response_keeps_function_call_turn_open() {
2176        let response = adk_gemini::GenerationResponse {
2177            candidates: vec![adk_gemini::Candidate {
2178                content: adk_gemini::Content {
2179                    role: Some(adk_gemini::Role::Model),
2180                    parts: Some(vec![adk_gemini::Part::FunctionCall {
2181                        function_call: adk_gemini::FunctionCall {
2182                            name: "get_weather".to_string(),
2183                            args: serde_json::json!({"city": "Boston"}),
2184                            id: Some("call-1".to_string()),
2185                            thought_signature: None,
2186                        },
2187                        thought_signature: None,
2188                    }]),
2189                },
2190                safety_ratings: None,
2191                citation_metadata: None,
2192                grounding_metadata: None,
2193                finish_reason: Some(adk_gemini::FinishReason::Stop),
2194                index: Some(0),
2195            }],
2196            prompt_feedback: None,
2197            usage_metadata: None,
2198            model_version: None,
2199            response_id: None,
2200        };
2201
2202        let converted =
2203            GeminiModel::convert_response(&response).expect("conversion should succeed");
2204
2205        assert!(!converted.turn_complete);
2206        assert!(converted.content.is_some_and(|content| content.has_function_calls()));
2207    }
2208
2209    #[test]
2210    fn convert_response_handles_inline_data_from_model() {
2211        let image_bytes = vec![0x89, 0x50, 0x4E, 0x47];
2212        let encoded = crate::attachment::encode_base64(&image_bytes);
2213
2214        let response = adk_gemini::GenerationResponse {
2215            candidates: vec![adk_gemini::Candidate {
2216                content: adk_gemini::Content {
2217                    role: Some(adk_gemini::Role::Model),
2218                    parts: Some(vec![
2219                        adk_gemini::Part::Text {
2220                            text: "Here is the image".to_string(),
2221                            thought: None,
2222                            thought_signature: None,
2223                        },
2224                        adk_gemini::Part::InlineData {
2225                            inline_data: adk_gemini::Blob {
2226                                mime_type: "image/png".to_string(),
2227                                data: encoded,
2228                            },
2229                        },
2230                    ]),
2231                },
2232                safety_ratings: None,
2233                citation_metadata: None,
2234                grounding_metadata: None,
2235                finish_reason: Some(adk_gemini::FinishReason::Stop),
2236                index: Some(0),
2237            }],
2238            prompt_feedback: None,
2239            usage_metadata: None,
2240            model_version: None,
2241            response_id: None,
2242        };
2243
2244        let converted =
2245            GeminiModel::convert_response(&response).expect("conversion should succeed");
2246        let content = converted.content.expect("should have content");
2247        assert!(
2248            content
2249                .parts
2250                .iter()
2251                .any(|part| matches!(part, Part::Text { text } if text == "Here is the image"))
2252        );
2253        assert!(content.parts.iter().any(|part| {
2254            matches!(
2255                part,
2256                Part::InlineData { mime_type, data, .. }
2257                    if mime_type == "image/png" && data.as_slice() == image_bytes.as_slice()
2258            )
2259        }));
2260    }
2261
2262    #[test]
2263    fn gemini_function_response_payload_preserves_objects() {
2264        let value = serde_json::json!({
2265            "documents": [
2266                { "id": "pricing", "score": 0.91 }
2267            ]
2268        });
2269
2270        let payload = GeminiModel::gemini_function_response_payload(value.clone());
2271
2272        assert_eq!(payload, value);
2273    }
2274
2275    #[test]
2276    fn gemini_function_response_payload_wraps_arrays() {
2277        let payload =
2278            GeminiModel::gemini_function_response_payload(serde_json::json!([{ "id": "pricing" }]));
2279
2280        assert_eq!(payload, serde_json::json!({ "result": [{ "id": "pricing" }] }));
2281    }
2282
2283    // ===== Multimodal function response conversion tests =====
2284
2285    /// Helper to build a FunctionResponse with nested multimodal parts
2286    /// simulating the conversion logic from generate_content_internal.
2287    fn convert_function_response_to_gemini_fr(
2288        frd: &adk_core::FunctionResponseData,
2289    ) -> adk_gemini::tools::FunctionResponse {
2290        let mut fr_parts = Vec::new();
2291
2292        for inline in &frd.inline_data {
2293            let encoded = crate::attachment::encode_base64(&inline.data);
2294            fr_parts.push(adk_gemini::FunctionResponsePart::InlineData {
2295                inline_data: adk_gemini::Blob {
2296                    mime_type: inline.mime_type.clone(),
2297                    data: encoded,
2298                },
2299            });
2300        }
2301
2302        for file in &frd.file_data {
2303            fr_parts.push(adk_gemini::FunctionResponsePart::FileData {
2304                file_data: adk_gemini::FileDataRef {
2305                    mime_type: file.mime_type.clone(),
2306                    file_uri: file.file_uri.clone(),
2307                },
2308            });
2309        }
2310
2311        let mut gemini_fr = adk_gemini::tools::FunctionResponse::new(
2312            &frd.name,
2313            GeminiModel::gemini_function_response_payload(frd.response.clone()),
2314        );
2315        gemini_fr.parts = fr_parts;
2316        gemini_fr
2317    }
2318
2319    #[test]
2320    fn json_only_function_response_has_no_nested_parts() {
2321        let frd = adk_core::FunctionResponseData::new("tool", serde_json::json!({"ok": true}));
2322        let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2323        assert!(gemini_fr.parts.is_empty());
2324        // Serialized JSON should have name and response but no parts key
2325        let json = serde_json::to_string(&gemini_fr).unwrap();
2326        assert!(!json.contains("\"parts\""));
2327    }
2328
2329    #[test]
2330    fn function_response_with_inline_data_has_nested_parts() {
2331        let frd = adk_core::FunctionResponseData::with_inline_data(
2332            "chart",
2333            serde_json::json!({"status": "ok"}),
2334            vec![adk_core::InlineDataPart {
2335                mime_type: "image/png".to_string(),
2336                data: vec![0x89, 0x50, 0x4E, 0x47],
2337                uri: None,
2338                annotations: None,
2339            }],
2340        );
2341        let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2342        assert_eq!(gemini_fr.parts.len(), 1);
2343        match &gemini_fr.parts[0] {
2344            adk_gemini::FunctionResponsePart::InlineData { inline_data } => {
2345                assert_eq!(inline_data.mime_type, "image/png");
2346                let decoded = BASE64_STANDARD.decode(&inline_data.data).unwrap();
2347                assert_eq!(decoded, vec![0x89, 0x50, 0x4E, 0x47]);
2348            }
2349            other => panic!("expected InlineData, got {other:?}"),
2350        }
2351    }
2352
2353    #[test]
2354    fn function_response_with_file_data_has_nested_parts() {
2355        let frd = adk_core::FunctionResponseData::with_file_data(
2356            "doc",
2357            serde_json::json!({"ok": true}),
2358            vec![adk_core::FileDataPart {
2359                mime_type: "application/pdf".to_string(),
2360                file_uri: "gs://bucket/report.pdf".to_string(),
2361                annotations: None,
2362            }],
2363        );
2364        let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2365        assert_eq!(gemini_fr.parts.len(), 1);
2366        match &gemini_fr.parts[0] {
2367            adk_gemini::FunctionResponsePart::FileData { file_data } => {
2368                assert_eq!(file_data.mime_type, "application/pdf");
2369                assert_eq!(file_data.file_uri, "gs://bucket/report.pdf");
2370            }
2371            other => panic!("expected FileData, got {other:?}"),
2372        }
2373    }
2374
2375    #[test]
2376    fn function_response_with_both_inline_and_file_data_ordering() {
2377        let frd = adk_core::FunctionResponseData::with_multimodal(
2378            "multi",
2379            serde_json::json!({}),
2380            vec![
2381                adk_core::InlineDataPart {
2382                    mime_type: "image/png".to_string(),
2383                    data: vec![1, 2],
2384                    uri: None,
2385                    annotations: None,
2386                },
2387                adk_core::InlineDataPart {
2388                    mime_type: "image/jpeg".to_string(),
2389                    data: vec![3, 4],
2390                    uri: None,
2391                    annotations: None,
2392                },
2393            ],
2394            vec![adk_core::FileDataPart {
2395                mime_type: "application/pdf".to_string(),
2396                file_uri: "gs://b/f.pdf".to_string(),
2397                annotations: None,
2398            }],
2399        );
2400        let gemini_fr = convert_function_response_to_gemini_fr(&frd);
2401        // 2 inline + 1 file = 3 nested parts
2402        assert_eq!(gemini_fr.parts.len(), 3);
2403        assert!(matches!(&gemini_fr.parts[0], adk_gemini::FunctionResponsePart::InlineData { .. }));
2404        assert!(matches!(&gemini_fr.parts[1], adk_gemini::FunctionResponsePart::InlineData { .. }));
2405        assert!(matches!(&gemini_fr.parts[2], adk_gemini::FunctionResponsePart::FileData { .. }));
2406    }
2407}
2408
2409#[cfg(all(test, feature = "gemini-interactions"))]
2410mod interactions_transport_tests {
2411    use super::*;
2412    use adk_core::GenerateContentConfig;
2413
2414    /// **Feature: gemini-interactions-runtime, Property 2: Default options match the API**
2415    /// *For any* default `InteractionOptions`, `store == true`, `stateful == true`,
2416    /// `background` is `AgentTargetsOnly`, and `poll_interval` is 1 second.
2417    /// **Validates: Requirements 3.1, 3.2, 3.3**
2418    #[test]
2419    fn default_interaction_options_match_api_posture() {
2420        let opts = InteractionOptions::default();
2421        assert!(opts.store, "store should default to true");
2422        assert!(opts.stateful, "stateful should default to true");
2423        assert_eq!(opts.background, BackgroundMode::AgentTargetsOnly);
2424        assert_eq!(opts.poll_interval, std::time::Duration::from_secs(1));
2425    }
2426
2427    #[test]
2428    fn new_model_defaults_to_generate_content_transport() {
2429        let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2430            .expect("constructing a Gemini model should not require network");
2431        assert_eq!(model.transport(), GeminiTransport::GenerateContent);
2432    }
2433
2434    #[test]
2435    fn use_interactions_api_enables_transport_for_allowlisted_model() {
2436        let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2437            .expect("construct model")
2438            .use_interactions_api(true)
2439            .expect("allowlisted model should enable the Interactions transport");
2440
2441        assert_eq!(model.transport(), GeminiTransport::Interactions);
2442        assert_eq!(
2443            model.interaction_target,
2444            Some(InteractionTarget::Model("gemini-2.5-flash".to_string()))
2445        );
2446    }
2447
2448    #[test]
2449    fn use_interactions_api_rejects_unsupported_model_with_invalid_input() {
2450        let result = GeminiModel::new("test-key", "gemini-2.0-flash")
2451            .expect("construct model")
2452            .use_interactions_api(true);
2453
2454        let err = match result {
2455            Ok(_) => panic!("unsupported model should be rejected"),
2456            Err(err) => err,
2457        };
2458        assert_eq!(err.category, adk_core::ErrorCategory::InvalidInput);
2459        assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2460    }
2461
2462    #[test]
2463    fn use_interactions_api_false_reverts_to_generate_content() {
2464        let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2465            .expect("construct model")
2466            .use_interactions_api(true)
2467            .expect("enable interactions")
2468            .use_interactions_api(false)
2469            .expect("disabling should always succeed");
2470
2471        assert_eq!(model.transport(), GeminiTransport::GenerateContent);
2472        assert_eq!(model.interaction_target, None);
2473    }
2474
2475    #[test]
2476    fn interaction_options_override_is_stored() {
2477        let opts = InteractionOptions {
2478            store: false,
2479            stateful: false,
2480            background: BackgroundMode::Always,
2481            poll_interval: std::time::Duration::from_millis(250),
2482        };
2483        let model = GeminiModel::new("test-key", "gemini-2.5-flash")
2484            .expect("construct model")
2485            .interaction_options(opts.clone());
2486
2487        let stored = model.interaction_options_ref();
2488        assert_eq!(stored.store, opts.store);
2489        assert_eq!(stored.stateful, opts.stateful);
2490        assert_eq!(stored.background, opts.background);
2491        assert_eq!(stored.poll_interval, opts.poll_interval);
2492    }
2493
2494    #[test]
2495    fn resolve_background_honors_background_mode() {
2496        // Always → true regardless of target.
2497        let always = GeminiModel::new("test-key", "gemini-2.5-flash")
2498            .expect("construct model")
2499            .use_interactions_api(true)
2500            .expect("enable")
2501            .interaction_options(InteractionOptions {
2502                background: BackgroundMode::Always,
2503                ..InteractionOptions::default()
2504            });
2505        assert!(always.resolve_background());
2506
2507        // Never → false regardless of target.
2508        let never = GeminiModel::new("test-key", "gemini-2.5-flash")
2509            .expect("construct model")
2510            .use_interactions_api(true)
2511            .expect("enable")
2512            .interaction_options(InteractionOptions {
2513                background: BackgroundMode::Never,
2514                ..InteractionOptions::default()
2515            });
2516        assert!(!never.resolve_background());
2517
2518        // AgentTargetsOnly → false for a model target.
2519        let model_target = GeminiModel::new("test-key", "gemini-2.5-flash")
2520            .expect("construct model")
2521            .use_interactions_api(true)
2522            .expect("enable");
2523        assert!(!model_target.resolve_background());
2524
2525        // AgentTargetsOnly → true for an agent target.
2526        let agent_target = GeminiModel::new("test-key", "deep-research-preview-04-2026")
2527            .expect("construct model")
2528            .use_interactions_api(true)
2529            .expect("enable");
2530        assert!(agent_target.resolve_background());
2531    }
2532
2533    /// Requirements 7.7 / 9.2: a terminal `failed` status maps to an
2534    /// `Internal` `AdkError` (provider `"gemini"`).
2535    #[test]
2536    fn terminal_failed_status_maps_to_error() {
2537        use adk_gemini::interactions::InteractionStatus;
2538
2539        let err = interaction_status_to_result(InteractionStatus::Failed)
2540            .expect_err("a failed interaction must surface an error");
2541        assert_eq!(err.category, adk_core::ErrorCategory::Internal);
2542        assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2543        assert_eq!(err.code, "model.gemini.interactions.failed");
2544    }
2545
2546    /// Requirements 7.7 / 9.2: a terminal `budget_exceeded` status maps to an
2547    /// `Internal` `AdkError` (provider `"gemini"`).
2548    #[test]
2549    fn terminal_budget_exceeded_status_maps_to_error() {
2550        use adk_gemini::interactions::InteractionStatus;
2551
2552        let err = interaction_status_to_result(InteractionStatus::BudgetExceeded)
2553            .expect_err("a budget_exceeded interaction must surface an error");
2554        assert_eq!(err.category, adk_core::ErrorCategory::Internal);
2555        assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2556        assert_eq!(err.code, "model.gemini.interactions.budget_exceeded");
2557    }
2558
2559    /// Non-failure statuses (including `requires_action` and the other terminal
2560    /// states the transport reads content from) do not produce an error.
2561    #[test]
2562    fn non_failure_statuses_are_ok() {
2563        use adk_gemini::interactions::InteractionStatus;
2564
2565        for status in [
2566            InteractionStatus::InProgress,
2567            InteractionStatus::RequiresAction,
2568            InteractionStatus::Completed,
2569            InteractionStatus::Cancelled,
2570            InteractionStatus::Incomplete,
2571        ] {
2572            assert!(
2573                interaction_status_to_result(status).is_ok(),
2574                "status {status:?} should not map to an error"
2575            );
2576        }
2577    }
2578
2579    /// A constructed `failed` `Interaction` flows through the same status check
2580    /// the transport uses, surfacing an error after conversion.
2581    #[test]
2582    fn failed_interaction_resource_surfaces_error() {
2583        use adk_gemini::interactions::{Interaction, InteractionStatus};
2584
2585        let interaction = Interaction {
2586            id: "v1_failed".to_string(),
2587            model: Some("gemini-2.5-flash".to_string()),
2588            agent: None,
2589            status: InteractionStatus::Failed,
2590            steps: Vec::new(),
2591            usage: None,
2592            created: None,
2593            updated: None,
2594            environment_id: None,
2595        };
2596
2597        let err = interaction_status_to_result(interaction.status)
2598            .expect_err("failed interaction must surface an error");
2599        assert_eq!(err.category, adk_core::ErrorCategory::Internal);
2600        assert_eq!(err.details.provider.as_deref(), Some("gemini"));
2601    }
2602    #[test]
2603    fn gemini_37_rejects_sampling_before_network_io() {
2604        let model = GeminiModel::new("test-key", "gemini-3.7-flash").expect("construct model");
2605        let mut request = LlmRequest::new("gemini-3.7-flash", Vec::new());
2606        request.config =
2607            Some(GenerateContentConfig { temperature: Some(0.2), ..Default::default() });
2608
2609        let error = model
2610            .validate_request_contract(&request)
2611            .expect_err("sampling must be rejected locally");
2612        assert_eq!(error.code, "model.gemini.sampling_unsupported");
2613    }
2614
2615    #[test]
2616    fn gemini_37_rejects_token_budget_thinking() {
2617        let model = GeminiModel::new("test-key", "gemini-3.7-flash")
2618            .expect("construct model")
2619            .with_thinking_config(adk_gemini::ThinkingConfig {
2620                thinking_budget: Some(1024),
2621                include_thoughts: None,
2622                thinking_level: None,
2623            });
2624
2625        let error = model
2626            .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2627            .expect_err("token budgets must be rejected locally");
2628        assert_eq!(error.code, "model.gemini.thinking_budget_unsupported");
2629    }
2630}
2631
2632#[cfg(all(test, feature = "gemini-vertex"))]
2633mod vertex_rag_tests {
2634    use super::*;
2635    use adk_gemini::VertexRagStore;
2636
2637    fn test_store() -> VertexRagStore {
2638        VertexRagStore::corpus("projects/p/locations/us-central1/ragCorpora/123").with_top_k(5)
2639    }
2640
2641    fn vertex_model() -> GeminiModel {
2642        GeminiModel::new_google_cloud("test-key", "p", "us-central1", "gemini-3.7-flash")
2643            .expect("construct vertex model")
2644    }
2645
2646    // The Vertex constructor builds credential plumbing that requires a
2647    // current Tokio runtime, so this test is async.
2648    #[tokio::test]
2649    async fn vertex_backend_accepts_and_stores_the_rag_store() {
2650        let model = vertex_model().with_vertex_rag_store(test_store());
2651
2652        model
2653            .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2654            .expect("vertex backend must accept the rag store");
2655
2656        // The stored store is handed to adk-gemini's ContentBuilder per
2657        // request; the exact wire-shape injection is asserted in adk-gemini's
2658        // backend tests.
2659        assert_eq!(model.vertex_rag_store(), Some(&test_store()));
2660    }
2661
2662    #[test]
2663    fn studio_backend_rejects_rag_store_before_network_io() {
2664        let model = GeminiModel::new("test-key", "gemini-3.7-flash")
2665            .expect("construct studio model")
2666            .with_vertex_rag_store(test_store());
2667
2668        let error = model
2669            .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2670            .expect_err("studio backend must reject the rag store locally");
2671        assert_eq!(error.code, "model.gemini.rag_store_requires_vertex");
2672        assert_eq!(error.category, adk_core::ErrorCategory::Unsupported);
2673        assert_eq!(error.details.provider.as_deref(), Some("gemini"));
2674    }
2675
2676    #[test]
2677    fn no_rag_store_means_no_gate() {
2678        let model = GeminiModel::new("test-key", "gemini-3.7-flash").expect("construct model");
2679        assert_eq!(model.vertex_rag_store(), None);
2680        model
2681            .validate_request_contract(&LlmRequest::new("gemini-3.7-flash", Vec::new()))
2682            .expect("no store configured, nothing to reject");
2683    }
2684}