Skip to main content

reson_agentic/providers/
google.rs

1//! Google Generative AI (Gemini) client implementation
2//!
3//! Implements the InferenceClient trait for Google's Gemini models.
4//! Uses the REST API directly since there's no official Rust SDK.
5//!
6//! Supports:
7//! - Native tool calling via function_declarations
8//! - Thinking/reasoning mode
9//! - Streaming via Server-Sent Events
10//! - API key authentication
11//! - Application Default Credentials (ADC) via `google-adc` feature
12//! - File API for uploading large media files (>20MB)
13
14use async_trait::async_trait;
15use futures::stream::{Stream, StreamExt};
16use reqwest::StatusCode;
17use std::pin::Pin;
18#[cfg(feature = "google-adc")]
19use std::sync::Arc;
20#[cfg(feature = "google-adc")]
21use tokio::sync::RwLock;
22
23use crate::error::{Error, Result};
24use crate::providers::{
25    GenerationConfig, GenerationResponse, InferenceClient, StreamChunk, TraceCallback,
26};
27use crate::retry::{retry_with_backoff, RetryConfig};
28use crate::types::ChatRole;
29use crate::types::{Provider, TokenUsage};
30use crate::utils::{media_part_to_google_format, ConversationMessage, JsonStreamAccumulator};
31
32#[cfg(feature = "google-adc")]
33use crate::utils::parse_json_value_strict_str;
34
35fn google_debug_enabled() -> bool {
36    std::env::var("RESON_DEBUG_GOOGLE")
37        .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE"))
38        .unwrap_or(false)
39}
40
41fn google_stream_debug_enabled() -> bool {
42    std::env::var("RESON_DEBUG_GOOGLE_STREAM")
43        .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE"))
44        .unwrap_or(false)
45}
46
47fn sanitize_google_endpoint(endpoint: &str) -> String {
48    endpoint
49        .split_once("?key=")
50        .map(|(base, _)| format!("{}?key=***", base))
51        .unwrap_or_else(|| endpoint.to_string())
52}
53
54/// Authentication method for Google GenAI
55#[derive(Clone)]
56pub enum GoogleAuth {
57    /// API key authentication (passed in URL)
58    ApiKey(String),
59    /// Application Default Credentials (ADC) - uses GOOGLE_APPLICATION_CREDENTIALS
60    #[cfg(feature = "google-adc")]
61    Adc(Arc<RwLock<Option<Arc<dyn gcp_auth::TokenProvider>>>>),
62}
63
64/// Status of an uploaded file
65#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
67pub enum FileState {
68    /// File is being processed
69    Processing,
70    /// File is ready to use
71    Active,
72    /// Processing failed
73    Failed,
74}
75
76/// Response from uploading a file to Google's File API
77#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct UploadedFile {
80    /// The file resource name (e.g., "files/abc123")
81    pub name: String,
82    /// Display name of the file
83    #[serde(default)]
84    pub display_name: String,
85    /// MIME type of the file
86    pub mime_type: String,
87    /// Size of the file in bytes
88    #[serde(default)]
89    pub size_bytes: String,
90    /// The URI to use in generateContent requests
91    pub uri: String,
92    /// Current state of the file
93    pub state: FileState,
94    /// Error details if state is FAILED
95    #[serde(default)]
96    pub error: Option<serde_json::Value>,
97}
98
99/// Google Generative AI (Gemini) client
100#[derive(Clone)]
101pub struct GoogleGenAIClient {
102    model: String,
103    auth: GoogleAuth,
104    api_url: String,
105    thinking_budget: Option<u32>,
106    trace_callback: Option<TraceCallback>,
107}
108
109impl GoogleGenAIClient {
110    /// Create a new Google GenAI client with API key authentication
111    pub fn new(api_key: impl Into<String>, model: impl Into<String>) -> Self {
112        Self {
113            model: model.into(),
114            auth: GoogleAuth::ApiKey(api_key.into()),
115            api_url: "https://generativelanguage.googleapis.com/v1beta".to_string(),
116            thinking_budget: None,
117            trace_callback: None,
118        }
119    }
120
121    /// Create a new Google GenAI client with Application Default Credentials (ADC)
122    /// using the Vertex AI endpoint.
123    ///
124    /// This uses the `GOOGLE_APPLICATION_CREDENTIALS` environment variable or
125    /// credentials from `gcloud auth application-default login`.
126    ///
127    /// The project ID is automatically extracted from the service account JSON file.
128    /// Location defaults to "us-central1" if not specified via `GOOGLE_CLOUD_LOCATION`.
129    ///
130    /// Note: ADC/service accounts require using the Vertex AI endpoint, not the
131    /// standard Generative Language API (which uses API keys).
132    ///
133    /// Requires the `google-adc` feature to be enabled.
134    ///
135    /// # Arguments
136    /// * `model` - The model name (e.g., "gemini-2.0-flash-thinking-exp")
137    ///
138    /// # Panics
139    /// Panics if `GOOGLE_APPLICATION_CREDENTIALS` is not set or if the credentials
140    /// file cannot be read or doesn't contain a `project_id`.
141    #[cfg(feature = "google-adc")]
142    pub fn from_adc(model: impl Into<String>) -> Self {
143        // Read project_id from the service account JSON file
144        let creds_path = std::env::var("GOOGLE_APPLICATION_CREDENTIALS")
145            .expect("GOOGLE_APPLICATION_CREDENTIALS environment variable must be set");
146
147        let creds_content = std::fs::read_to_string(&creds_path)
148            .unwrap_or_else(|_| panic!("Failed to read credentials file: {}", creds_path));
149
150        let creds_json: serde_json::Value = parse_json_value_strict_str(&creds_content)
151            .expect("Failed to parse credentials file as JSON");
152
153        let project = creds_json["project_id"]
154            .as_str()
155            .expect("Credentials file must contain project_id")
156            .to_string();
157
158        // Get location from env var or default to us-central1
159        let loc =
160            std::env::var("GOOGLE_CLOUD_LOCATION").unwrap_or_else(|_| "us-central1".to_string());
161
162        let model_str = model.into();
163        Self {
164            model: model_str.clone(),
165            auth: GoogleAuth::Adc(Arc::new(RwLock::new(None))),
166            // Vertex AI endpoint format
167            api_url: format!(
168                "https://{}-aiplatform.googleapis.com/v1/projects/{}/locations/{}/publishers/google/models/{}",
169                loc, project, loc, model_str
170            ),
171            thinking_budget: None,
172            trace_callback: None,
173        }
174    }
175
176    /// Create a new Google GenAI client with ADC and explicit project/location.
177    ///
178    /// Use this if you want to override the project_id from the credentials file
179    /// or specify a different location.
180    #[cfg(feature = "google-adc")]
181    pub fn from_adc_with_config(
182        model: impl Into<String>,
183        project_id: impl Into<String>,
184        location: impl Into<String>,
185    ) -> Self {
186        let project = project_id.into();
187        let loc = location.into();
188        let model_str = model.into();
189        Self {
190            model: model_str.clone(),
191            auth: GoogleAuth::Adc(Arc::new(RwLock::new(None))),
192            // Vertex AI endpoint format
193            api_url: format!(
194                "https://{}-aiplatform.googleapis.com/v1/projects/{}/locations/{}/publishers/google/models/{}",
195                loc, project, loc, model_str
196            ),
197            thinking_budget: None,
198            trace_callback: None,
199        }
200    }
201
202    /// Set the thinking budget for reasoning mode
203    pub fn with_thinking_budget(mut self, budget: u32) -> Self {
204        self.thinking_budget = Some(budget);
205        self
206    }
207
208    /// Set a custom API URL
209    pub fn with_api_url(mut self, url: impl Into<String>) -> Self {
210        self.api_url = url.into();
211        self
212    }
213
214    // ==================== File API Methods ====================
215
216    /// Upload a file to Google's File API for use in generateContent requests.
217    ///
218    /// Use this for files larger than 20MB or when you want to reuse the same
219    /// file across multiple requests.
220    ///
221    /// # Arguments
222    /// * `data` - The file bytes to upload
223    /// * `mime_type` - The MIME type of the file (e.g., "video/mp4", "image/png")
224    /// * `display_name` - Optional display name for the file
225    ///
226    /// # Returns
227    /// An `UploadedFile` containing the `uri` to use in requests. Note that video
228    /// files may need processing time - check the `state` field and use
229    /// `wait_for_file_processing` if needed.
230    ///
231    /// # Example
232    /// ```ignore
233    /// let video_bytes = std::fs::read("video.mp4")?;
234    /// let uploaded = client.upload_file(&video_bytes, "video/mp4", Some("my-video")).await?;
235    /// // For videos, wait for processing
236    /// let ready = client.wait_for_file_processing(&uploaded.name, None).await?;
237    /// // Use ready.uri in your generateContent request
238    /// ```
239    pub async fn upload_file(
240        &self,
241        data: &[u8],
242        mime_type: &str,
243        display_name: Option<&str>,
244    ) -> Result<UploadedFile> {
245        let client = reqwest::Client::new();
246        let file_api_base = "https://generativelanguage.googleapis.com";
247
248        // Step 1: Start resumable upload - get upload URL from response headers
249        let start_url = match &self.auth {
250            GoogleAuth::ApiKey(key) => {
251                format!("{}/upload/v1beta/files?key={}", file_api_base, key)
252            }
253            #[cfg(feature = "google-adc")]
254            GoogleAuth::Adc(_) => {
255                format!("{}/upload/v1beta/files", file_api_base)
256            }
257        };
258
259        let metadata = serde_json::json!({
260            "file": {
261                "display_name": display_name.unwrap_or("uploaded_file")
262            }
263        });
264
265        #[allow(unused_mut)] // mut needed only with google-adc feature
266        let mut start_request = client
267            .post(&start_url)
268            .header("X-Goog-Upload-Protocol", "resumable")
269            .header("X-Goog-Upload-Command", "start")
270            .header(
271                "X-Goog-Upload-Header-Content-Length",
272                data.len().to_string(),
273            )
274            .header("X-Goog-Upload-Header-Content-Type", mime_type)
275            .header("Content-Type", "application/json");
276
277        // Add authorization header for ADC
278        #[cfg(feature = "google-adc")]
279        if let GoogleAuth::Adc(_) = &self.auth {
280            let token = self.get_adc_token().await?;
281            start_request = start_request.header("Authorization", format!("Bearer {}", token));
282        }
283
284        let start_response = start_request
285            .json(&metadata)
286            .send()
287            .await
288            .map_err(|e| Error::NonRetryable(format!("Failed to start file upload: {}", e)))?;
289
290        if !start_response.status().is_success() {
291            let error_body = start_response.text().await.unwrap_or_default();
292            return Err(Error::NonRetryable(format!(
293                "Failed to start file upload: {}",
294                error_body
295            )));
296        }
297
298        // Extract upload URL from response headers
299        let upload_url = start_response
300            .headers()
301            .get("x-goog-upload-url")
302            .ok_or_else(|| {
303                Error::NonRetryable("Missing x-goog-upload-url header in response".to_string())
304            })?
305            .to_str()
306            .map_err(|e| Error::NonRetryable(format!("Invalid upload URL header: {}", e)))?
307            .to_string();
308
309        // Step 2: Upload file bytes to the upload URL
310        let upload_response = client
311            .post(&upload_url)
312            .header("Content-Length", data.len().to_string())
313            .header("X-Goog-Upload-Offset", "0")
314            .header("X-Goog-Upload-Command", "upload, finalize")
315            .body(data.to_vec())
316            .send()
317            .await
318            .map_err(|e| Error::NonRetryable(format!("Failed to upload file data: {}", e)))?;
319
320        if !upload_response.status().is_success() {
321            let error_body = upload_response.text().await.unwrap_or_default();
322            return Err(Error::NonRetryable(format!(
323                "Failed to upload file data: {}",
324                error_body
325            )));
326        }
327
328        // Parse the response to get the file info
329        let response_json: serde_json::Value = upload_response
330            .json()
331            .await
332            .map_err(|e| Error::NonRetryable(format!("Failed to parse upload response: {}", e)))?;
333
334        // Extract the file object from the response
335        let file_json = response_json
336            .get("file")
337            .ok_or_else(|| {
338                Error::NonRetryable(format!(
339                    "Missing 'file' in upload response: {:?}",
340                    response_json
341                ))
342            })?
343            .clone();
344
345        let uploaded_file: UploadedFile = serde_json::from_value(file_json)
346            .map_err(|e| Error::NonRetryable(format!("Failed to parse UploadedFile: {}", e)))?;
347
348        Ok(uploaded_file)
349    }
350
351    /// Get the current status of an uploaded file.
352    ///
353    /// # Arguments
354    /// * `file_name` - The file resource name (e.g., "files/abc123")
355    pub async fn get_file(&self, file_name: &str) -> Result<UploadedFile> {
356        let client = reqwest::Client::new();
357        let file_api_base = "https://generativelanguage.googleapis.com";
358
359        let url = match &self.auth {
360            GoogleAuth::ApiKey(key) => {
361                format!("{}/v1beta/{}?key={}", file_api_base, file_name, key)
362            }
363            #[cfg(feature = "google-adc")]
364            GoogleAuth::Adc(_) => {
365                format!("{}/v1beta/{}", file_api_base, file_name)
366            }
367        };
368
369        #[allow(unused_mut)] // mut needed only with google-adc feature
370        let mut request = client.get(&url);
371
372        #[cfg(feature = "google-adc")]
373        if let GoogleAuth::Adc(_) = &self.auth {
374            let token = self.get_adc_token().await?;
375            request = request.header("Authorization", format!("Bearer {}", token));
376        }
377
378        let response = request
379            .send()
380            .await
381            .map_err(|e| Error::NonRetryable(format!("Failed to get file status: {}", e)))?;
382
383        if !response.status().is_success() {
384            let error_body = response.text().await.unwrap_or_default();
385            return Err(Error::NonRetryable(format!(
386                "Failed to get file status: {}",
387                error_body
388            )));
389        }
390
391        let file: UploadedFile = response.json().await.map_err(|e| {
392            Error::NonRetryable(format!("Failed to parse file status response: {}", e))
393        })?;
394
395        Ok(file)
396    }
397
398    /// Wait for a file to finish processing and become ACTIVE.
399    ///
400    /// Video files require server-side processing before they can be used.
401    /// This method polls the file status until it becomes ACTIVE or FAILED.
402    ///
403    /// # Arguments
404    /// * `file_name` - The file resource name (e.g., "files/abc123")
405    /// * `timeout_secs` - Maximum time to wait (defaults to 300 seconds / 5 minutes)
406    ///
407    /// # Returns
408    /// The file when it reaches ACTIVE state, or an error if it fails or times out.
409    pub async fn wait_for_file_processing(
410        &self,
411        file_name: &str,
412        timeout_secs: Option<u64>,
413    ) -> Result<UploadedFile> {
414        let timeout = timeout_secs.unwrap_or(300);
415        let start = std::time::Instant::now();
416        let poll_interval = std::time::Duration::from_secs(2);
417
418        loop {
419            let file = self.get_file(file_name).await?;
420
421            match file.state {
422                FileState::Active => return Ok(file),
423                FileState::Failed => {
424                    let error_msg = file
425                        .error
426                        .map(|e| e.to_string())
427                        .unwrap_or_else(|| "Unknown error".to_string());
428                    return Err(Error::NonRetryable(format!(
429                        "File processing failed: {}",
430                        error_msg
431                    )));
432                }
433                FileState::Processing => {
434                    if start.elapsed().as_secs() > timeout {
435                        return Err(Error::NonRetryable(format!(
436                            "Timeout waiting for file {} to process",
437                            file_name
438                        )));
439                    }
440                    tokio::time::sleep(poll_interval).await;
441                }
442            }
443        }
444    }
445
446    /// Delete an uploaded file.
447    ///
448    /// # Arguments
449    /// * `file_name` - The file resource name (e.g., "files/abc123")
450    pub async fn delete_file(&self, file_name: &str) -> Result<()> {
451        let client = reqwest::Client::new();
452        let file_api_base = "https://generativelanguage.googleapis.com";
453
454        let url = match &self.auth {
455            GoogleAuth::ApiKey(key) => {
456                format!("{}/v1beta/{}?key={}", file_api_base, file_name, key)
457            }
458            #[cfg(feature = "google-adc")]
459            GoogleAuth::Adc(_) => {
460                format!("{}/v1beta/{}", file_api_base, file_name)
461            }
462        };
463
464        #[allow(unused_mut)] // mut needed only with google-adc feature
465        let mut request = client.delete(&url);
466
467        #[cfg(feature = "google-adc")]
468        if let GoogleAuth::Adc(_) = &self.auth {
469            let token = self.get_adc_token().await?;
470            request = request.header("Authorization", format!("Bearer {}", token));
471        }
472
473        let response = request
474            .send()
475            .await
476            .map_err(|e| Error::NonRetryable(format!("Failed to delete file: {}", e)))?;
477
478        if !response.status().is_success() {
479            let error_body = response.text().await.unwrap_or_default();
480            return Err(Error::NonRetryable(format!(
481                "Failed to delete file: {}",
482                error_body
483            )));
484        }
485
486        Ok(())
487    }
488
489    // ==================== End File API Methods ====================
490
491    /// Get a valid access token for ADC authentication
492    #[cfg(feature = "google-adc")]
493    async fn get_adc_token(&self) -> Result<String> {
494        if let GoogleAuth::Adc(token_provider) = &self.auth {
495            let mut provider = token_provider.write().await;
496
497            // Initialize if needed
498            if provider.is_none() {
499                let tp = gcp_auth::provider()
500                    .await
501                    .map_err(|e| Error::NonRetryable(format!("Failed to initialize ADC: {}", e)))?;
502                *provider = Some(tp);
503            }
504
505            // Get token with cloud-platform scope (required for Vertex AI/Generative AI)
506            let tp = provider.as_ref().unwrap();
507            let scopes = &["https://www.googleapis.com/auth/cloud-platform"];
508            let token = tp
509                .token(scopes)
510                .await
511                .map_err(|e| Error::NonRetryable(format!("Failed to get ADC token: {}", e)))?;
512
513            Ok(token.as_str().to_string())
514        } else {
515            Err(Error::NonRetryable(
516                "Not using ADC authentication".to_string(),
517            ))
518        }
519    }
520
521    /// Build the request body for Google GenAI API
522    fn build_request_body(
523        &self,
524        messages: &[ConversationMessage],
525        config: &GenerationConfig,
526    ) -> Result<serde_json::Value> {
527        // Extract system instruction if present
528        let (system_instruction, messages) = self.extract_system_message(messages)?;
529
530        // Convert messages to Google format
531        let contents = self.convert_messages_to_contents(messages)?;
532
533        let mut request = serde_json::json!({
534            "contents": contents,
535            "generationConfig": {
536                "maxOutputTokens": config.max_tokens.unwrap_or(4096),
537            }
538        });
539
540        // Add temperature and topP if not in thinking mode
541        if self.thinking_budget.is_none() {
542            if let Some(temp) = config.temperature {
543                request["generationConfig"]["temperature"] = serde_json::json!(temp);
544            }
545            if let Some(top_p) = config.top_p {
546                request["generationConfig"]["topP"] = serde_json::json!(top_p);
547            }
548        }
549
550        // Add system instruction if present
551        if let Some(system) = system_instruction {
552            request["systemInstruction"] = serde_json::json!({
553                "parts": [{"text": system}]
554            });
555        }
556
557        // Add tools if provided (Google format)
558        if let Some(ref tools) = config.tools {
559            if !tools.is_empty() {
560                // Check if tools are already in Google format or need conversion
561                let google_tools = self.convert_tools_to_google_format(tools)?;
562                request["tools"] = google_tools;
563            }
564        }
565
566        // Add thinking config if enabled
567        if let Some(budget) = self.thinking_budget {
568            request["generationConfig"]["thinkingConfig"] = serde_json::json!({
569                "includeThoughts": true,
570                "thinkingBudget": budget
571            });
572        }
573
574        // Add structured output schema if provided
575        if let Some(ref schema) = config.output_schema {
576            request["generationConfig"]["responseMimeType"] = serde_json::json!("application/json");
577            request["generationConfig"]["responseSchema"] = schema.clone();
578        }
579
580        Ok(request)
581    }
582
583    /// Extract system message and return (system, remaining_messages)
584    fn extract_system_message<'a>(
585        &self,
586        messages: &'a [ConversationMessage],
587    ) -> Result<(Option<String>, &'a [ConversationMessage])> {
588        if let Some(ConversationMessage::Chat(first)) = messages.first() {
589            if first.role == ChatRole::System {
590                return Ok((Some(first.content.clone()), &messages[1..]));
591            }
592        }
593        Ok((None, messages))
594    }
595
596    /// Convert messages to Google's contents format
597    fn convert_messages_to_contents(
598        &self,
599        messages: &[ConversationMessage],
600    ) -> Result<Vec<serde_json::Value>> {
601        let mut contents = Vec::new();
602
603        for msg in messages {
604            match msg {
605                ConversationMessage::Chat(chat_msg) => {
606                    let role = match chat_msg.role {
607                        ChatRole::User => "user",
608                        ChatRole::Assistant => "model",
609                        ChatRole::System => continue, // Skip system messages (handled separately)
610                        ChatRole::Tool => "user",     // Tool results come from user role
611                    };
612
613                    contents.push(serde_json::json!({
614                        "role": role,
615                        "parts": [{"text": chat_msg.content}]
616                    }));
617                }
618                ConversationMessage::ToolCall(tool_call) => {
619                    // Google uses functionCall for assistant tool calls
620                    let mut part = serde_json::json!({
621                        "functionCall": {
622                            "name": tool_call.tool_name,
623                            "args": tool_call.args
624                        }
625                    });
626                    // Include thoughtSignature if available (required by Google for multi-turn)
627                    if let Some(ref obj) = tool_call.tool_obj {
628                        if let Some(sig) = obj.get("thoughtSignature") {
629                            part["thoughtSignature"] = sig.clone();
630                        }
631                    }
632                    contents.push(serde_json::json!({
633                        "role": "model",
634                        "parts": [part]
635                    }));
636                }
637                ConversationMessage::ToolResult(tool_result) => {
638                    // Google uses functionResponse for tool results
639                    // Get tool_name from: 1) tool_name field, 2) tool_obj["_tool_name"], 3) empty string
640                    let tool_name = tool_result.tool_name.clone().unwrap_or_else(|| {
641                        tool_result
642                            .tool_obj
643                            .as_ref()
644                            .and_then(|obj| obj.get("_tool_name"))
645                            .and_then(|v| v.as_str())
646                            .map(|s| s.to_string())
647                            .unwrap_or_default()
648                    });
649
650                    contents.push(serde_json::json!({
651                        "role": "user",
652                        "parts": [{
653                            "functionResponse": {
654                                "name": tool_name,
655                                "response": {
656                                    "result": tool_result.content
657                                }
658                            }
659                        }]
660                    }));
661                }
662                ConversationMessage::Reasoning(segment) => {
663                    // Google uses thought: true for reasoning
664                    contents.push(serde_json::json!({
665                        "role": "model",
666                        "parts": [{
667                            "thought": true,
668                            "text": segment.content
669                        }]
670                    }));
671                }
672                ConversationMessage::Multimodal(multimodal_msg) => {
673                    // Convert multimodal message with media parts (images, video, audio)
674                    let role = match multimodal_msg.role {
675                        ChatRole::User => "user",
676                        ChatRole::Assistant => "model",
677                        ChatRole::System => continue, // Skip system messages
678                        ChatRole::Tool => "user",
679                    };
680
681                    let parts: Vec<serde_json::Value> = multimodal_msg
682                        .parts
683                        .iter()
684                        .map(|part| media_part_to_google_format(part, None))
685                        .collect();
686
687                    contents.push(serde_json::json!({
688                        "role": role,
689                        "parts": parts
690                    }));
691                }
692            }
693        }
694
695        Ok(contents)
696    }
697
698    /// Convert tools from Anthropic/OpenAI format to Google format
699    fn convert_tools_to_google_format(
700        &self,
701        tools: &[serde_json::Value],
702    ) -> Result<serde_json::Value> {
703        // Check if already in Google format
704        if tools
705            .iter()
706            .any(|t| t.get("function_declarations").is_some())
707        {
708            return Ok(serde_json::json!(tools));
709        }
710
711        // Convert from Anthropic format
712        let function_declarations: Vec<serde_json::Value> = tools
713            .iter()
714            .map(|tool| {
715                serde_json::json!({
716                    "name": tool.get("name").cloned().unwrap_or(serde_json::json!("")),
717                    "description": tool.get("description").cloned().unwrap_or(serde_json::json!("")),
718                    "parameters": tool.get("input_schema").cloned().unwrap_or_else(|| {
719                        tool.get("parameters").cloned().unwrap_or(serde_json::json!({}))
720                    })
721                })
722            })
723            .collect();
724
725        Ok(serde_json::json!([{
726            "function_declarations": function_declarations
727        }]))
728    }
729
730    /// Extract text content from response
731    fn extract_text_content(&self, candidates: &serde_json::Value) -> String {
732        if let Some(first) = candidates.as_array().and_then(|a| a.first()) {
733            if let Some(parts) = first["content"]["parts"].as_array() {
734                for part in parts {
735                    // Skip thought parts
736                    if part
737                        .get("thought")
738                        .and_then(|t| t.as_bool())
739                        .unwrap_or(false)
740                    {
741                        continue;
742                    }
743                    if let Some(text) = part["text"].as_str() {
744                        return text.to_string();
745                    }
746                }
747            }
748        }
749        String::new()
750    }
751
752    /// Extract reasoning content from response
753    fn extract_reasoning(&self, candidates: &serde_json::Value) -> Option<String> {
754        if let Some(first) = candidates.as_array().and_then(|a| a.first()) {
755            if let Some(parts) = first["content"]["parts"].as_array() {
756                let reasoning: Vec<String> = parts
757                    .iter()
758                    .filter(|part| {
759                        part.get("thought")
760                            .and_then(|t| t.as_bool())
761                            .unwrap_or(false)
762                    })
763                    .filter_map(|part| part["text"].as_str().map(|s| s.to_string()))
764                    .collect();
765
766                if !reasoning.is_empty() {
767                    return Some(reasoning.join("\n"));
768                }
769            }
770        }
771        None
772    }
773
774    /// Extract tool calls from response
775    fn extract_tool_calls(&self, candidates: &serde_json::Value) -> Vec<serde_json::Value> {
776        let mut tool_calls = Vec::new();
777
778        if let Some(first) = candidates.as_array().and_then(|a| a.first()) {
779            if let Some(parts) = first["content"]["parts"].as_array() {
780                for part in parts {
781                    if let Some(func_call) = part.get("functionCall") {
782                        let name = func_call["name"].as_str().unwrap_or("");
783                        let args = func_call
784                            .get("args")
785                            .cloned()
786                            .unwrap_or(serde_json::json!({}));
787
788                        // Generate unique ID since Google doesn't provide one
789                        let id = format!("google_{}_{}", name, rand_id());
790
791                        // Preserve thoughtSignature if present (required for multi-turn)
792                        let thought_signature = part.get("thoughtSignature").cloned();
793
794                        // Convert to normalized format with _tool_name for compatibility
795                        let mut tc = serde_json::json!({
796                            "id": id,
797                            "_tool_name": name,
798                            "_tool_use_id": id,
799                            "name": name,
800                            "input": args,
801                            "function": {
802                                "name": name,
803                                "arguments": args
804                            }
805                        });
806                        if let Some(sig) = thought_signature {
807                            tc["thoughtSignature"] = sig;
808                        }
809                        tool_calls.push(tc);
810                    }
811                }
812            }
813        }
814
815        tool_calls
816    }
817
818    /// Parse token usage from response
819    fn parse_usage(&self, usage_metadata: &serde_json::Value) -> TokenUsage {
820        TokenUsage {
821            input_tokens: usage_metadata["promptTokenCount"].as_u64().unwrap_or(0),
822            output_tokens: usage_metadata["candidatesTokenCount"].as_u64().unwrap_or(0),
823            cached_tokens: usage_metadata["cachedContentTokenCount"]
824                .as_u64()
825                .unwrap_or(0),
826        }
827    }
828
829    /// Check if using Vertex AI endpoint
830    #[allow(dead_code)]
831    fn is_vertex_ai(&self) -> bool {
832        self.api_url.contains("aiplatform.googleapis.com")
833    }
834
835    /// Get the API endpoint for the model
836    fn get_endpoint(&self, stream: bool) -> String {
837        let action = if stream {
838            "streamGenerateContent"
839        } else {
840            "generateContent"
841        };
842
843        match &self.auth {
844            GoogleAuth::ApiKey(key) => {
845                // Standard Gemini API: {base}/models/{model}:{action}?key={key}
846                format!(
847                    "{}/models/{}:{}?key={}",
848                    self.api_url, self.model, action, key
849                )
850            }
851            #[cfg(feature = "google-adc")]
852            GoogleAuth::Adc(_) => {
853                // Vertex AI: {base}:{action} (model is already in the URL)
854                format!("{}:{}", self.api_url, action)
855            }
856        }
857    }
858
859    /// Make HTTP request to Google API
860    async fn make_request(
861        &self,
862        body: serde_json::Value,
863        stream: bool,
864        timeout: Option<std::time::Duration>,
865    ) -> Result<reqwest::Response> {
866        let client = reqwest::Client::new();
867        if google_debug_enabled() {
868            let endpoint = sanitize_google_endpoint(&self.get_endpoint(stream));
869            eprintln!(
870                "google request: model={} stream={} endpoint={} body={}",
871                self.model, stream, endpoint, body
872            );
873        }
874        #[allow(unused_mut)] // mut needed only with google-adc feature
875        let mut request = client
876            .post(self.get_endpoint(stream))
877            .timeout(timeout.unwrap_or(std::time::Duration::from_secs(300)))
878            .header("Content-Type", "application/json");
879
880        // Add authorization header for ADC
881        #[cfg(feature = "google-adc")]
882        if let GoogleAuth::Adc(_) = &self.auth {
883            let token = self.get_adc_token().await?;
884            request = request.header("Authorization", format!("Bearer {}", token));
885        }
886
887        let response = request.json(&body).send().await?;
888
889        Ok(response)
890    }
891
892    /// Handle error responses - categorize as retryable or non-retryable
893    fn handle_error_response(&self, status: StatusCode, body: String) -> Error {
894        match status {
895            // Client errors (4xx) are generally not retryable
896            StatusCode::BAD_REQUEST | StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
897                Error::NonRetryable(format!("{}: {}", status, body))
898            }
899            // Rate limit - retryable
900            StatusCode::TOO_MANY_REQUESTS => Error::Inference(format!("Rate limited: {}", body)),
901            // Server errors (5xx) are retryable
902            StatusCode::INTERNAL_SERVER_ERROR
903            | StatusCode::BAD_GATEWAY
904            | StatusCode::SERVICE_UNAVAILABLE
905            | StatusCode::GATEWAY_TIMEOUT => Error::Inference(format!("{}: {}", status, body)),
906            // Default: assume retryable for unknown errors
907            _ => Error::Inference(format!("{}: {}", status, body)),
908        }
909    }
910
911    /// Make request with retry and exponential backoff
912    async fn make_request_with_retry(
913        &self,
914        body: serde_json::Value,
915        timeout: Option<std::time::Duration>,
916    ) -> Result<serde_json::Value> {
917        let config = RetryConfig::default();
918
919        retry_with_backoff(config, || async {
920            let response = self.make_request(body.clone(), false, timeout).await?;
921            let status = response.status();
922
923            if !status.is_success() {
924                let error_body = response.text().await.unwrap_or_default();
925                if google_debug_enabled() {
926                    eprintln!(
927                        "google response error: model={} status={} body={}",
928                        self.model, status, error_body
929                    );
930                }
931                return Err(self.handle_error_response(status, error_body));
932            }
933
934            response.json().await.map_err(Error::from)
935        })
936        .await
937    }
938}
939
940#[async_trait]
941impl InferenceClient for GoogleGenAIClient {
942    async fn get_generation(
943        &self,
944        messages: &[ConversationMessage],
945        config: &GenerationConfig,
946    ) -> Result<GenerationResponse> {
947        let request_body = self.build_request_body(messages, config)?;
948        let body = self
949            .make_request_with_retry(request_body, config.timeout)
950            .await?;
951
952        // Parse response
953        let candidates = &body["candidates"];
954        let text_content = self.extract_text_content(candidates);
955        let reasoning = self.extract_reasoning(candidates);
956        let tool_calls = self.extract_tool_calls(candidates);
957
958        // Parse usage
959        let usage = self.parse_usage(&body["usageMetadata"]);
960
961        // If tools were provided, return full response
962        let has_tools = config.tools.is_some() && !config.tools.as_ref().unwrap().is_empty();
963        let has_tool_calls = !tool_calls.is_empty();
964
965        Ok(GenerationResponse {
966            content: text_content,
967            reasoning,
968            tool_calls,
969            reasoning_segments: Vec::new(),
970            usage,
971            provider_cost_dollars: None,
972            raw: if has_tools || has_tool_calls {
973                Some(body)
974            } else {
975                None
976            },
977        })
978    }
979
980    async fn connect_and_listen(
981        &self,
982        messages: &[ConversationMessage],
983        config: &GenerationConfig,
984    ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamChunk>> + Send>>> {
985        let request_body = self.build_request_body(messages, config)?;
986        let timeout = config.timeout;
987
988        // Retry the connection establishment with backoff
989        let retry_config = RetryConfig::default();
990        let response = retry_with_backoff(retry_config, || async {
991            let resp = self
992                .make_request(request_body.clone(), true, timeout)
993                .await?;
994            let status = resp.status();
995
996            if !status.is_success() {
997                let error_body = resp.text().await.unwrap_or_default();
998                if google_debug_enabled() {
999                    eprintln!(
1000                        "google streaming response error: model={} status={} body={}",
1001                        self.model, status, error_body
1002                    );
1003                }
1004                return Err(self.handle_error_response(status, error_body));
1005            }
1006
1007            Ok(resp)
1008        })
1009        .await?;
1010
1011        let _has_tools = config.tools.is_some() && !config.tools.as_ref().unwrap().is_empty();
1012
1013        // Google streams a JSON array; use serde_json's streaming deserializer.
1014        let stream = response.bytes_stream();
1015
1016        Ok(Box::pin(
1017            stream
1018                .scan(JsonStreamAccumulator::new(), move |parser, chunk_result| {
1019                    let mut chunks = Vec::new();
1020
1021                    match chunk_result {
1022                        Ok(bytes) => {
1023                            if google_stream_debug_enabled() {
1024                                let text = String::from_utf8_lossy(&bytes);
1025                                eprintln!("google stream chunk: {}", text);
1026                            }
1027                            match parser.push_bytes(&bytes) {
1028                                Ok(values) => {
1029                                    for json in values {
1030                                        if let Some(candidates) =
1031                                            json.get("candidates").and_then(|c| c.as_array())
1032                                        {
1033                                            for candidate in candidates {
1034                                                if let Some(parts) =
1035                                                    candidate["content"]["parts"].as_array()
1036                                                {
1037                                                    for part in parts {
1038                                                        if let Some(func_call) =
1039                                                            part.get("functionCall")
1040                                                        {
1041                                                            let name = func_call["name"]
1042                                                                .as_str()
1043                                                                .unwrap_or("");
1044                                                            let args = func_call
1045                                                                .get("args")
1046                                                                .cloned()
1047                                                                .unwrap_or(serde_json::json!({}));
1048
1049                                                            let id = format!(
1050                                                                "google_{}_{}",
1051                                                                name,
1052                                                                rand_id()
1053                                                            );
1054
1055                                                            let tool_call = serde_json::json!({
1056                                                                "id": id,
1057                                                                "_tool_name": name,
1058                                                                "_tool_use_id": id,
1059                                                                "name": name,
1060                                                                "input": args,
1061                                                                "function": {
1062                                                                    "name": name,
1063                                                                    "arguments": args
1064                                                                }
1065                                                            });
1066
1067                                                            chunks.push(Ok(
1068                                                                StreamChunk::ToolCallComplete(
1069                                                                    tool_call,
1070                                                                ),
1071                                                            ));
1072                                                        } else if part
1073                                                            .get("thought")
1074                                                            .and_then(|t| t.as_bool())
1075                                                            .unwrap_or(false)
1076                                                        {
1077                                                            if let Some(text) =
1078                                                                part["text"].as_str()
1079                                                            {
1080                                                                chunks.push(Ok(
1081                                                                    StreamChunk::Reasoning(
1082                                                                        text.to_string(),
1083                                                                    ),
1084                                                                ));
1085                                                            }
1086                                                        } else if let Some(sig) = part
1087                                                            .get("thoughtSignature")
1088                                                            .and_then(|s| s.as_str())
1089                                                        {
1090                                                            chunks.push(Ok(
1091                                                                StreamChunk::Signature(
1092                                                                    sig.to_string(),
1093                                                                ),
1094                                                            ));
1095                                                        } else if let Some(text) =
1096                                                            part["text"].as_str()
1097                                                        {
1098                                                            chunks.push(Ok(StreamChunk::Content(
1099                                                                text.to_string(),
1100                                                            )));
1101                                                        }
1102                                                    }
1103                                                }
1104                                            }
1105                                        }
1106
1107                                        if let Some(usage) = json.get("usageMetadata") {
1108                                            let input =
1109                                                usage["promptTokenCount"].as_u64().unwrap_or(0);
1110                                            let output =
1111                                                usage["candidatesTokenCount"].as_u64().unwrap_or(0);
1112                                            let cached = usage["cachedContentTokenCount"]
1113                                                .as_u64()
1114                                                .unwrap_or(0);
1115
1116                                            chunks.push(Ok(StreamChunk::Usage {
1117                                                input_tokens: input,
1118                                                output_tokens: output,
1119                                                cached_tokens: cached,
1120                                            }));
1121                                        }
1122                                    }
1123                                }
1124                                Err(err) => {
1125                                    if google_stream_debug_enabled() {
1126                                        eprintln!("google stream parse error: {}", err);
1127                                    }
1128                                    chunks.push(Err(err));
1129                                }
1130                            }
1131                        }
1132                        Err(e) => {
1133                            chunks
1134                                .push(Err(Error::Inference(format!("Google stream error: {}", e))));
1135                        }
1136                    }
1137
1138                    futures::future::ready(Some(chunks))
1139                })
1140                .flat_map(futures::stream::iter),
1141        ))
1142    }
1143
1144    fn provider(&self) -> Provider {
1145        Provider::GoogleGenAI
1146    }
1147
1148    fn set_trace_callback(&mut self, callback: TraceCallback) {
1149        self.trace_callback = Some(callback);
1150    }
1151}
1152
1153/// Generate a simple random ID for tool calls
1154fn rand_id() -> u64 {
1155    use std::time::{SystemTime, UNIX_EPOCH};
1156    SystemTime::now()
1157        .duration_since(UNIX_EPOCH)
1158        .map(|d| d.as_nanos() as u64)
1159        .unwrap_or(0)
1160}
1161
1162#[cfg(test)]
1163mod tests {
1164    use super::*;
1165    use crate::types::ChatMessage;
1166
1167    #[test]
1168    fn test_client_creation() {
1169        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1170        assert_eq!(client.model, "gemini-1.5-pro");
1171        assert!(matches!(client.auth, GoogleAuth::ApiKey(ref k) if k == "test-key"));
1172        assert!(client.api_url.contains("generativelanguage.googleapis.com"));
1173    }
1174
1175    #[test]
1176    fn test_with_thinking_budget() {
1177        let client =
1178            GoogleGenAIClient::new("test-key", "gemini-1.5-pro").with_thinking_budget(1024);
1179        assert_eq!(client.thinking_budget, Some(1024));
1180    }
1181
1182    #[test]
1183    fn test_extract_system_message() {
1184        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1185        let messages = vec![
1186            ConversationMessage::Chat(ChatMessage::system("You are helpful")),
1187            ConversationMessage::Chat(ChatMessage::user("Hello")),
1188        ];
1189
1190        let (system, remaining) = client.extract_system_message(&messages).unwrap();
1191        assert_eq!(system, Some("You are helpful".to_string()));
1192        assert_eq!(remaining.len(), 1);
1193    }
1194
1195    #[test]
1196    fn test_extract_system_message_none() {
1197        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1198        let messages = vec![ConversationMessage::Chat(ChatMessage::user("Hello"))];
1199
1200        let (system, remaining) = client.extract_system_message(&messages).unwrap();
1201        assert_eq!(system, None);
1202        assert_eq!(remaining.len(), 1);
1203    }
1204
1205    #[test]
1206    fn test_convert_messages_to_contents() {
1207        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1208        let messages = vec![
1209            ConversationMessage::Chat(ChatMessage::user("Hello")),
1210            ConversationMessage::Chat(ChatMessage::assistant("Hi there!")),
1211        ];
1212
1213        let contents = client.convert_messages_to_contents(&messages).unwrap();
1214
1215        assert_eq!(contents.len(), 2);
1216        assert_eq!(contents[0]["role"], "user");
1217        assert_eq!(contents[0]["parts"][0]["text"], "Hello");
1218        assert_eq!(contents[1]["role"], "model");
1219        assert_eq!(contents[1]["parts"][0]["text"], "Hi there!");
1220    }
1221
1222    #[test]
1223    fn test_convert_tools_to_google_format() {
1224        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1225
1226        // Anthropic format tools
1227        let tools = vec![serde_json::json!({
1228            "name": "get_weather",
1229            "description": "Get weather for a location",
1230            "input_schema": {
1231                "type": "object",
1232                "properties": {
1233                    "location": {"type": "string"}
1234                }
1235            }
1236        })];
1237
1238        let google_tools = client.convert_tools_to_google_format(&tools).unwrap();
1239
1240        assert!(google_tools.is_array());
1241        let declarations = &google_tools[0]["function_declarations"];
1242        assert!(declarations.is_array());
1243        assert_eq!(declarations[0]["name"], "get_weather");
1244    }
1245
1246    #[test]
1247    fn test_build_request_body_basic() {
1248        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1249        let messages = vec![ConversationMessage::Chat(ChatMessage::user("Hello"))];
1250        let config = GenerationConfig::new("gemini-1.5-pro")
1251            .with_max_tokens(1024)
1252            .with_temperature(0.7);
1253
1254        let body = client.build_request_body(&messages, &config).unwrap();
1255
1256        assert!(body["contents"].is_array());
1257        assert_eq!(body["generationConfig"]["maxOutputTokens"], 1024);
1258        assert!((body["generationConfig"]["temperature"].as_f64().unwrap() - 0.7).abs() < 0.01);
1259    }
1260
1261    #[test]
1262    fn test_build_request_with_system() {
1263        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1264        let messages = vec![
1265            ConversationMessage::Chat(ChatMessage::system("You are helpful")),
1266            ConversationMessage::Chat(ChatMessage::user("Hello")),
1267        ];
1268        let config = GenerationConfig::new("gemini-1.5-pro");
1269
1270        let body = client.build_request_body(&messages, &config).unwrap();
1271
1272        assert!(body["systemInstruction"].is_object());
1273        assert_eq!(
1274            body["systemInstruction"]["parts"][0]["text"],
1275            "You are helpful"
1276        );
1277        // Only user message should be in contents
1278        assert_eq!(body["contents"].as_array().unwrap().len(), 1);
1279    }
1280
1281    #[test]
1282    fn test_build_request_with_thinking() {
1283        let client =
1284            GoogleGenAIClient::new("test-key", "gemini-1.5-pro").with_thinking_budget(1024);
1285        let messages = vec![ConversationMessage::Chat(ChatMessage::user("Think"))];
1286        let config = GenerationConfig::new("gemini-1.5-pro");
1287
1288        let body = client.build_request_body(&messages, &config).unwrap();
1289
1290        assert!(body["generationConfig"]["thinkingConfig"].is_object());
1291        assert_eq!(
1292            body["generationConfig"]["thinkingConfig"]["includeThoughts"],
1293            true
1294        );
1295        assert_eq!(
1296            body["generationConfig"]["thinkingConfig"]["thinkingBudget"],
1297            1024
1298        );
1299    }
1300
1301    #[test]
1302    fn test_extract_text_content() {
1303        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1304        let candidates = serde_json::json!([{
1305            "content": {
1306                "parts": [
1307                    {"text": "Hello, world!"}
1308                ]
1309            }
1310        }]);
1311
1312        let text = client.extract_text_content(&candidates);
1313        assert_eq!(text, "Hello, world!");
1314    }
1315
1316    #[test]
1317    fn test_extract_text_skips_thoughts() {
1318        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1319        let candidates = serde_json::json!([{
1320            "content": {
1321                "parts": [
1322                    {"thought": true, "text": "Let me think..."},
1323                    {"text": "The answer is 42"}
1324                ]
1325            }
1326        }]);
1327
1328        let text = client.extract_text_content(&candidates);
1329        assert_eq!(text, "The answer is 42");
1330    }
1331
1332    #[test]
1333    fn test_extract_reasoning() {
1334        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1335        let candidates = serde_json::json!([{
1336            "content": {
1337                "parts": [
1338                    {"thought": true, "text": "Let me think..."},
1339                    {"text": "The answer is 42"}
1340                ]
1341            }
1342        }]);
1343
1344        let reasoning = client.extract_reasoning(&candidates);
1345        assert_eq!(reasoning, Some("Let me think...".to_string()));
1346    }
1347
1348    #[test]
1349    fn test_extract_tool_calls() {
1350        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1351        let candidates = serde_json::json!([{
1352            "content": {
1353                "parts": [{
1354                    "functionCall": {
1355                        "name": "get_weather",
1356                        "args": {"location": "San Francisco"}
1357                    }
1358                }]
1359            }
1360        }]);
1361
1362        let tool_calls = client.extract_tool_calls(&candidates);
1363        assert_eq!(tool_calls.len(), 1);
1364        assert_eq!(tool_calls[0]["name"], "get_weather");
1365        assert_eq!(tool_calls[0]["_tool_name"], "get_weather");
1366    }
1367
1368    #[test]
1369    fn test_parse_usage() {
1370        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1371        let usage = serde_json::json!({
1372            "promptTokenCount": 100,
1373            "candidatesTokenCount": 50,
1374            "cachedContentTokenCount": 25
1375        });
1376
1377        let parsed = client.parse_usage(&usage);
1378        assert_eq!(parsed.input_tokens, 100);
1379        assert_eq!(parsed.output_tokens, 50);
1380        assert_eq!(parsed.cached_tokens, 25);
1381    }
1382
1383    #[test]
1384    fn test_get_endpoint() {
1385        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1386
1387        let endpoint = client.get_endpoint(false);
1388        assert!(endpoint.contains("generateContent"));
1389        assert!(endpoint.contains("gemini-1.5-pro"));
1390        assert!(endpoint.contains("key=test-key"));
1391
1392        let stream_endpoint = client.get_endpoint(true);
1393        assert!(stream_endpoint.contains("streamGenerateContent"));
1394    }
1395
1396    #[test]
1397    fn test_provider() {
1398        let client = GoogleGenAIClient::new("test-key", "gemini-1.5-pro");
1399        assert_eq!(client.provider(), Provider::GoogleGenAI);
1400    }
1401}