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