1use 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#[derive(Clone)]
56pub enum GoogleAuth {
57 ApiKey(String),
59 #[cfg(feature = "google-adc")]
61 Adc(Arc<RwLock<Option<Arc<dyn gcp_auth::TokenProvider>>>>),
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
66#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
67pub enum FileState {
68 Processing,
70 Active,
72 Failed,
74}
75
76#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct UploadedFile {
80 pub name: String,
82 #[serde(default)]
84 pub display_name: String,
85 pub mime_type: String,
87 #[serde(default)]
89 pub size_bytes: String,
90 pub uri: String,
92 pub state: FileState,
94 #[serde(default)]
96 pub error: Option<serde_json::Value>,
97}
98
99#[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 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 #[cfg(feature = "google-adc")]
142 pub fn from_adc(model: impl Into<String>) -> Self {
143 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 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 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 #[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 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 pub fn with_thinking_budget(mut self, budget: u32) -> Self {
204 self.thinking_budget = Some(budget);
205 self
206 }
207
208 pub fn with_api_url(mut self, url: impl Into<String>) -> Self {
210 self.api_url = url.into();
211 self
212 }
213
214 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 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)] 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 #[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 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 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 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 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 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)] 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 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 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)] 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 #[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 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 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 fn build_request_body(
523 &self,
524 messages: &[ConversationMessage],
525 config: &GenerationConfig,
526 ) -> Result<serde_json::Value> {
527 let (system_instruction, messages) = self.extract_system_message(messages)?;
529
530 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 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 if let Some(system) = system_instruction {
552 request["systemInstruction"] = serde_json::json!({
553 "parts": [{"text": system}]
554 });
555 }
556
557 if let Some(ref tools) = config.tools {
559 if !tools.is_empty() {
560 let google_tools = self.convert_tools_to_google_format(tools)?;
562 request["tools"] = google_tools;
563 }
564 }
565
566 if let Some(budget) = self.thinking_budget {
568 request["generationConfig"]["thinkingConfig"] = serde_json::json!({
569 "includeThoughts": true,
570 "thinkingBudget": budget
571 });
572 }
573
574 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 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 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, ChatRole::Tool => "user", };
612
613 contents.push(serde_json::json!({
614 "role": role,
615 "parts": [{"text": chat_msg.content}]
616 }));
617 }
618 ConversationMessage::ToolCall(tool_call) => {
619 let mut part = serde_json::json!({
621 "functionCall": {
622 "name": tool_call.tool_name,
623 "args": tool_call.args
624 }
625 });
626 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 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 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 let role = match multimodal_msg.role {
675 ChatRole::User => "user",
676 ChatRole::Assistant => "model",
677 ChatRole::System => continue, 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 fn convert_tools_to_google_format(
700 &self,
701 tools: &[serde_json::Value],
702 ) -> Result<serde_json::Value> {
703 if tools
705 .iter()
706 .any(|t| t.get("function_declarations").is_some())
707 {
708 return Ok(serde_json::json!(tools));
709 }
710
711 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 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 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 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 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 let id = format!("google_{}_{}", name, rand_id());
790
791 let thought_signature = part.get("thoughtSignature").cloned();
793
794 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 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 #[allow(dead_code)]
831 fn is_vertex_ai(&self) -> bool {
832 self.api_url.contains("aiplatform.googleapis.com")
833 }
834
835 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 format!(
847 "{}/models/{}:{}?key={}",
848 self.api_url, self.model, action, key
849 )
850 }
851 #[cfg(feature = "google-adc")]
852 GoogleAuth::Adc(_) => {
853 format!("{}:{}", self.api_url, action)
855 }
856 }
857 }
858
859 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)] 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 #[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 fn handle_error_response(&self, status: StatusCode, body: String) -> Error {
894 match status {
895 StatusCode::BAD_REQUEST | StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
897 Error::NonRetryable(format!("{}: {}", status, body))
898 }
899 StatusCode::TOO_MANY_REQUESTS => Error::Inference(format!("Rate limited: {}", body)),
901 StatusCode::INTERNAL_SERVER_ERROR
903 | StatusCode::BAD_GATEWAY
904 | StatusCode::SERVICE_UNAVAILABLE
905 | StatusCode::GATEWAY_TIMEOUT => Error::Inference(format!("{}: {}", status, body)),
906 _ => Error::Inference(format!("{}: {}", status, body)),
908 }
909 }
910
911 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 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 let usage = self.parse_usage(&body["usageMetadata"]);
960
961 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 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 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
1153fn 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 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 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}