1#[cfg(feature = "http")]
8pub mod http;
9
10use std::sync::Arc;
11
12use crate::protocol::types::{ApiEndpoint, ModelId, SessionConfig};
13use crate::transport::ConnectBuilder;
14use crate::transport::auth::{
15 AuthProvider, GoogleAIAuth, GoogleAITokenAuth, RestAuth, ServiceEndpoint, VertexAIAuth,
16};
17
18pub struct Client {
42 endpoint: ApiEndpoint,
43 model: ModelId,
44 auth: Arc<dyn RestAuth>,
45 base_url: Option<String>,
47 #[cfg(feature = "http")]
48 http: http::HttpClient,
49}
50
51impl Client {
52 pub fn from_api_key(api_key: impl Into<String>) -> Self {
54 let key: String = api_key.into();
55 let endpoint = ApiEndpoint::google_ai(key.clone());
56 let auth: Arc<dyn RestAuth> = Arc::new(GoogleAIAuth::new(key));
57 Self {
58 endpoint,
59 model: ModelId::FLASH_LATEST,
60 auth,
61 base_url: None,
62 #[cfg(feature = "http")]
63 http: http::HttpClient::new(http::HttpConfig::default()),
64 }
65 }
66
67 pub fn from_access_token(access_token: impl Into<String>) -> Self {
69 let token: String = access_token.into();
70 let endpoint = ApiEndpoint::google_ai_token(token.clone());
71 let auth: Arc<dyn RestAuth> = Arc::new(GoogleAITokenAuth::new(token));
72 Self {
73 endpoint,
74 model: ModelId::FLASH_LATEST,
75 auth,
76 base_url: None,
77 #[cfg(feature = "http")]
78 http: http::HttpClient::new(http::HttpConfig::default()),
79 }
80 }
81
82 pub fn from_vertex(
84 project: impl Into<String>,
85 location: impl Into<String>,
86 access_token: impl Into<String>,
87 ) -> Self {
88 let proj: String = project.into();
89 let loc: String = location.into();
90 let tok: String = access_token.into();
91 let endpoint = ApiEndpoint::vertex(proj.clone(), loc.clone(), tok.clone());
92 let auth: Arc<dyn RestAuth> = Arc::new(VertexAIAuth::new(proj, loc, tok));
93 Self {
94 endpoint,
95 model: ModelId::FLASH_LATEST,
96 auth,
97 base_url: None,
98 #[cfg(feature = "http")]
99 http: http::HttpClient::new(http::HttpConfig::default()),
100 }
101 }
102
103 pub fn from_vertex_refreshable(
113 project: impl Into<String>,
114 location: impl Into<String>,
115 refresher: impl Fn() -> String + Send + Sync + 'static,
116 ) -> Self {
117 let proj: String = project.into();
118 let loc: String = location.into();
119 let refresher = Arc::new(refresher);
122 let live_refresher = refresher.clone();
123 let endpoint =
124 ApiEndpoint::vertex_refreshing(proj.clone(), loc.clone(), move || live_refresher());
125 let auth: Arc<dyn RestAuth> =
126 Arc::new(VertexAIAuth::with_token_refresher(proj, loc, move || {
127 refresher()
128 }));
129 Self {
130 endpoint,
131 model: ModelId::FLASH_LATEST,
132 auth,
133 base_url: None,
134 #[cfg(feature = "http")]
135 http: http::HttpClient::new(http::HttpConfig::default()),
136 }
137 }
138
139 pub fn model(mut self, model: impl Into<ModelId>) -> Self {
141 self.model = model.into();
142 self
143 }
144
145 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
160 self.base_url = Some(base_url.into().trim_end_matches('/').to_string());
161 self
162 }
163
164 #[cfg(feature = "http")]
166 pub fn http_config(mut self, config: http::HttpConfig) -> Self {
167 self.http = http::HttpClient::new(config);
168 self
169 }
170
171 pub fn auth(&self) -> &dyn AuthProvider {
173 &*self.auth
174 }
175
176 pub fn default_model(&self) -> &ModelId {
178 &self.model
179 }
180
181 pub fn rest_url(&self, endpoint: ServiceEndpoint) -> String {
183 self.rebase(self.auth.rest_url(endpoint, Some(&self.model)))
184 }
185
186 pub fn rest_url_for(&self, endpoint: ServiceEndpoint, model: &ModelId) -> String {
188 self.rebase(self.auth.rest_url(endpoint, Some(model)))
189 }
190
191 fn rebase(&self, url: String) -> String {
193 let Some(base) = &self.base_url else {
194 return url;
195 };
196 let path = url
198 .split_once("://")
199 .and_then(|(_, rest)| rest.find('/').map(|i| &rest[i..]))
200 .unwrap_or("");
201 format!("{base}{path}")
202 }
203
204 pub async fn auth_headers(&self) -> Result<Vec<(String, String)>, crate::session::AuthError> {
206 self.auth.auth_headers().await
207 }
208
209 pub fn live(&self, model: Option<ModelId>) -> ConnectBuilder {
215 let mut config = SessionConfig::from_endpoint(self.endpoint.clone());
216 config.model = model;
217 ConnectBuilder::new(config)
218 }
219
220 #[cfg(feature = "http")]
222 pub fn http_client(&self) -> &http::HttpClient {
223 &self.http
224 }
225
226 #[cfg(feature = "http")]
230 pub async fn rest_request(
231 &self,
232 endpoint: ServiceEndpoint,
233 body: &impl serde::Serialize,
234 ) -> Result<serde_json::Value, http::HttpError> {
235 let url = self.rest_url(endpoint);
236 let headers = self.auth.auth_headers().await?;
237 self.http.post_json(&url, headers, body).await
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn client_from_api_key() {
247 let client = Client::from_api_key("test-key");
248 assert_eq!(client.default_model(), &ModelId::FLASH_LATEST);
251 }
252
253 #[test]
254 fn client_from_vertex() {
255 let client = Client::from_vertex("proj", "us-central1", "tok");
256 let url = client.auth().ws_url(&ModelId::FLASH_LATEST);
257 assert!(url.contains("us-central1-aiplatform.googleapis.com"));
258 }
259
260 #[test]
261 fn client_model_override() {
262 let client = Client::from_api_key("key").model("models/gemini-2.0-flash-live-001");
263 assert_eq!(
264 client.default_model(),
265 &ModelId::from_static("models/gemini-2.0-flash-live-001")
266 );
267 }
268
269 #[test]
270 fn client_rest_url_generate() {
271 let client = Client::from_api_key("my-key")
272 .model(ModelId::from_static("models/gemini-2.0-flash-live-001"));
273 let url = client.rest_url(ServiceEndpoint::GenerateContent);
274 assert!(url.contains(":generateContent"));
275 assert!(!url.contains("my-key"), "the key rides in a header: {url}");
276 }
277
278 #[test]
279 fn client_rest_url_vertex() {
280 let client = Client::from_vertex("proj", "us-east1", "tok")
281 .model(ModelId::from_static("models/gemini-2.0-flash-live-001"));
282 let url = client.rest_url(ServiceEndpoint::GenerateContent);
283 assert!(url.contains("us-east1-aiplatform.googleapis.com"));
284 assert!(url.contains(":generateContent"));
285 }
286
287 #[test]
288 fn base_url_replaces_only_the_host() {
289 let model = ModelId::from_static("gemini-3.5-flash-lite");
290 let google = Client::from_api_key("my-key").base_url("http://127.0.0.1:8766/");
291 assert_eq!(
292 google.rest_url_for(ServiceEndpoint::GenerateContent, &model),
293 "http://127.0.0.1:8766/v1beta/models/gemini-3.5-flash-lite:generateContent"
294 );
295 let vertex = Client::from_vertex("proj", "us-east1", "tok").base_url("https://gw.example");
296 let url = vertex.rest_url_for(ServiceEndpoint::GenerateContent, &model);
297 assert!(
298 url.starts_with("https://gw.example/v1beta1/projects/proj/locations/us-east1/"),
299 "{url}"
300 );
301 let plain =
303 Client::from_api_key("k").rest_url_for(ServiceEndpoint::GenerateContent, &model);
304 assert!(plain.starts_with("https://generativelanguage.googleapis.com/v1beta/"));
305 }
306
307 #[test]
308 fn live_session_builder_created() {
309 let client = Client::from_api_key("key");
310 let _builder = client.live(Some(ModelId::from_static(
311 "models/gemini-2.0-flash-live-001",
312 )));
313 }
314
315 #[tokio::test]
316 async fn client_from_vertex_refreshable() {
317 use std::sync::atomic::{AtomicU32, Ordering};
318 let call_count = Arc::new(AtomicU32::new(0));
319 let cc = call_count.clone();
320 let client = Client::from_vertex_refreshable("proj", "us-central1", move || {
321 cc.fetch_add(1, Ordering::SeqCst);
322 "refreshed-token".to_string()
323 });
324 assert_eq!(call_count.load(Ordering::SeqCst), 0);
327 let headers = client.auth_headers().await.unwrap();
329 assert_eq!(headers[0].1, "Bearer refreshed-token");
330 assert_eq!(call_count.load(Ordering::SeqCst), 1);
331 let live_config = SessionConfig::from_endpoint(client.endpoint.clone());
333 assert_eq!(
334 live_config.bearer_token().as_deref(),
335 Some("refreshed-token")
336 );
337 assert_eq!(call_count.load(Ordering::SeqCst), 2);
338 }
339}