//! Gemini (Google) Provider Adapter //! //! 支持 API Key 和 OAuth 两种认证方式 //! //! ## 认证模式 //! - **Gemini**: API Key 认证 (x-goog-api-key) //! - **GeminiCli**: OAuth Bearer 认证 (用于 Gemini CLI) use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType}; use crate::provider::Provider; use crate::proxy::error::ProxyError; use reqwest::RequestBuilder; /// Gemini 适配器 pub struct GeminiAdapter; /// OAuth 凭证结构 #[derive(Debug, Clone)] #[allow(dead_code)] pub struct OAuthCredentials { pub access_token: String, pub refresh_token: Option, pub client_id: Option, pub client_secret: Option, } #[allow(dead_code)] impl OAuthCredentials { /// 检查是否需要刷新 token(有 refresh_token 但没有有效的 access_token) pub fn needs_refresh(&self) -> bool { self.refresh_token.is_some() && self.access_token.is_empty() } /// 检查是否可以刷新 token pub fn can_refresh(&self) -> bool { self.refresh_token.is_some() && self.client_id.is_some() && self.client_secret.is_some() } } impl GeminiAdapter { pub fn new() -> Self { Self } /// 获取供应商类型 /// /// 根据 API Key 格式检测: /// - GeminiCli: access_token (ya29. 开头) 或 JSON 格式凭证 /// - Gemini: 普通 API Key pub fn provider_type(&self, provider: &Provider) -> ProviderType { if let Some(key) = self.extract_key_raw(provider) { // OAuth access_token 以 ya29. 开头 if key.starts_with("ya29.") { return ProviderType::GeminiCli; } // JSON 格式的 OAuth 凭证 if key.starts_with('{') { return ProviderType::GeminiCli; } } ProviderType::Gemini } /// 检测认证类型 pub fn detect_auth_type(&self, provider: &Provider) -> AuthStrategy { match self.provider_type(provider) { ProviderType::GeminiCli => AuthStrategy::GoogleOAuth, _ => AuthStrategy::Google, } } /// 解析 OAuth 凭证 pub fn parse_oauth_credentials(&self, key: &str) -> Option { // 直接是 access_token if key.starts_with("ya29.") { return Some(OAuthCredentials { access_token: key.to_string(), refresh_token: None, client_id: None, client_secret: None, }); } // JSON 格式 if key.starts_with('{') { if let Ok(json) = serde_json::from_str::(key) { let access_token = json .get("access_token") .and_then(|v| v.as_str()) .map(|s| s.to_string()) .unwrap_or_default(); let refresh_token = json .get("refresh_token") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let client_id = json .get("client_id") .and_then(|v| v.as_str()) .map(|s| s.to_string()); let client_secret = json .get("client_secret") .and_then(|v| v.as_str()) .map(|s| s.to_string()); // 如果有 access_token 或 refresh_token,返回凭证 if !access_token.is_empty() || refresh_token.is_some() { return Some(OAuthCredentials { access_token, refresh_token, client_id, client_secret, }); } } } None } /// 从 Provider 配置中提取原始 API Key fn extract_key_raw(&self, provider: &Provider) -> Option { if let Some(env) = provider.settings_config.get("env") { // 使用 GEMINI_API_KEY if let Some(key) = env.get("GEMINI_API_KEY").and_then(|v| v.as_str()) { return Some(key.to_string()); } } // 尝试直接获取 if let Some(key) = provider .settings_config .get("apiKey") .or_else(|| provider.settings_config.get("api_key")) .and_then(|v| v.as_str()) { return Some(key.to_string()); } None } } impl Default for GeminiAdapter { fn default() -> Self { Self::new() } } impl ProviderAdapter for GeminiAdapter { fn name(&self) -> &'static str { "Gemini" } fn extract_base_url(&self, provider: &Provider) -> Result { // 从 env 中获取 if let Some(env) = provider.settings_config.get("env") { if let Some(url) = env.get("GOOGLE_GEMINI_BASE_URL").and_then(|v| v.as_str()) { return Ok(url.trim_end_matches('/').to_string()); } } // 尝试直接获取 if let Some(url) = provider .settings_config .get("base_url") .and_then(|v| v.as_str()) { return Ok(url.trim_end_matches('/').to_string()); } if let Some(url) = provider .settings_config .get("baseURL") .and_then(|v| v.as_str()) { return Ok(url.trim_end_matches('/').to_string()); } Err(ProxyError::ConfigError( "Gemini Provider 缺少 base_url 配置".to_string(), )) } fn extract_auth(&self, provider: &Provider) -> Option { let key = self.extract_key_raw(provider)?; let strategy = self.detect_auth_type(provider); match strategy { AuthStrategy::GoogleOAuth => { // 解析 OAuth 凭证 if let Some(creds) = self.parse_oauth_credentials(&key) { Some(AuthInfo::with_access_token(key, creds.access_token)) } else { // 回退到普通 API Key Some(AuthInfo::new(key, AuthStrategy::Google)) } } _ => Some(AuthInfo::new(key, AuthStrategy::Google)), } } fn build_url(&self, base_url: &str, endpoint: &str) -> String { let base_trimmed = base_url.trim_end_matches('/'); let endpoint_trimmed = endpoint.trim_start_matches('/'); let mut url = format!("{base_trimmed}/{endpoint_trimmed}"); // 处理 /v1beta 路径去重 let version_patterns = ["/v1beta", "/v1"]; for pattern in &version_patterns { let duplicate = format!("{pattern}{pattern}"); if url.contains(&duplicate) { url = url.replace(&duplicate, pattern); } } url } fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder { match auth.strategy { // OAuth Bearer 认证 AuthStrategy::GoogleOAuth => { let token = auth.access_token.as_ref().unwrap_or(&auth.api_key); request .header("Authorization", format!("Bearer {token}")) .header("x-goog-api-client", "GeminiCLI/1.0") } // API Key 认证 _ => request.header("x-goog-api-key", &auth.api_key), } } } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn create_provider(config: serde_json::Value) -> Provider { Provider { id: "test".to_string(), name: "Test Gemini".to_string(), settings_config: config, website_url: None, category: Some("gemini".to_string()), created_at: None, sort_index: None, notes: None, meta: None, icon: None, icon_color: None, in_failover_queue: false, } } #[test] fn test_extract_base_url_from_env() { let adapter = GeminiAdapter::new(); let provider = create_provider(json!({ "env": { "GOOGLE_GEMINI_BASE_URL": "https://generativelanguage.googleapis.com/v1beta" } })); let url = adapter.extract_base_url(&provider).unwrap(); assert_eq!(url, "https://generativelanguage.googleapis.com/v1beta"); } #[test] fn test_extract_auth_api_key() { let adapter = GeminiAdapter::new(); let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "AIza-test-key-12345678" } })); let auth = adapter.extract_auth(&provider).unwrap(); assert_eq!(auth.api_key, "AIza-test-key-12345678"); assert_eq!(auth.strategy, AuthStrategy::Google); assert!(auth.access_token.is_none()); } #[test] fn test_extract_auth_oauth_access_token() { let adapter = GeminiAdapter::new(); let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "ya29.test-access-token-12345" } })); let auth = adapter.extract_auth(&provider).unwrap(); assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth); assert_eq!( auth.access_token, Some("ya29.test-access-token-12345".to_string()) ); } #[test] fn test_extract_auth_oauth_json() { let adapter = GeminiAdapter::new(); let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "{\"access_token\":\"ya29.test-token\",\"refresh_token\":\"1//refresh\"}" } })); let auth = adapter.extract_auth(&provider).unwrap(); assert_eq!(auth.strategy, AuthStrategy::GoogleOAuth); assert_eq!(auth.access_token, Some("ya29.test-token".to_string())); } #[test] fn test_provider_type_detection() { let adapter = GeminiAdapter::new(); // API Key let api_key_provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "AIza-test-key" } })); assert_eq!( adapter.provider_type(&api_key_provider), ProviderType::Gemini ); // OAuth access_token let oauth_provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "ya29.test-token" } })); assert_eq!( adapter.provider_type(&oauth_provider), ProviderType::GeminiCli ); // OAuth JSON let oauth_json_provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "{\"access_token\":\"ya29.test\"}" } })); assert_eq!( adapter.provider_type(&oauth_json_provider), ProviderType::GeminiCli ); } #[test] fn test_extract_auth_fallback() { let adapter = GeminiAdapter::new(); let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "AIza-fallback-key" } })); let auth = adapter.extract_auth(&provider).unwrap(); assert_eq!(auth.api_key, "AIza-fallback-key"); } #[test] fn test_build_url_dedup() { let adapter = GeminiAdapter::new(); // 模拟 base_url 已包含 /v1beta,endpoint 也包含 /v1beta let url = adapter.build_url( "https://generativelanguage.googleapis.com/v1beta", "/v1beta/models/gemini-pro:generateContent", ); assert_eq!( url, "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent" ); } #[test] fn test_build_url_normal() { let adapter = GeminiAdapter::new(); let url = adapter.build_url( "https://generativelanguage.googleapis.com/v1beta", "/models/gemini-pro:generateContent", ); assert_eq!( url, "https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent" ); } #[test] fn test_parse_oauth_credentials_direct_token() { let adapter = GeminiAdapter::new(); let creds = adapter .parse_oauth_credentials("ya29.test-access-token") .unwrap(); assert_eq!(creds.access_token, "ya29.test-access-token"); assert!(creds.refresh_token.is_none()); } #[test] fn test_parse_oauth_credentials_json() { let adapter = GeminiAdapter::new(); let creds = adapter .parse_oauth_credentials( "{\"access_token\":\"ya29.test\",\"refresh_token\":\"1//refresh\"}", ) .unwrap(); assert_eq!(creds.access_token, "ya29.test"); assert_eq!(creds.refresh_token, Some("1//refresh".to_string())); } #[test] fn test_parse_oauth_credentials_invalid() { let adapter = GeminiAdapter::new(); assert!(adapter.parse_oauth_credentials("AIza-api-key").is_none()); assert!(adapter.parse_oauth_credentials("invalid-json{").is_none()); } }