1use 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
21pub const USER_AGENT: &str = concat!("ferrin-google/", env!("CARGO_PKG_VERSION"));
23
24pub const API_KEY_ENV: &str = "GOOGLE_GENERATIVE_AI_API_KEY";
26
27pub const DEFAULT_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta";
29
30pub const API_KEY_HEADER: &str = "x-goog-api-key";
32
33pub const CANONICAL_OPTIONS_KEY: &str = "google";
36
37pub const DEFAULT_NAME: &str = "google";
39
40pub const UPLOAD_PATH: &str = "/upload/v1beta/files";
42
43pub const DOWNLOAD_PATH_PREFIX: &str = "/download/v1beta/";
45
46pub const AUTH_TOKENS_PATH: &str = "/v1alpha/auth_tokens";
48
49pub struct GoogleConfig {
54 pub name: String,
56 pub base_url: Url,
59 pub api_key: Option<SecretString>,
61 pub headers: Headers,
63 pub url_policy: UrlPolicy,
65 pub transport: SharedTransport,
67 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 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 #[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 #[must_use]
114 pub fn provider_id(&self, family: &str) -> ProviderId {
115 ProviderId::new(format!("{}.{family}", self.name))
116 }
117
118 #[must_use]
121 pub fn options_key(&self) -> &str {
122 &self.name
123 }
124
125 #[must_use]
127 pub fn url(&self, path: &str) -> Url {
128 join_path(&self.base_url, path)
129 }
130
131 #[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 #[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 #[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 #[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 let _ = url.set_scheme(scheme);
186 url
187 }
188
189 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 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 #[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 #[must_use]
239 pub fn generate_id(&self) -> String {
240 self.id_generator.generate()
241 }
242}
243
244pub type SharedConfig = Arc<GoogleConfig>;