Skip to main content

rust_genai/
client.rs

1//! Client configuration and transport layer.
2
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5use std::sync::{Arc, LazyLock};
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use reqwest::header::{HeaderMap, HeaderName, HeaderValue, AUTHORIZATION};
9use reqwest::{Client as HttpClient, Proxy};
10use tokio::sync::OnceCell;
11
12use crate::auth::OAuthTokenProvider;
13use crate::error::{Error, Result};
14use google_cloud_auth::credentials::{
15    Builder as AuthBuilder, CacheableResource, Credentials as GoogleCredentials,
16};
17use http::Extensions;
18use rust_genai_types::http::HttpRetryOptions;
19
20const X_GOOG_API_CLIENT_HEADER: &str = "x-goog-api-client";
21const SDK_USAGE_HEADER_VALUE: &str = concat!(
22    "google-genai-sdk/",
23    env!("CARGO_PKG_VERSION"),
24    " gl-rust/unknown"
25);
26
27/// Gemini 客户端。
28#[derive(Clone)]
29pub struct Client {
30    inner: Arc<ClientInner>,
31}
32
33pub(crate) struct ClientInner {
34    pub http: HttpClient,
35    pub config: ClientConfig,
36    pub api_client: ApiClient,
37    pub(crate) auth_provider: Option<AuthProvider>,
38}
39
40/// 客户端配置。
41#[derive(Debug, Clone)]
42pub struct ClientConfig {
43    /// API 密钥(Gemini API)。
44    pub api_key: Option<String>,
45    /// 后端选择。
46    pub backend: Backend,
47    /// Vertex AI 配置。
48    pub vertex_config: Option<VertexConfig>,
49    /// HTTP 配置。
50    pub http_options: HttpOptions,
51    /// 认证信息。
52    pub credentials: Credentials,
53    /// OAuth scopes(服务账号/ADC 使用)。
54    pub auth_scopes: Vec<String>,
55}
56
57/// 后端选择。
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Backend {
60    GeminiApi,
61    VertexAi,
62}
63
64/// 认证方式。
65#[derive(Debug, Clone)]
66pub enum Credentials {
67    /// API Key(Gemini API)。
68    ApiKey(String),
69    /// OAuth 用户凭据。
70    OAuth {
71        client_secret_path: PathBuf,
72        token_cache_path: Option<PathBuf>,
73    },
74    /// Application Default Credentials (ADC)。
75    ApplicationDefault,
76}
77
78/// Vertex AI 配置。
79#[derive(Debug, Clone)]
80pub struct VertexConfig {
81    pub project: String,
82    pub location: String,
83    pub credentials: Option<VertexCredentials>,
84}
85
86/// Vertex AI 认证占位。
87#[derive(Debug, Clone)]
88pub struct VertexCredentials {
89    pub access_token: Option<String>,
90}
91
92/// HTTP 配置。
93#[derive(Debug, Clone, Default)]
94pub struct HttpOptions {
95    pub timeout: Option<u64>,
96    pub proxy: Option<String>,
97    pub headers: HashMap<String, String>,
98    pub base_url: Option<String>,
99    pub api_version: Option<String>,
100    pub retry_options: Option<HttpRetryOptions>,
101}
102
103impl Client {
104    /// 创建新客户端(Gemini API)。
105    ///
106    /// # Errors
107    /// 当配置无效或构建客户端失败时返回错误。
108    pub fn new(api_key: impl Into<String>) -> Result<Self> {
109        Self::builder()
110            .api_key(api_key)
111            .backend(Backend::GeminiApi)
112            .build()
113    }
114
115    /// 从环境变量创建客户端。
116    ///
117    /// # Errors
118    /// 当环境变量缺失或构建客户端失败时返回错误。
119    pub fn from_env() -> Result<Self> {
120        let vertex_override = env_flag("GOOGLE_GENAI_USE_VERTEXAI");
121        let vertex_project = first_nonempty_env(&["GOOGLE_CLOUD_PROJECT"]);
122        let vertex_location = first_nonempty_env(&["GOOGLE_CLOUD_LOCATION"]);
123        let api_key = first_nonempty_env(&["GEMINI_API_KEY", "GOOGLE_API_KEY"]);
124        let has_complete_vertex_env = vertex_project.is_some() && vertex_location.is_some();
125        let use_vertex = match vertex_override {
126            Some(flag) => flag,
127            None => has_complete_vertex_env && api_key.is_none(),
128        };
129
130        let mut builder = if use_vertex {
131            let mut builder = Self::builder().backend(Backend::VertexAi);
132            if let Some(project) = vertex_project {
133                builder = builder.vertex_project(project);
134            }
135            if let Some(location) = vertex_location {
136                builder = builder.vertex_location(location);
137            }
138            builder
139        } else {
140            let api_key = api_key.ok_or_else(|| Error::InvalidConfig {
141                message: "GEMINI_API_KEY or GOOGLE_API_KEY not found".into(),
142            })?;
143            Self::builder().api_key(api_key).backend(Backend::GeminiApi)
144        };
145
146        let base_url_envs: &[&str] = if use_vertex {
147            &["GOOGLE_GENAI_BASE_URL", "GENAI_BASE_URL"]
148        } else {
149            &["GOOGLE_GENAI_BASE_URL", "GENAI_BASE_URL", "GEMINI_BASE_URL"]
150        };
151        if let Some(base_url) = first_nonempty_env(base_url_envs) {
152            builder = builder.base_url(base_url);
153        }
154        if let Some(api_version) =
155            first_nonempty_env(&["GOOGLE_GENAI_API_VERSION", "GENAI_API_VERSION"])
156        {
157            builder = builder.api_version(api_version);
158        }
159        builder.build()
160    }
161
162    /// 创建 Vertex AI 客户端。
163    ///
164    /// # Errors
165    /// 当配置无效或构建客户端失败时返回错误。
166    pub fn new_vertex(project: impl Into<String>, location: impl Into<String>) -> Result<Self> {
167        Self::builder()
168            .backend(Backend::VertexAi)
169            .vertex_project(project)
170            .vertex_location(location)
171            .build()
172    }
173
174    /// 使用 OAuth 凭据创建客户端(默认读取 token.json)。
175    ///
176    /// # Errors
177    /// 当凭据路径无效或构建客户端失败时返回错误。
178    pub fn with_oauth(client_secret_path: impl AsRef<Path>) -> Result<Self> {
179        Self::builder()
180            .credentials(Credentials::OAuth {
181                client_secret_path: client_secret_path.as_ref().to_path_buf(),
182                token_cache_path: None,
183            })
184            .build()
185    }
186
187    /// 使用 Application Default Credentials 创建客户端。
188    ///
189    /// # Errors
190    /// 当构建客户端失败时返回错误。
191    pub fn with_adc() -> Result<Self> {
192        Self::builder()
193            .credentials(Credentials::ApplicationDefault)
194            .build()
195    }
196
197    /// 创建 Builder。
198    #[must_use]
199    pub fn builder() -> ClientBuilder {
200        ClientBuilder::default()
201    }
202
203    /// 访问 Models API。
204    #[must_use]
205    pub fn models(&self) -> crate::models::Models {
206        crate::models::Models::new(self.inner.clone())
207    }
208
209    /// 访问 Chats API。
210    #[must_use]
211    pub fn chats(&self) -> crate::chats::Chats {
212        crate::chats::Chats::new(self.inner.clone())
213    }
214
215    /// 访问 Files API。
216    #[must_use]
217    pub fn files(&self) -> crate::files::Files {
218        crate::files::Files::new(self.inner.clone())
219    }
220
221    /// 访问 `FileSearchStores` API。
222    #[must_use]
223    pub fn file_search_stores(&self) -> crate::file_search_stores::FileSearchStores {
224        crate::file_search_stores::FileSearchStores::new(self.inner.clone())
225    }
226
227    /// 访问 Documents API。
228    #[must_use]
229    pub fn documents(&self) -> crate::documents::Documents {
230        crate::documents::Documents::new(self.inner.clone())
231    }
232
233    /// 访问 Live API。
234    #[must_use]
235    pub fn live(&self) -> crate::live::Live {
236        crate::live::Live::new(self.inner.clone())
237    }
238
239    /// 访问 Live Music API。
240    #[must_use]
241    pub fn live_music(&self) -> crate::live_music::LiveMusic {
242        crate::live_music::LiveMusic::new(self.inner.clone())
243    }
244
245    /// 访问 Caches API。
246    #[must_use]
247    pub fn caches(&self) -> crate::caches::Caches {
248        crate::caches::Caches::new(self.inner.clone())
249    }
250
251    /// 访问 Batches API。
252    #[must_use]
253    pub fn batches(&self) -> crate::batches::Batches {
254        crate::batches::Batches::new(self.inner.clone())
255    }
256
257    /// 访问 Tunings API。
258    #[must_use]
259    pub fn tunings(&self) -> crate::tunings::Tunings {
260        crate::tunings::Tunings::new(self.inner.clone())
261    }
262
263    /// 访问 Operations API。
264    #[must_use]
265    pub fn operations(&self) -> crate::operations::Operations {
266        crate::operations::Operations::new(self.inner.clone())
267    }
268
269    /// 访问 `AuthTokens` API(Ephemeral Tokens)。
270    #[must_use]
271    pub fn auth_tokens(&self) -> crate::tokens::AuthTokens {
272        crate::tokens::AuthTokens::new(self.inner.clone())
273    }
274
275    /// 访问 Tokens API(Ephemeral Tokens)。
276    ///
277    /// 与官方 SDK 的 `tokens` 命名保持一致(等价于 `auth_tokens()`)。
278    #[must_use]
279    pub fn tokens(&self) -> crate::tokens::Tokens {
280        self.auth_tokens()
281    }
282
283    /// 访问 Interactions API。
284    #[must_use]
285    pub fn interactions(&self) -> crate::interactions::Interactions {
286        crate::interactions::Interactions::new(self.inner.clone())
287    }
288
289    /// 访问 Webhooks API。
290    #[must_use]
291    pub fn webhooks(&self) -> crate::webhooks::Webhooks {
292        crate::webhooks::Webhooks::new(self.inner.clone())
293    }
294
295    /// 访问 Deep Research。
296    #[must_use]
297    pub fn deep_research(&self) -> crate::deep_research::DeepResearch {
298        crate::deep_research::DeepResearch::new(self.inner.clone())
299    }
300}
301
302/// 客户端 Builder。
303#[derive(Default)]
304pub struct ClientBuilder {
305    api_key: Option<String>,
306    credentials: Option<Credentials>,
307    backend: Option<Backend>,
308    vertex_project: Option<String>,
309    vertex_location: Option<String>,
310    http_options: HttpOptions,
311    auth_scopes: Option<Vec<String>>,
312}
313
314impl ClientBuilder {
315    /// 设置 API Key(Gemini API)。
316    #[must_use]
317    pub fn api_key(mut self, key: impl Into<String>) -> Self {
318        self.api_key = Some(key.into());
319        self
320    }
321
322    /// 设置认证方式(OAuth/ADC/API Key)。
323    #[must_use]
324    pub fn credentials(mut self, credentials: Credentials) -> Self {
325        self.credentials = Some(credentials);
326        self
327    }
328
329    /// 设置后端(Gemini API 或 Vertex AI)。
330    #[must_use]
331    pub const fn backend(mut self, backend: Backend) -> Self {
332        self.backend = Some(backend);
333        self
334    }
335
336    /// 设置 Vertex AI 项目 ID。
337    #[must_use]
338    pub fn vertex_project(mut self, project: impl Into<String>) -> Self {
339        self.vertex_project = Some(project.into());
340        self
341    }
342
343    /// 设置 Vertex AI 区域。
344    #[must_use]
345    pub fn vertex_location(mut self, location: impl Into<String>) -> Self {
346        self.vertex_location = Some(location.into());
347        self
348    }
349
350    /// 设置请求超时(秒)。
351    #[must_use]
352    pub const fn timeout(mut self, secs: u64) -> Self {
353        self.http_options.timeout = Some(secs);
354        self
355    }
356
357    /// 设置代理。
358    #[must_use]
359    pub fn proxy(mut self, url: impl Into<String>) -> Self {
360        self.http_options.proxy = Some(url.into());
361        self
362    }
363
364    /// 增加默认 HTTP 头。
365    #[must_use]
366    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
367        self.http_options.headers.insert(key.into(), value.into());
368        self
369    }
370
371    /// 设置自定义基础 URL。
372    #[must_use]
373    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
374        self.http_options.base_url = Some(base_url.into());
375        self
376    }
377
378    /// 设置 API 版本。
379    #[must_use]
380    pub fn api_version(mut self, api_version: impl Into<String>) -> Self {
381        self.http_options.api_version = Some(api_version.into());
382        self
383    }
384
385    /// 设置 HTTP 重试选项。
386    #[must_use]
387    pub fn retry_options(mut self, retry_options: HttpRetryOptions) -> Self {
388        self.http_options.retry_options = Some(retry_options);
389        self
390    }
391
392    /// 设置 OAuth scopes。
393    #[must_use]
394    pub fn auth_scopes(mut self, scopes: Vec<String>) -> Self {
395        self.auth_scopes = Some(scopes);
396        self
397    }
398
399    /// 构建客户端。
400    ///
401    /// # Errors
402    /// 当配置不完整、参数无效或构建 HTTP 客户端失败时返回错误。
403    pub fn build(self) -> Result<Client> {
404        let Self {
405            api_key,
406            credentials,
407            backend,
408            vertex_project,
409            vertex_location,
410            http_options,
411            auth_scopes,
412        } = self;
413
414        let backend = Self::resolve_backend(
415            backend,
416            vertex_project.as_deref(),
417            vertex_location.as_deref(),
418        );
419        Self::validate_vertex_config(
420            backend,
421            vertex_project.as_deref(),
422            vertex_location.as_deref(),
423        )?;
424        let credentials = Self::resolve_credentials(backend, api_key.as_deref(), credentials)?;
425        let headers = Self::build_headers(&http_options, backend, &credentials)?;
426        let http = Self::build_http_client(&http_options, headers)?;
427
428        let auth_scopes = auth_scopes.unwrap_or_else(|| default_auth_scopes(backend));
429        let api_key = match &credentials {
430            Credentials::ApiKey(key) => Some(key.clone()),
431            _ => None,
432        };
433        let vertex_config = Self::build_vertex_config(backend, vertex_project, vertex_location)?;
434        let config = ClientConfig {
435            api_key,
436            backend,
437            vertex_config,
438            http_options,
439            credentials: credentials.clone(),
440            auth_scopes,
441        };
442
443        let auth_provider = build_auth_provider(&credentials)?;
444        let api_client = ApiClient::new(&config);
445
446        Ok(Client {
447            inner: Arc::new(ClientInner {
448                http,
449                config,
450                api_client,
451                auth_provider,
452            }),
453        })
454    }
455
456    fn resolve_backend(
457        backend: Option<Backend>,
458        vertex_project: Option<&str>,
459        vertex_location: Option<&str>,
460    ) -> Backend {
461        backend.unwrap_or_else(|| {
462            if vertex_project.is_some() || vertex_location.is_some() {
463                Backend::VertexAi
464            } else {
465                Backend::GeminiApi
466            }
467        })
468    }
469
470    fn validate_vertex_config(
471        backend: Backend,
472        vertex_project: Option<&str>,
473        vertex_location: Option<&str>,
474    ) -> Result<()> {
475        if backend == Backend::VertexAi && (vertex_project.is_none() || vertex_location.is_none()) {
476            return Err(Error::InvalidConfig {
477                message: "Project and location required for Vertex AI".into(),
478            });
479        }
480        Ok(())
481    }
482
483    fn resolve_credentials(
484        backend: Backend,
485        api_key: Option<&str>,
486        credentials: Option<Credentials>,
487    ) -> Result<Credentials> {
488        if credentials.is_some()
489            && api_key.is_some()
490            && !matches!(credentials, Some(Credentials::ApiKey(_)))
491        {
492            return Err(Error::InvalidConfig {
493                message: "API key cannot be combined with OAuth/ADC credentials".into(),
494            });
495        }
496
497        let credentials = match credentials {
498            Some(credentials) => credentials,
499            None => {
500                if let Some(api_key) = api_key {
501                    Credentials::ApiKey(api_key.to_string())
502                } else if backend == Backend::VertexAi {
503                    Credentials::ApplicationDefault
504                } else {
505                    return Err(Error::InvalidConfig {
506                        message: "API key or OAuth credentials required for Gemini API".into(),
507                    });
508                }
509            }
510        };
511
512        if backend == Backend::VertexAi && matches!(credentials, Credentials::ApiKey(_)) {
513            return Err(Error::InvalidConfig {
514                message: "Vertex AI does not support API key authentication".into(),
515            });
516        }
517
518        Ok(credentials)
519    }
520
521    fn build_headers(
522        http_options: &HttpOptions,
523        backend: Backend,
524        credentials: &Credentials,
525    ) -> Result<HeaderMap> {
526        let mut headers = HeaderMap::new();
527        for (key, value) in &http_options.headers {
528            let name =
529                HeaderName::from_bytes(key.as_bytes()).map_err(|_| Error::InvalidConfig {
530                    message: format!("Invalid header name: {key}"),
531                })?;
532            let value = HeaderValue::from_str(value).map_err(|_| Error::InvalidConfig {
533                message: format!("Invalid header value for {key}"),
534            })?;
535            headers.insert(name, value);
536        }
537
538        if backend == Backend::GeminiApi {
539            let api_key = match credentials {
540                Credentials::ApiKey(key) => key.as_str(),
541                _ => "",
542            };
543            let header_name = HeaderName::from_static("x-goog-api-key");
544            if !api_key.is_empty() && !headers.contains_key(&header_name) {
545                let mut header_value =
546                    HeaderValue::from_str(api_key).map_err(|_| Error::InvalidConfig {
547                        message: "Invalid API key value".into(),
548                    })?;
549                header_value.set_sensitive(true);
550                headers.insert(header_name, header_value);
551            }
552        }
553
554        Ok(headers)
555    }
556
557    fn build_http_client(http_options: &HttpOptions, headers: HeaderMap) -> Result<HttpClient> {
558        let mut http_builder = HttpClient::builder();
559        if let Some(timeout) = http_options.timeout {
560            http_builder = http_builder.timeout(Duration::from_secs(timeout));
561        }
562
563        if let Some(proxy_url) = &http_options.proxy {
564            let proxy = Proxy::all(proxy_url).map_err(|e| Error::InvalidConfig {
565                message: format!("Invalid proxy: {e}"),
566            })?;
567            http_builder = http_builder.proxy(proxy);
568        }
569
570        if !headers.is_empty() {
571            http_builder = http_builder.default_headers(headers);
572        }
573
574        Ok(http_builder.build()?)
575    }
576
577    fn build_vertex_config(
578        backend: Backend,
579        vertex_project: Option<String>,
580        vertex_location: Option<String>,
581    ) -> Result<Option<VertexConfig>> {
582        if backend != Backend::VertexAi {
583            return Ok(None);
584        }
585        let project = vertex_project.ok_or_else(|| Error::InvalidConfig {
586            message: "Project and location required for Vertex AI".into(),
587        })?;
588        let location = vertex_location.ok_or_else(|| Error::InvalidConfig {
589            message: "Project and location required for Vertex AI".into(),
590        })?;
591        Ok(Some(VertexConfig {
592            project,
593            location,
594            credentials: None,
595        }))
596    }
597}
598
599fn build_auth_provider(credentials: &Credentials) -> Result<Option<AuthProvider>> {
600    match credentials {
601        Credentials::ApiKey(_) => Ok(None),
602        Credentials::OAuth {
603            client_secret_path,
604            token_cache_path,
605        } => Ok(Some(AuthProvider::OAuth(Arc::new(
606            OAuthTokenProvider::from_paths(client_secret_path.clone(), token_cache_path.clone())?,
607        )))),
608        Credentials::ApplicationDefault => Ok(Some(AuthProvider::ApplicationDefault(Arc::new(
609            OnceCell::new(),
610        )))),
611    }
612}
613
614#[derive(Clone)]
615pub(crate) enum AuthProvider {
616    OAuth(Arc<OAuthTokenProvider>),
617    ApplicationDefault(Arc<OnceCell<Arc<GoogleCredentials>>>),
618}
619
620impl AuthProvider {
621    async fn headers(&self, scopes: &[&str]) -> Result<HeaderMap> {
622        match self {
623            Self::OAuth(provider) => {
624                let token = provider.token().await?;
625                let mut header =
626                    HeaderValue::from_str(&format!("Bearer {token}")).map_err(|_| Error::Auth {
627                        message: "Invalid OAuth access token".into(),
628                    })?;
629                header.set_sensitive(true);
630                let mut headers = HeaderMap::new();
631                headers.insert(AUTHORIZATION, header);
632                Ok(headers)
633            }
634            Self::ApplicationDefault(cell) => {
635                let credentials = cell
636                    .get_or_try_init(|| async {
637                        AuthBuilder::default()
638                            .with_scopes(scopes.iter().copied())
639                            .build()
640                            .map(Arc::new)
641                            .map_err(|err| Error::Auth {
642                                message: format!("ADC init failed: {err}"),
643                            })
644                    })
645                    .await?;
646                let headers = credentials
647                    .headers(Extensions::new())
648                    .await
649                    .map_err(|err| Error::Auth {
650                        message: format!("ADC header fetch failed: {err}"),
651                    })?;
652                match headers {
653                    CacheableResource::New { data, .. } => Ok(data),
654                    CacheableResource::NotModified => Err(Error::Auth {
655                        message: "ADC header fetch returned NotModified without cached headers"
656                            .into(),
657                    }),
658                }
659            }
660        }
661    }
662}
663
664const DEFAULT_RETRY_ATTEMPTS: u32 = 5; // Including the initial call
665const DEFAULT_RETRY_INITIAL_DELAY_SECS: f64 = 1.0;
666const DEFAULT_RETRY_MAX_DELAY_SECS: f64 = 60.0;
667const DEFAULT_RETRY_EXP_BASE: f64 = 2.0;
668const DEFAULT_RETRY_JITTER: f64 = 1.0;
669const DEFAULT_RETRY_HTTP_STATUS_CODES: [u16; 6] = [408, 429, 500, 502, 503, 504];
670static DEFAULT_HTTP_RETRY_OPTIONS: LazyLock<HttpRetryOptions> =
671    LazyLock::new(|| HttpRetryOptions {
672        attempts: Some(DEFAULT_RETRY_ATTEMPTS),
673        initial_delay: Some(DEFAULT_RETRY_INITIAL_DELAY_SECS),
674        max_delay: Some(DEFAULT_RETRY_MAX_DELAY_SECS),
675        exp_base: Some(DEFAULT_RETRY_EXP_BASE),
676        jitter: Some(DEFAULT_RETRY_JITTER),
677        http_status_codes: Some(DEFAULT_RETRY_HTTP_STATUS_CODES.to_vec()),
678    });
679
680#[derive(Debug, Clone, Copy)]
681pub(crate) struct RetryMetadata {
682    pub attempts: u32,
683    pub retryable: bool,
684}
685
686impl ClientInner {
687    /// 发送请求并自动注入鉴权头。
688    ///
689    /// # Errors
690    /// 当请求构建、鉴权头获取或网络请求失败时返回错误。
691    pub async fn send(&self, request: reqwest::RequestBuilder) -> Result<reqwest::Response> {
692        self.send_with_http_options(request, None).await
693    }
694
695    /// 发送请求(支持 per-request HTTP options,例如 retry_options)。
696    ///
697    /// # Errors
698    /// 当请求构建、鉴权头获取或网络请求失败时返回错误。
699    pub async fn send_with_http_options(
700        &self,
701        request: reqwest::RequestBuilder,
702        request_http_options: Option<&rust_genai_types::http::HttpOptions>,
703    ) -> Result<reqwest::Response> {
704        let retry_options = request_http_options
705            .and_then(|options| options.retry_options.as_ref())
706            .or(self.config.http_options.retry_options.as_ref())
707            .unwrap_or(&DEFAULT_HTTP_RETRY_OPTIONS);
708
709        let request_template = request.build()?;
710        self.execute_with_retry(request_template, retry_options)
711            .await
712    }
713
714    async fn execute_once(&self, mut request: reqwest::Request) -> Result<reqwest::Response> {
715        self.prepare_request(&mut request).await?;
716        Ok(self.http.execute(request).await?)
717    }
718
719    async fn execute_with_retry(
720        &self,
721        request_template: reqwest::Request,
722        retry_options: &HttpRetryOptions,
723    ) -> Result<reqwest::Response> {
724        let attempts = retry_options.attempts.unwrap_or(DEFAULT_RETRY_ATTEMPTS);
725        let retryable_codes: &[u16] = retry_options
726            .http_status_codes
727            .as_deref()
728            .unwrap_or(&DEFAULT_RETRY_HTTP_STATUS_CODES);
729        if attempts <= 1 {
730            let mut response = self.execute_once(request_template).await?;
731            if !response.status().is_success() {
732                attach_retry_metadata_for_codes(&mut response, 1, retryable_codes);
733            }
734            return Ok(response);
735        }
736
737        // If the request body can't be cloned, we can't safely retry.
738        if request_template.try_clone().is_none() {
739            let mut response = self.execute_once(request_template).await?;
740            if !response.status().is_success() {
741                attach_retry_metadata_for_codes(&mut response, 1, retryable_codes);
742            }
743            return Ok(response);
744        }
745
746        for attempt in 0..attempts {
747            let request = request_template
748                .try_clone()
749                .expect("request_template is cloneable");
750            let response = self.execute_once(request).await?;
751
752            if response.status().is_success() {
753                return Ok(response);
754            }
755
756            let status = response.status().as_u16();
757            let should_retry = retryable_codes.contains(&status);
758            let is_last_attempt = attempt + 1 >= attempts;
759            if !should_retry || is_last_attempt {
760                let mut response = response;
761                attach_retry_metadata(&mut response, attempt + 1, should_retry);
762                return Ok(response);
763            }
764
765            let delay = bounded_retry_delay_secs(
766                retry_options,
767                attempt,
768                retry_after_delay_secs(response.headers()),
769            );
770            // Drop the response before retrying to release the connection back to the pool.
771            drop(response);
772            if delay > 0.0 {
773                tokio::time::sleep(Duration::from_secs_f64(delay)).await;
774            }
775        }
776
777        // Loop always returns on success or final attempt.
778        unreachable!("retry loop must return a response");
779    }
780
781    async fn prepare_request(&self, request: &mut reqwest::Request) -> Result<()> {
782        if let Some(headers) = self.auth_headers().await? {
783            for (name, value) in &headers {
784                if request.headers().contains_key(name) {
785                    continue;
786                }
787                let mut value = value.clone();
788                if name == AUTHORIZATION {
789                    value.set_sensitive(true);
790                }
791                request.headers_mut().insert(name.clone(), value);
792            }
793        }
794        if self.config.backend == Backend::GeminiApi {
795            append_sdk_usage_header(request.headers_mut())?;
796        }
797        #[cfg(feature = "mcp")]
798        crate::mcp::append_mcp_usage_header(request.headers_mut())?;
799        Ok(())
800    }
801
802    async fn auth_headers(&self) -> Result<Option<HeaderMap>> {
803        let Some(provider) = &self.auth_provider else {
804            return Ok(None);
805        };
806
807        let scopes: Vec<&str> = self.config.auth_scopes.iter().map(String::as_str).collect();
808        let headers = provider.headers(&scopes).await?;
809        Ok(Some(headers))
810    }
811}
812
813fn append_sdk_usage_header(headers: &mut HeaderMap) -> Result<()> {
814    let header_name = HeaderName::from_static(X_GOOG_API_CLIENT_HEADER);
815    let existing_values = headers
816        .get_all(&header_name)
817        .iter()
818        .map(|value| {
819            value
820                .to_str()
821                .map(str::trim)
822                .map(str::to_string)
823                .map_err(|_| Error::InvalidConfig {
824                    message: "Invalid x-goog-api-client header value".into(),
825                })
826        })
827        .collect::<Result<Vec<_>>>()?;
828    let existing = existing_values
829        .into_iter()
830        .filter(|value| !value.is_empty())
831        .collect::<Vec<_>>()
832        .join(" ");
833    let combined = if existing.contains(SDK_USAGE_HEADER_VALUE) {
834        existing
835    } else if existing.is_empty() {
836        SDK_USAGE_HEADER_VALUE.to_string()
837    } else {
838        format!("{SDK_USAGE_HEADER_VALUE} {existing}")
839    };
840    let value = HeaderValue::from_str(&combined).map_err(|_| Error::InvalidConfig {
841        message: "Invalid x-goog-api-client header value".into(),
842    })?;
843    headers.insert(header_name, value);
844    Ok(())
845}
846
847fn first_nonempty_env(names: &[&str]) -> Option<String> {
848    names.iter().find_map(|name| {
849        std::env::var(name)
850            .ok()
851            .map(|value| value.trim().to_string())
852            .filter(|value| !value.is_empty())
853    })
854}
855
856fn env_flag(name: &str) -> Option<bool> {
857    let value = std::env::var(name).ok()?;
858    match value.trim().to_ascii_lowercase().as_str() {
859        "1" | "true" | "yes" | "on" => Some(true),
860        "0" | "false" | "no" | "off" => Some(false),
861        _ => None,
862    }
863}
864
865fn attach_retry_metadata(response: &mut reqwest::Response, attempts: u32, retryable: bool) {
866    response.extensions_mut().insert(RetryMetadata {
867        attempts,
868        retryable,
869    });
870}
871
872fn attach_retry_metadata_for_codes(
873    response: &mut reqwest::Response,
874    attempts: u32,
875    retryable_codes: &[u16],
876) {
877    let retryable = retryable_codes.contains(&response.status().as_u16());
878    attach_retry_metadata(response, attempts, retryable);
879}
880
881fn retry_after_delay_secs(headers: &HeaderMap) -> Option<f64> {
882    let retry_after = headers
883        .get(reqwest::header::RETRY_AFTER)
884        .and_then(|value| value.to_str().ok())
885        .map(str::trim)?;
886
887    retry_after
888        .parse::<f64>()
889        .ok()
890        .map(|delay| delay.max(0.0))
891        .or_else(|| {
892            httpdate::parse_http_date(retry_after).ok().map(|deadline| {
893                deadline
894                    .duration_since(SystemTime::now())
895                    .unwrap_or_default()
896                    .as_secs_f64()
897            })
898        })
899}
900
901fn bounded_retry_delay_secs(
902    options: &HttpRetryOptions,
903    retry_index: u32,
904    retry_after_secs: Option<f64>,
905) -> f64 {
906    let delay = retry_after_secs.unwrap_or_else(|| retry_delay_secs(options, retry_index));
907    let max_delay = options
908        .max_delay
909        .unwrap_or(DEFAULT_RETRY_MAX_DELAY_SECS)
910        .max(0.0);
911    delay.min(max_delay)
912}
913
914fn retry_delay_secs(options: &HttpRetryOptions, retry_index: u32) -> f64 {
915    let initial = options
916        .initial_delay
917        .unwrap_or(DEFAULT_RETRY_INITIAL_DELAY_SECS)
918        .max(0.0);
919    let max_delay = options
920        .max_delay
921        .unwrap_or(DEFAULT_RETRY_MAX_DELAY_SECS)
922        .max(0.0);
923    let exp_base = options.exp_base.unwrap_or(DEFAULT_RETRY_EXP_BASE).max(0.0);
924    let jitter = options.jitter.unwrap_or(DEFAULT_RETRY_JITTER).max(0.0);
925
926    let exp_delay = if exp_base == 0.0 {
927        0.0
928    } else {
929        initial * exp_base.powf(retry_index as f64)
930    };
931    let base_delay = if max_delay > 0.0 {
932        exp_delay.min(max_delay)
933    } else {
934        exp_delay
935    };
936
937    let jitter_delay = if jitter > 0.0 {
938        // Basic pseudo-random jitter without adding a new RNG dependency.
939        let nanos = SystemTime::now()
940            .duration_since(UNIX_EPOCH)
941            .unwrap_or_default()
942            .subsec_nanos() as f64;
943        let frac = (nanos / 1_000_000_000.0).clamp(0.0, 1.0);
944        frac * jitter
945    } else {
946        0.0
947    };
948
949    let delay = base_delay + jitter_delay;
950    if max_delay > 0.0 {
951        delay.min(max_delay)
952    } else {
953        delay
954    }
955}
956
957fn default_auth_scopes(backend: Backend) -> Vec<String> {
958    match backend {
959        Backend::VertexAi => vec!["https://www.googleapis.com/auth/cloud-platform".into()],
960        Backend::GeminiApi => vec![
961            "https://www.googleapis.com/auth/generative-language".into(),
962            "https://www.googleapis.com/auth/generative-language.retriever".into(),
963        ],
964    }
965}
966
967pub(crate) struct ApiClient {
968    pub base_url: String,
969    pub api_version: String,
970}
971
972impl ApiClient {
973    /// 创建 API 客户端配置。
974    pub fn new(config: &ClientConfig) -> Self {
975        let base_url = config.http_options.base_url.as_deref().map_or_else(
976            || match config.backend {
977                Backend::VertexAi => {
978                    let location = config
979                        .vertex_config
980                        .as_ref()
981                        .map_or("", |cfg| cfg.location.as_str());
982                    if location.is_empty() {
983                        "https://aiplatform.googleapis.com/".to_string()
984                    } else {
985                        format!("https://{location}-aiplatform.googleapis.com/")
986                    }
987                }
988                Backend::GeminiApi => "https://generativelanguage.googleapis.com/".to_string(),
989            },
990            normalize_base_url,
991        );
992
993        let api_version =
994            config
995                .http_options
996                .api_version
997                .clone()
998                .unwrap_or_else(|| match config.backend {
999                    Backend::VertexAi => "v1beta1".to_string(),
1000                    Backend::GeminiApi => "v1beta".to_string(),
1001                });
1002
1003        Self {
1004            base_url,
1005            api_version,
1006        }
1007    }
1008}
1009
1010fn normalize_base_url(base_url: &str) -> String {
1011    let mut value = base_url.trim().to_string();
1012    if !value.ends_with('/') {
1013        value.push('/');
1014    }
1015    value
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020    use super::*;
1021    use crate::test_support::with_env;
1022    use bytes::Bytes;
1023    use futures_util::stream;
1024    use std::path::PathBuf;
1025    use std::time::SystemTime;
1026    use tempfile::tempdir;
1027    use wiremock::matchers::{method, path};
1028    use wiremock::{Mock, MockServer, ResponseTemplate};
1029
1030    #[test]
1031    fn test_client_from_api_key() {
1032        let client = Client::new("test-api-key").unwrap();
1033        assert_eq!(client.inner.config.backend, Backend::GeminiApi);
1034    }
1035
1036    #[test]
1037    fn test_client_builder() {
1038        let client = Client::builder()
1039            .api_key("test-key")
1040            .timeout(30)
1041            .build()
1042            .unwrap();
1043        assert!(client.inner.config.api_key.is_some());
1044    }
1045
1046    #[test]
1047    fn test_vertex_ai_config() {
1048        let client = Client::new_vertex("my-project", "us-central1").unwrap();
1049        assert_eq!(client.inner.config.backend, Backend::VertexAi);
1050        assert_eq!(
1051            client.inner.api_client.base_url,
1052            "https://us-central1-aiplatform.googleapis.com/"
1053        );
1054    }
1055
1056    #[test]
1057    fn test_base_url_normalization() {
1058        let client = Client::builder()
1059            .api_key("test-key")
1060            .base_url("https://example.com")
1061            .build()
1062            .unwrap();
1063        assert_eq!(client.inner.api_client.base_url, "https://example.com/");
1064    }
1065
1066    #[test]
1067    fn test_from_env_reads_overrides() {
1068        with_env(
1069            &[
1070                ("GEMINI_API_KEY", Some("env-key")),
1071                ("GENAI_BASE_URL", Some("https://env.example.com")),
1072                ("GENAI_API_VERSION", Some("v99")),
1073                ("GOOGLE_GENAI_API_VERSION", None),
1074                ("GOOGLE_API_KEY", None),
1075            ],
1076            || {
1077                let client = Client::from_env().unwrap();
1078                assert_eq!(client.inner.api_client.base_url, "https://env.example.com/");
1079                assert_eq!(client.inner.api_client.api_version, "v99");
1080            },
1081        );
1082    }
1083
1084    #[test]
1085    fn test_from_env_ignores_gemini_base_url_for_vertex() {
1086        with_env(
1087            &[
1088                ("GEMINI_API_KEY", None),
1089                ("GOOGLE_API_KEY", None),
1090                ("GOOGLE_GENAI_USE_VERTEXAI", Some("true")),
1091                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1092                ("GOOGLE_CLOUD_LOCATION", Some("us-central1")),
1093                ("GEMINI_BASE_URL", Some("https://gemini-only.example.com")),
1094                ("GENAI_BASE_URL", None),
1095                ("GOOGLE_GENAI_BASE_URL", None),
1096            ],
1097            || {
1098                let client = Client::from_env().unwrap();
1099                assert_eq!(client.inner.config.backend, Backend::VertexAi);
1100                assert_eq!(
1101                    client.inner.api_client.base_url,
1102                    "https://us-central1-aiplatform.googleapis.com/"
1103                );
1104            },
1105        );
1106    }
1107
1108    #[test]
1109    fn test_from_env_ignores_empty_overrides() {
1110        with_env(
1111            &[
1112                ("GEMINI_API_KEY", Some("env-key")),
1113                ("GENAI_BASE_URL", Some("   ")),
1114                ("GENAI_API_VERSION", Some("")),
1115                ("GOOGLE_GENAI_API_VERSION", None),
1116                ("GOOGLE_API_KEY", None),
1117            ],
1118            || {
1119                let client = Client::from_env().unwrap();
1120                assert_eq!(
1121                    client.inner.api_client.base_url,
1122                    "https://generativelanguage.googleapis.com/"
1123                );
1124                assert_eq!(client.inner.api_client.api_version, "v1beta");
1125            },
1126        );
1127    }
1128
1129    #[test]
1130    fn test_from_env_missing_key_errors() {
1131        with_env(
1132            &[
1133                ("GEMINI_API_KEY", None),
1134                ("GOOGLE_API_KEY", None),
1135                ("GENAI_BASE_URL", None),
1136                ("GOOGLE_GENAI_USE_VERTEXAI", None),
1137                ("GOOGLE_CLOUD_PROJECT", None),
1138                ("GOOGLE_CLOUD_LOCATION", None),
1139            ],
1140            || {
1141                let result = Client::from_env();
1142                assert!(result.is_err());
1143            },
1144        );
1145    }
1146
1147    #[test]
1148    fn test_from_env_google_api_key_fallback() {
1149        with_env(
1150            &[
1151                ("GEMINI_API_KEY", None),
1152                ("GOOGLE_API_KEY", Some("google-key")),
1153                ("GOOGLE_GENAI_USE_VERTEXAI", None),
1154            ],
1155            || {
1156                let client = Client::from_env().unwrap();
1157                assert_eq!(client.inner.config.api_key.as_deref(), Some("google-key"));
1158            },
1159        );
1160    }
1161
1162    #[test]
1163    fn test_from_env_supports_official_vertex_envs() {
1164        with_env(
1165            &[
1166                ("GEMINI_API_KEY", Some("env-key")),
1167                ("GOOGLE_API_KEY", None),
1168                ("GOOGLE_GENAI_USE_VERTEXAI", Some("true")),
1169                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1170                ("GOOGLE_CLOUD_LOCATION", Some("us-central1")),
1171                ("GOOGLE_GENAI_API_VERSION", Some("v1")),
1172                ("GENAI_API_VERSION", Some("v1beta")),
1173            ],
1174            || {
1175                let client = Client::from_env().unwrap();
1176                assert_eq!(client.inner.config.backend, Backend::VertexAi);
1177                assert!(matches!(
1178                    client.inner.config.credentials,
1179                    Credentials::ApplicationDefault
1180                ));
1181                assert_eq!(client.inner.config.api_key, None);
1182                assert_eq!(
1183                    client.inner.api_client.base_url,
1184                    "https://us-central1-aiplatform.googleapis.com/"
1185                );
1186                assert_eq!(client.inner.api_client.api_version, "v1");
1187            },
1188        );
1189    }
1190
1191    #[test]
1192    fn test_from_env_uses_complete_vertex_env_without_flag_when_api_key_is_absent() {
1193        with_env(
1194            &[
1195                ("GEMINI_API_KEY", None),
1196                ("GOOGLE_API_KEY", None),
1197                ("GOOGLE_GENAI_USE_VERTEXAI", None),
1198                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1199                ("GOOGLE_CLOUD_LOCATION", Some("us-central1")),
1200            ],
1201            || {
1202                let client = Client::from_env().unwrap();
1203                assert_eq!(client.inner.config.backend, Backend::VertexAi);
1204                assert_eq!(client.inner.config.api_key, None);
1205                assert_eq!(
1206                    client.inner.api_client.base_url,
1207                    "https://us-central1-aiplatform.googleapis.com/"
1208                );
1209            },
1210        );
1211    }
1212
1213    #[test]
1214    fn test_from_env_prefers_gemini_when_api_key_and_complete_vertex_env_exist() {
1215        with_env(
1216            &[
1217                ("GEMINI_API_KEY", Some("env-key")),
1218                ("GOOGLE_API_KEY", None),
1219                ("GOOGLE_GENAI_USE_VERTEXAI", None),
1220                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1221                ("GOOGLE_CLOUD_LOCATION", Some("us-central1")),
1222            ],
1223            || {
1224                let client = Client::from_env().unwrap();
1225                assert_eq!(client.inner.config.backend, Backend::GeminiApi);
1226                assert_eq!(client.inner.config.api_key.as_deref(), Some("env-key"));
1227                assert_eq!(
1228                    client.inner.api_client.base_url,
1229                    "https://generativelanguage.googleapis.com/"
1230                );
1231            },
1232        );
1233    }
1234
1235    #[test]
1236    fn test_from_env_explicit_false_prefers_gemini_even_with_complete_vertex_env() {
1237        with_env(
1238            &[
1239                ("GEMINI_API_KEY", Some("env-key")),
1240                ("GOOGLE_API_KEY", None),
1241                ("GOOGLE_GENAI_USE_VERTEXAI", Some("false")),
1242                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1243                ("GOOGLE_CLOUD_LOCATION", Some("us-central1")),
1244            ],
1245            || {
1246                let client = Client::from_env().unwrap();
1247                assert_eq!(client.inner.config.backend, Backend::GeminiApi);
1248                assert_eq!(client.inner.config.api_key.as_deref(), Some("env-key"));
1249                assert_eq!(
1250                    client.inner.api_client.base_url,
1251                    "https://generativelanguage.googleapis.com/"
1252                );
1253            },
1254        );
1255    }
1256
1257    #[test]
1258    fn test_from_env_prefers_gemini_when_vertex_env_is_partial() {
1259        with_env(
1260            &[
1261                ("GEMINI_API_KEY", Some("env-key")),
1262                ("GOOGLE_API_KEY", None),
1263                ("GOOGLE_GENAI_USE_VERTEXAI", None),
1264                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1265                ("GOOGLE_CLOUD_LOCATION", None),
1266            ],
1267            || {
1268                let client = Client::from_env().unwrap();
1269                assert_eq!(client.inner.config.backend, Backend::GeminiApi);
1270                assert_eq!(client.inner.config.api_key.as_deref(), Some("env-key"));
1271                assert_eq!(
1272                    client.inner.api_client.base_url,
1273                    "https://generativelanguage.googleapis.com/"
1274                );
1275            },
1276        );
1277    }
1278
1279    #[test]
1280    fn test_from_env_vertex_requires_project_and_location() {
1281        with_env(
1282            &[
1283                ("GOOGLE_GENAI_USE_VERTEXAI", Some("true")),
1284                ("GOOGLE_CLOUD_PROJECT", Some("vertex-project")),
1285                ("GOOGLE_CLOUD_LOCATION", None),
1286                ("GEMINI_API_KEY", None),
1287                ("GOOGLE_API_KEY", None),
1288            ],
1289            || {
1290                let result = Client::from_env();
1291                assert!(matches!(result, Err(Error::InvalidConfig { .. })));
1292            },
1293        );
1294    }
1295
1296    #[test]
1297    fn test_bounded_retry_delay_secs_prefers_retry_after_with_cap() {
1298        let options = HttpRetryOptions {
1299            max_delay: Some(2.0),
1300            ..Default::default()
1301        };
1302
1303        let delay = bounded_retry_delay_secs(&options, 0, Some(120.0));
1304        assert_eq!(delay, 2.0);
1305    }
1306
1307    #[test]
1308    fn test_bounded_retry_delay_secs_uses_retry_after_when_below_cap() {
1309        let options = HttpRetryOptions {
1310            max_delay: Some(5.0),
1311            ..Default::default()
1312        };
1313
1314        let delay = bounded_retry_delay_secs(&options, 0, Some(1.5));
1315        assert_eq!(delay, 1.5);
1316    }
1317
1318    #[test]
1319    fn test_bounded_retry_delay_secs_caps_retry_after_at_zero() {
1320        let options = HttpRetryOptions {
1321            max_delay: Some(0.0),
1322            ..Default::default()
1323        };
1324
1325        let delay = bounded_retry_delay_secs(&options, 0, Some(120.0));
1326        assert_eq!(delay, 0.0);
1327    }
1328
1329    #[test]
1330    fn test_bounded_retry_delay_secs_falls_back_to_backoff() {
1331        let options = HttpRetryOptions {
1332            initial_delay: Some(1.0),
1333            max_delay: Some(10.0),
1334            exp_base: Some(2.0),
1335            jitter: Some(0.0),
1336            ..Default::default()
1337        };
1338
1339        let delay = bounded_retry_delay_secs(&options, 2, None);
1340        assert_eq!(delay, 4.0);
1341    }
1342
1343    #[test]
1344    fn test_retry_after_delay_secs_parses_http_date() {
1345        let deadline = SystemTime::now() + Duration::from_secs(120);
1346        let mut headers = HeaderMap::new();
1347        headers.insert(
1348            reqwest::header::RETRY_AFTER,
1349            HeaderValue::from_str(&httpdate::fmt_http_date(deadline)).unwrap(),
1350        );
1351
1352        let delay = retry_after_delay_secs(&headers).unwrap();
1353        assert!((110.0..=120.0).contains(&delay));
1354    }
1355
1356    #[tokio::test]
1357    async fn test_send_with_http_options_preserves_custom_retry_metadata_without_retries() {
1358        let server = MockServer::start().await;
1359        Mock::given(method("POST"))
1360            .and(path("/retry-once"))
1361            .respond_with(ResponseTemplate::new(409).set_body_string("conflict"))
1362            .mount(&server)
1363            .await;
1364
1365        let client = Client::new("test-key").unwrap();
1366        let request = client
1367            .inner
1368            .http
1369            .post(format!("{}/retry-once", server.uri()))
1370            .body(reqwest::Body::wrap_stream(stream::once(async {
1371                Ok::<Bytes, std::io::Error>(Bytes::from_static(b"payload"))
1372            })));
1373        let http_options = rust_genai_types::http::HttpOptions {
1374            retry_options: Some(HttpRetryOptions {
1375                attempts: Some(2),
1376                http_status_codes: Some(vec![409]),
1377                initial_delay: Some(0.0),
1378                max_delay: Some(0.0),
1379                exp_base: Some(0.0),
1380                jitter: Some(0.0),
1381            }),
1382            ..Default::default()
1383        };
1384
1385        let response = client
1386            .inner
1387            .send_with_http_options(request, Some(&http_options))
1388            .await
1389            .unwrap();
1390        let retry_metadata = response
1391            .extensions()
1392            .get::<RetryMetadata>()
1393            .copied()
1394            .unwrap();
1395
1396        assert_eq!(response.status().as_u16(), 409);
1397        assert_eq!(retry_metadata.attempts, 1);
1398        assert!(retry_metadata.retryable);
1399    }
1400
1401    #[test]
1402    fn test_with_oauth_missing_client_secret_errors() {
1403        let dir = tempdir().unwrap();
1404        let secret_path = dir.path().join("missing_client_secret.json");
1405        let err = Client::with_oauth(&secret_path).err().unwrap();
1406        assert!(matches!(err, Error::InvalidConfig { .. }));
1407    }
1408
1409    #[test]
1410    fn test_with_adc_builds_client() {
1411        let client = Client::with_adc().unwrap();
1412        assert!(matches!(
1413            client.inner.config.credentials,
1414            Credentials::ApplicationDefault
1415        ));
1416    }
1417
1418    #[test]
1419    fn test_builder_defaults_to_vertex_when_project_set() {
1420        let client = Client::builder()
1421            .vertex_project("proj")
1422            .vertex_location("loc")
1423            .build()
1424            .unwrap();
1425        assert_eq!(client.inner.config.backend, Backend::VertexAi);
1426        assert!(matches!(
1427            client.inner.config.credentials,
1428            Credentials::ApplicationDefault
1429        ));
1430    }
1431
1432    #[test]
1433    fn test_valid_proxy_is_accepted() {
1434        let client = Client::builder()
1435            .api_key("test-key")
1436            .proxy("http://127.0.0.1:8888")
1437            .build();
1438        assert!(client.is_ok());
1439    }
1440
1441    #[test]
1442    fn test_vertex_requires_project_and_location() {
1443        let result = Client::builder().backend(Backend::VertexAi).build();
1444        assert!(result.is_err());
1445    }
1446
1447    #[test]
1448    fn test_api_key_with_oauth_is_invalid() {
1449        let result = Client::builder()
1450            .api_key("test-key")
1451            .credentials(Credentials::OAuth {
1452                client_secret_path: PathBuf::from("client_secret.json"),
1453                token_cache_path: None,
1454            })
1455            .build();
1456        assert!(result.is_err());
1457    }
1458
1459    #[test]
1460    fn test_missing_api_key_for_gemini_errors() {
1461        let result = Client::builder().backend(Backend::GeminiApi).build();
1462        assert!(result.is_err());
1463    }
1464
1465    #[test]
1466    fn test_invalid_header_name_is_rejected() {
1467        let result = Client::builder()
1468            .api_key("test-key")
1469            .header("bad header", "value")
1470            .build();
1471        assert!(result.is_err());
1472    }
1473
1474    #[test]
1475    fn test_invalid_header_value_is_rejected() {
1476        let result = Client::builder()
1477            .api_key("test-key")
1478            .header("x-test", "bad\nvalue")
1479            .build();
1480        assert!(result.is_err());
1481    }
1482
1483    #[test]
1484    fn test_invalid_api_key_value_is_rejected() {
1485        let err = Client::builder().api_key("bad\nkey").build().err().unwrap();
1486        assert!(
1487            matches!(err, Error::InvalidConfig { message } if message.contains("Invalid API key value"))
1488        );
1489    }
1490
1491    #[test]
1492    fn test_invalid_proxy_is_rejected() {
1493        let result = Client::builder()
1494            .api_key("test-key")
1495            .proxy("not a url")
1496            .build();
1497        assert!(result.is_err());
1498    }
1499
1500    #[test]
1501    fn test_vertex_api_key_is_rejected() {
1502        let result = Client::builder()
1503            .backend(Backend::VertexAi)
1504            .vertex_project("proj")
1505            .vertex_location("loc")
1506            .credentials(Credentials::ApiKey("key".into()))
1507            .build();
1508        assert!(result.is_err());
1509    }
1510
1511    #[test]
1512    fn test_default_auth_scopes() {
1513        let gemini = default_auth_scopes(Backend::GeminiApi);
1514        assert!(gemini.iter().any(|s| s.contains("generative-language")));
1515
1516        let vertex = default_auth_scopes(Backend::VertexAi);
1517        assert!(vertex.iter().any(|s| s.contains("cloud-platform")));
1518    }
1519
1520    #[test]
1521    fn test_custom_auth_scopes_override_default() {
1522        let client = Client::builder()
1523            .api_key("test-key")
1524            .auth_scopes(vec!["scope-1".to_string()])
1525            .build()
1526            .unwrap();
1527        assert_eq!(client.inner.config.auth_scopes, vec!["scope-1".to_string()]);
1528    }
1529
1530    #[test]
1531    fn test_append_sdk_usage_header() {
1532        let mut headers = HeaderMap::new();
1533        append_sdk_usage_header(&mut headers).unwrap();
1534        assert_eq!(
1535            headers
1536                .get(X_GOOG_API_CLIENT_HEADER)
1537                .and_then(|value| value.to_str().ok()),
1538            Some(SDK_USAGE_HEADER_VALUE)
1539        );
1540    }
1541
1542    #[test]
1543    fn test_append_sdk_usage_header_preserves_existing_value() {
1544        let mut headers = HeaderMap::new();
1545        headers.insert(
1546            HeaderName::from_static(X_GOOG_API_CLIENT_HEADER),
1547            HeaderValue::from_static("custom-client/1.0.0"),
1548        );
1549        append_sdk_usage_header(&mut headers).unwrap();
1550        append_sdk_usage_header(&mut headers).unwrap();
1551        assert_eq!(
1552            headers
1553                .get(X_GOOG_API_CLIENT_HEADER)
1554                .and_then(|value| value.to_str().ok()),
1555            Some(concat!(
1556                "google-genai-sdk/",
1557                env!("CARGO_PKG_VERSION"),
1558                " gl-rust/unknown custom-client/1.0.0"
1559            ))
1560        );
1561    }
1562}