gemini_genai_rs/client/
mod.rs1#[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 #[cfg(feature = "http")]
46 http: http::HttpClient,
47}
48
49impl Client {
50 pub fn from_api_key(api_key: impl Into<String>) -> Self {
52 let key: String = api_key.into();
53 let endpoint = ApiEndpoint::google_ai(key.clone());
54 let auth: Arc<dyn RestAuth> = Arc::new(GoogleAIAuth::new(key));
55 Self {
56 endpoint,
57 model: ModelId::FLASH_LATEST,
58 auth,
59 #[cfg(feature = "http")]
60 http: http::HttpClient::new(http::HttpConfig::default()),
61 }
62 }
63
64 pub fn from_access_token(access_token: impl Into<String>) -> Self {
66 let token: String = access_token.into();
67 let endpoint = ApiEndpoint::google_ai_token(token.clone());
68 let auth: Arc<dyn RestAuth> = Arc::new(GoogleAITokenAuth::new(token));
69 Self {
70 endpoint,
71 model: ModelId::FLASH_LATEST,
72 auth,
73 #[cfg(feature = "http")]
74 http: http::HttpClient::new(http::HttpConfig::default()),
75 }
76 }
77
78 pub fn from_vertex(
80 project: impl Into<String>,
81 location: impl Into<String>,
82 access_token: impl Into<String>,
83 ) -> Self {
84 let proj: String = project.into();
85 let loc: String = location.into();
86 let tok: String = access_token.into();
87 let endpoint = ApiEndpoint::vertex(proj.clone(), loc.clone(), tok.clone());
88 let auth: Arc<dyn RestAuth> = Arc::new(VertexAIAuth::new(proj, loc, tok));
89 Self {
90 endpoint,
91 model: ModelId::FLASH_LATEST,
92 auth,
93 #[cfg(feature = "http")]
94 http: http::HttpClient::new(http::HttpConfig::default()),
95 }
96 }
97
98 pub fn from_vertex_refreshable(
108 project: impl Into<String>,
109 location: impl Into<String>,
110 refresher: impl Fn() -> String + Send + Sync + 'static,
111 ) -> Self {
112 let proj: String = project.into();
113 let loc: String = location.into();
114 let refresher = Arc::new(refresher);
117 let live_refresher = refresher.clone();
118 let endpoint =
119 ApiEndpoint::vertex_refreshing(proj.clone(), loc.clone(), move || live_refresher());
120 let auth: Arc<dyn RestAuth> =
121 Arc::new(VertexAIAuth::with_token_refresher(proj, loc, move || {
122 refresher()
123 }));
124 Self {
125 endpoint,
126 model: ModelId::FLASH_LATEST,
127 auth,
128 #[cfg(feature = "http")]
129 http: http::HttpClient::new(http::HttpConfig::default()),
130 }
131 }
132
133 pub fn model(mut self, model: impl Into<ModelId>) -> Self {
135 self.model = model.into();
136 self
137 }
138
139 #[cfg(feature = "http")]
141 pub fn http_config(mut self, config: http::HttpConfig) -> Self {
142 self.http = http::HttpClient::new(config);
143 self
144 }
145
146 pub fn auth(&self) -> &dyn AuthProvider {
148 &*self.auth
149 }
150
151 pub fn default_model(&self) -> &ModelId {
153 &self.model
154 }
155
156 pub fn rest_url(&self, endpoint: ServiceEndpoint) -> String {
158 self.auth.rest_url(endpoint, Some(&self.model))
159 }
160
161 pub fn rest_url_for(&self, endpoint: ServiceEndpoint, model: &ModelId) -> String {
163 self.auth.rest_url(endpoint, Some(model))
164 }
165
166 pub async fn auth_headers(&self) -> Result<Vec<(String, String)>, crate::session::AuthError> {
168 self.auth.auth_headers().await
169 }
170
171 pub fn live(&self, model: Option<ModelId>) -> ConnectBuilder {
177 let mut config = SessionConfig::from_endpoint(self.endpoint.clone());
178 config.model = model;
179 ConnectBuilder::new(config)
180 }
181
182 #[cfg(feature = "http")]
184 pub fn http_client(&self) -> &http::HttpClient {
185 &self.http
186 }
187
188 #[cfg(feature = "http")]
192 pub async fn rest_request(
193 &self,
194 endpoint: ServiceEndpoint,
195 body: &impl serde::Serialize,
196 ) -> Result<serde_json::Value, http::HttpError> {
197 let url = self.rest_url(endpoint);
198 let headers = self.auth.auth_headers().await?;
199 self.http.post_json(&url, headers, body).await
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn client_from_api_key() {
209 let client = Client::from_api_key("test-key");
210 assert_eq!(client.default_model(), &ModelId::FLASH_LATEST);
213 }
214
215 #[test]
216 fn client_from_vertex() {
217 let client = Client::from_vertex("proj", "us-central1", "tok");
218 let url = client.auth().ws_url(&ModelId::FLASH_LATEST);
219 assert!(url.contains("us-central1-aiplatform.googleapis.com"));
220 }
221
222 #[test]
223 fn client_model_override() {
224 let client = Client::from_api_key("key").model("models/gemini-2.0-flash-live-001");
225 assert_eq!(
226 client.default_model(),
227 &ModelId::from_static("models/gemini-2.0-flash-live-001")
228 );
229 }
230
231 #[test]
232 fn client_rest_url_generate() {
233 let client = Client::from_api_key("my-key")
234 .model(ModelId::from_static("models/gemini-2.0-flash-live-001"));
235 let url = client.rest_url(ServiceEndpoint::GenerateContent);
236 assert!(url.contains(":generateContent"));
237 assert!(!url.contains("my-key"), "the key rides in a header: {url}");
238 }
239
240 #[test]
241 fn client_rest_url_vertex() {
242 let client = Client::from_vertex("proj", "us-east1", "tok")
243 .model(ModelId::from_static("models/gemini-2.0-flash-live-001"));
244 let url = client.rest_url(ServiceEndpoint::GenerateContent);
245 assert!(url.contains("us-east1-aiplatform.googleapis.com"));
246 assert!(url.contains(":generateContent"));
247 }
248
249 #[test]
250 fn live_session_builder_created() {
251 let client = Client::from_api_key("key");
252 let _builder = client.live(Some(ModelId::from_static(
253 "models/gemini-2.0-flash-live-001",
254 )));
255 }
256
257 #[tokio::test]
258 async fn client_from_vertex_refreshable() {
259 use std::sync::atomic::{AtomicU32, Ordering};
260 let call_count = Arc::new(AtomicU32::new(0));
261 let cc = call_count.clone();
262 let client = Client::from_vertex_refreshable("proj", "us-central1", move || {
263 cc.fetch_add(1, Ordering::SeqCst);
264 "refreshed-token".to_string()
265 });
266 assert_eq!(call_count.load(Ordering::SeqCst), 0);
269 let headers = client.auth_headers().await.unwrap();
271 assert_eq!(headers[0].1, "Bearer refreshed-token");
272 assert_eq!(call_count.load(Ordering::SeqCst), 1);
273 let live_config = SessionConfig::from_endpoint(client.endpoint.clone());
275 assert_eq!(
276 live_config.bearer_token().as_deref(),
277 Some("refreshed-token")
278 );
279 assert_eq!(call_count.load(Ordering::SeqCst), 2);
280 }
281}