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::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
20pub const USER_AGENT: &str = concat!("ferrin-google/", env!("CARGO_PKG_VERSION"));
22
23pub const API_KEY_ENV: &str = "GOOGLE_GENERATIVE_AI_API_KEY";
25
26pub const DEFAULT_BASE_URL: &str = "https://generativelanguage.googleapis.com/v1beta";
28
29pub const API_KEY_HEADER: &str = "x-goog-api-key";
31
32pub const CANONICAL_OPTIONS_KEY: &str = "google";
35
36pub const DEFAULT_NAME: &str = "google";
38
39pub const UPLOAD_PATH: &str = "/upload/v1beta/files";
41
42pub const DOWNLOAD_PATH_PREFIX: &str = "/download/v1beta/";
44
45pub const AUTH_TOKENS_PATH: &str = "/v1alpha/auth_tokens";
47
48pub struct GoogleConfig {
53 pub name: String,
55 pub base_url: Url,
58 pub api_key: Option<SecretString>,
60 pub headers: Headers,
62 pub transport: SharedTransport,
64 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 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 #[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 #[must_use]
110 pub fn provider_id(&self, family: &str) -> ProviderId {
111 ProviderId::new(format!("{}.{family}", self.name))
112 }
113
114 #[must_use]
117 pub fn options_key(&self) -> &str {
118 &self.name
119 }
120
121 #[must_use]
123 pub fn url(&self, path: &str) -> Url {
124 join_path(&self.base_url, path)
125 }
126
127 #[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 #[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 #[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 #[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 let _ = url.set_scheme(scheme);
182 url
183 }
184
185 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 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 #[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 #[must_use]
235 pub fn generate_id(&self) -> String {
236 self.id_generator.generate()
237 }
238}
239
240pub type SharedConfig = Arc<GoogleConfig>;