Skip to main content

ferrin_google/
config.rs

1//! Shared configuration of every Google model and service.
2
3use std::fmt;
4use std::sync::Arc;
5
6use ferrin_provider_util::IdGenerator;
7use ferrin_provider_util::PrefixedIdGenerator;
8use ferrin_provider_util::SharedTransport;
9use ferrin_provider_util::base_url::join_path;
10use ferrin_provider_util::secure_url::UrlPolicy;
11use ferrin_provider_util::settings::ApiKeyConfig;
12use ferrin_provider_util::settings::load_api_key;
13use ferrin_spec::Headers;
14use ferrin_spec::ProviderId;
15use ferrin_spec::error::InvalidArgumentError;
16use ferrin_spec::error::ProviderError;
17use secrecy::ExposeSecret;
18use secrecy::SecretString;
19use url::Url;
20
21/// User-agent suffix appended to every request.
22pub const USER_AGENT: &str = concat!("ferrin-google/", env!("CARGO_PKG_VERSION"));
23
24/// Environment variable read for the API key when none is configured.
25pub const API_KEY_ENV: &str = "GOOGLE_GENERATIVE_AI_API_KEY";
26
27/// Default base URL (Gemini API, `v1beta`).
28pub const DEFAULT_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta";
29
30/// Header carrying the API key.
31pub const API_KEY_HEADER: &str = "x-goog-api-key";
32
33/// Canonical provider options and metadata key, always consulted in addition
34/// to the configured name.
35pub const CANONICAL_OPTIONS_KEY: &str = "google";
36
37/// Default provider name.
38pub const DEFAULT_NAME: &str = "google";
39
40/// Path of the resumable upload endpoint (relative to the origin).
41pub const UPLOAD_PATH: &str = "/upload/v1beta/files";
42
43/// Path prefix of the media download endpoint (relative to the origin).
44pub const DOWNLOAD_PATH_PREFIX: &str = "/download/v1beta/";
45
46/// Path of the ephemeral auth token endpoint (relative to the origin).
47pub const AUTH_TOKENS_PATH: &str = "/v1alpha/auth_tokens";
48
49/// Configuration shared by the models and services of one provider instance.
50///
51/// Built by [`crate::create_google`]; exposed so that compatible endpoints
52/// can reuse the model types with their own settings.
53pub struct GoogleConfig {
54    /// Provider name used as the prefix of every provider id (`google`).
55    pub name: String,
56    /// Base URL without trailing slash
57    /// (`https://generativelanguage.googleapis.com/v1beta`).
58    pub base_url: Url,
59    /// API key; loaded lazily from `GOOGLE_GENERATIVE_AI_API_KEY` when `None`.
60    pub api_key: Option<SecretString>,
61    /// Extra headers sent with every request.
62    pub headers: Headers,
63    /// Security policy for server-provided URLs (HTTPS and public networks by default).
64    pub url_policy: UrlPolicy,
65    /// HTTP transport.
66    pub transport: SharedTransport,
67    /// Generator for synthetic ids (tool calls without an id, sources).
68    pub id_generator: Arc<dyn IdGenerator>,
69}
70
71impl fmt::Debug for GoogleConfig {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        f.debug_struct("GoogleConfig")
74            .field("name", &self.name)
75            .field("base_url", &self.base_url)
76            .field("api_key", &self.api_key.as_ref().map(|_| "***"))
77            .field("headers", &self.headers)
78            .finish_non_exhaustive()
79    }
80}
81
82impl GoogleConfig {
83    /// Creates a configuration with the default transport and id generator.
84    ///
85    /// # Errors
86    ///
87    /// Returns [`ProviderError::Other`] when the default HTTP transport cannot
88    /// be built.
89    pub fn new(name: impl Into<String>, base_url: Url) -> Result<Self, ProviderError> {
90        let transport = ferrin_provider_util::default_transport().map_err(ProviderError::other)?;
91        Ok(Self::with_transport(name, base_url, transport))
92    }
93
94    /// Creates a configuration with an explicit transport.
95    #[must_use]
96    pub fn with_transport(
97        name: impl Into<String>,
98        base_url: Url,
99        transport: SharedTransport,
100    ) -> Self {
101        Self {
102            name: name.into(),
103            base_url,
104            api_key: None,
105            headers: Headers::new(),
106            url_policy: UrlPolicy::default(),
107            transport,
108            id_generator: Arc::new(PrefixedIdGenerator::default()),
109        }
110    }
111
112    /// Provider id of an API family (`<name>.<family>`).
113    #[must_use]
114    pub fn provider_id(&self, family: &str) -> ProviderId {
115        ProviderId::new(format!("{}.{family}", self.name))
116    }
117
118    /// Key under which provider options addressed to this instance are read
119    /// in addition to [`CANONICAL_OPTIONS_KEY`]: the configured name.
120    #[must_use]
121    pub fn options_key(&self) -> &str {
122        &self.name
123    }
124
125    /// Full URL of an API path below the base URL (`/models/x:generateContent`).
126    #[must_use]
127    pub fn url(&self, path: &str) -> Url {
128        join_path(&self.base_url, path)
129    }
130
131    /// Resource path of a model: ids containing `/` are used as-is, others
132    /// are prefixed with `models/`.
133    #[must_use]
134    pub fn model_path(model_id: &str) -> String {
135        if model_id.contains('/') {
136            model_id.to_owned()
137        } else {
138            format!("models/{model_id}")
139        }
140    }
141
142    /// URL of a model action (`{base}/{model path}:{action}`).
143    #[must_use]
144    pub fn model_url(&self, model_id: &str, action: &str) -> Url {
145        self.url(&format!("{}:{action}", Self::model_path(model_id)))
146    }
147
148    /// URL of an absolute path on the base URL's origin (used by the upload,
149    /// download and auth token endpoints, which are not nested under the API
150    /// version).
151    #[must_use]
152    pub fn origin_url(&self, path: &str) -> Url {
153        let mut url = self.base_url.clone();
154        url.set_path(path);
155        url.set_query(None);
156        url.set_fragment(None);
157        url
158    }
159
160    /// WebSocket URL of a Live API service: the trailing `v1beta`/`v1alpha`
161    /// segment of the base URL is removed, the scheme becomes `wss`/`ws` and
162    /// `/ws/<service path>` is appended.
163    #[must_use]
164    pub fn websocket_url(&self, service_path: &str) -> Url {
165        let mut url = self.base_url.clone();
166        let mut segments: Vec<&str> = url
167            .path()
168            .split('/')
169            .filter(|segment| !segment.is_empty())
170            .collect();
171        if matches!(segments.last(), Some(&"v1beta" | &"v1alpha")) {
172            segments.pop();
173        }
174        let mut path = segments.join("/");
175        if !path.is_empty() {
176            path.insert(0, '/');
177        }
178        path.push_str("/ws/");
179        path.push_str(service_path);
180        url.set_path(&path);
181        url.set_query(None);
182        url.set_fragment(None);
183        let scheme = if url.scheme() == "http" { "ws" } else { "wss" };
184        // Changing `http(s)` to `ws(s)` is always accepted by the URL parser.
185        let _ = url.set_scheme(scheme);
186        url
187    }
188
189    /// Resolves the API key from the configuration or the environment.
190    ///
191    /// # Errors
192    ///
193    /// Returns [`ProviderError::LoadApiKey`] when neither is set.
194    pub fn api_key(&self) -> Result<SecretString, ProviderError> {
195        Ok(load_api_key(ApiKeyConfig {
196            api_key: self.api_key.clone(),
197            environment_variable: API_KEY_ENV,
198            parameter_name: "api_key",
199            description: "Google Generative AI",
200        })?)
201    }
202
203    /// Request headers: API key, configured headers, per-call headers and
204    /// the user-agent suffix.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`ProviderError::LoadApiKey`] when no API key is available and
209    /// [`ProviderError::InvalidArgument`] when the key is not a valid header
210    /// value.
211    pub fn headers(&self, call_headers: &Headers) -> Result<Headers, ProviderError> {
212        let mut headers = Headers::new();
213        let key = self.api_key()?;
214        headers
215            .insert(API_KEY_HEADER, key.expose_secret())
216            .map_err(|_| {
217                ProviderError::InvalidArgument(InvalidArgumentError::new(
218                    "api_key",
219                    "api_key is not a valid header value",
220                ))
221            })?;
222        headers.merge(&self.headers);
223        headers.merge(call_headers);
224        Ok(headers.with_user_agent_suffix([USER_AGENT]))
225    }
226
227    /// Headers of requests that must not carry the API key (resumable upload
228    /// sessions, ephemeral token creation): configured headers, `call_headers`
229    /// and the user-agent suffix.
230    #[must_use]
231    pub fn unauthenticated_headers(&self, call_headers: &Headers) -> Headers {
232        let mut headers = self.headers.clone();
233        headers.merge(call_headers);
234        headers.with_user_agent_suffix([USER_AGENT])
235    }
236
237    /// Generates a synthetic id.
238    #[must_use]
239    pub fn generate_id(&self) -> String {
240        self.id_generator.generate()
241    }
242}
243
244/// Shared handle to the configuration.
245pub type SharedConfig = Arc<GoogleConfig>;