1use crate::{
2 batch::{BatchBuilder, BatchHandle},
3 cache::{CacheBuilder, CachedContentHandle},
4 embedding::{
5 BatchContentEmbeddingResponse, BatchEmbedContentsRequest, ContentEmbeddingResponse,
6 EmbedBuilder, EmbedContentRequest,
7 },
8 files::{
9 handle::FileHandle,
10 model::{File, ListFilesResponse},
11 },
12 generation::{ContentBuilder, GenerateContentRequest, GenerationResponse},
13};
14use eventsource_stream::{EventStreamError, Eventsource};
15use futures::{Stream, StreamExt, TryStreamExt};
16use mime::Mime;
17use reqwest::{
18 header::{HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue},
19 Client, ClientBuilder, RequestBuilder, Response,
20};
21use serde::{Deserialize, Serialize};
22use serde_json::json;
23use snafu::{OptionExt, ResultExt, Snafu};
24use std::{
25 fmt::{self, Formatter},
26 pin::Pin,
27 sync::{Arc, LazyLock},
28};
29use tracing::{instrument, Level, Span};
30use url::Url;
31
32use crate::batch::model::*;
33use crate::cache::model::*;
34
35pub type GenerationStream = Pin<Box<dyn Stream<Item = Result<GenerationResponse, Error>> + Send>>;
41
42static DEFAULT_BASE_URL: LazyLock<Url> = LazyLock::new(|| {
43 Url::parse("https://generativelanguage.googleapis.com/v1beta/")
44 .expect("unreachable error: failed to parse default base URL")
45});
46
47#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
48pub enum Model {
49 #[default]
50 #[serde(rename = "models/gemini-2.5-flash")]
51 Gemini25Flash,
52 #[serde(rename = "models/gemini-2.5-flash-lite")]
53 Gemini25FlashLite,
54 #[serde(rename = "models/gemini-2.5-flash-image")]
55 Gemini25FlashImage,
56 #[serde(rename = "models/gemini-2.5-pro")]
57 Gemini25Pro,
58 #[serde(rename = "models/gemini-3-flash-preview")]
59 Gemini3Flash,
60 #[serde(rename = "models/gemini-3-pro-preview")]
61 Gemini3Pro,
62 #[serde(rename = "models/gemini-3-pro-image-preview")]
63 Gemini3ProImage,
64 #[serde(rename = "models/text-embedding-004")]
65 TextEmbedding004,
66 #[serde(untagged)]
67 Custom(String),
68}
69
70impl Model {
71 pub fn as_str(&self) -> &str {
72 match self {
73 Model::Gemini25Flash => "models/gemini-2.5-flash",
74 Model::Gemini25FlashLite => "models/gemini-2.5-flash-lite",
75 Model::Gemini25FlashImage => "models/gemini-2.5-flash-image",
76 Model::Gemini25Pro => "models/gemini-2.5-pro",
77 Model::Gemini3Flash => "models/gemini-3-flash-preview",
78 Model::Gemini3Pro => "models/gemini-3-pro-preview",
79 Model::Gemini3ProImage => "models/gemini-3-pro-image-preview",
80 Model::TextEmbedding004 => "models/text-embedding-004",
81 Model::Custom(model) => model,
82 }
83 }
84}
85
86impl From<String> for Model {
87 fn from(model: String) -> Self {
88 Self::Custom(model)
89 }
90}
91
92impl fmt::Display for Model {
93 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94 match self {
95 Model::Gemini25Flash => write!(f, "models/gemini-2.5-flash"),
96 Model::Gemini25FlashLite => write!(f, "models/gemini-2.5-flash-lite"),
97 Model::Gemini25FlashImage => write!(f, "models/gemini-2.5-flash-image"),
98 Model::Gemini25Pro => write!(f, "models/gemini-2.5-pro"),
99 Model::Gemini3Flash => write!(f, "models/gemini-3-flash-preview"),
100 Model::Gemini3Pro => write!(f, "models/gemini-3-pro-preview"),
101 Model::Gemini3ProImage => write!(f, "models/gemini-3-pro-image-preview"),
102 Model::TextEmbedding004 => write!(f, "models/text-embedding-004"),
103 Model::Custom(model) => write!(f, "{model}"),
104 }
105 }
106}
107
108#[derive(Debug, Snafu)]
109#[snafu(visibility(pub))]
110pub enum Error {
111 #[snafu(display("failed to parse API key"))]
112 InvalidApiKey {
113 source: InvalidHeaderValue,
114 },
115
116 #[snafu(display("failed to construct URL (probably incorrect model name): {suffix}"))]
117 ConstructUrl {
118 source: url::ParseError,
119 suffix: String,
120 },
121
122 PerformRequestNew {
123 source: reqwest::Error,
124 },
125
126 #[snafu(display("failed to perform request to '{url}'"))]
127 PerformRequest {
128 source: reqwest::Error,
129 url: Url,
130 },
131
132 #[snafu(display(
133 "bad response from server; code {code}; description: {}",
134 description.as_deref().unwrap_or("none")
135 ))]
136 BadResponse {
137 code: u16,
139 description: Option<String>,
141 },
142
143 MissingResponseHeader {
144 header: String,
145 },
146
147 #[snafu(display("failed to obtain stream SSE part"))]
148 BadPart {
149 source: EventStreamError<reqwest::Error>,
150 },
151
152 #[snafu(display("failed to deserialize JSON response"))]
153 Deserialize {
154 source: serde_json::Error,
155 },
156
157 #[snafu(display("failed to generate content"))]
158 DecodeResponse {
159 source: reqwest::Error,
160 },
161
162 #[snafu(display("failed to parse URL"))]
163 UrlParse {
164 source: url::ParseError,
165 },
166
167 #[snafu(display("I/O error during file operations"))]
168 Io {
169 source: std::io::Error,
170 },
171
172 #[snafu(display("operation timed out: {name}"))]
173 OperationTimeout {
174 name: String,
175 },
176
177 #[snafu(display("operation failed: {name}, code: {code}, message: {message}"))]
178 OperationFailed {
179 name: String,
180 code: i32,
181 message: String,
182 },
183
184 #[snafu(display("invalid resource name: {name}"))]
185 InvalidResourceName {
186 name: String,
187 },
188}
189
190#[derive(Debug)]
192pub struct GeminiClient {
193 http_client: Client,
194 pub model: Model,
195 base_url: Url,
196}
197
198impl GeminiClient {
199 fn with_base_url<K: AsRef<str>, M: Into<Model>>(
201 client_builder: ClientBuilder,
202 api_key: K,
203 model: M,
204 base_url: Url,
205 ) -> Result<Self, Error> {
206 let headers = HeaderMap::from_iter([(
207 HeaderName::from_static("x-goog-api-key"),
208 HeaderValue::from_str(api_key.as_ref()).context(InvalidApiKeySnafu)?,
209 )]);
210
211 let http_client = client_builder
212 .default_headers(headers)
213 .build()
214 .expect("all parameters must be valid");
215
216 Ok(Self {
217 http_client,
218 model: model.into(),
219 base_url,
220 })
221 }
222
223 #[tracing::instrument(skip_all, err)]
225 async fn check_response(response: Response) -> Result<Response, Error> {
226 let status = response.status();
227 if !status.is_success() {
228 let description = response.text().await.ok();
229 BadResponseSnafu {
230 code: status.as_u16(),
231 description,
232 }
233 .fail()
234 } else {
235 Ok(response)
236 }
237 }
238
239 #[tracing::instrument(skip_all)]
317 #[doc(hidden)]
318 pub async fn perform_request<
319 B: FnOnce(&Client) -> RequestBuilder,
320 D: AsyncFn(Response) -> Result<T, Error>,
321 T,
322 >(
323 &self,
324 builder: B,
325 deserializer: D,
326 ) -> Result<T, Error> {
327 let request = builder(&self.http_client);
328 tracing::debug!("request built successfully");
329 let response = request.send().await.context(PerformRequestNewSnafu)?;
330 tracing::debug!("response received successfully");
331 let response = Self::check_response(response).await?;
332 tracing::debug!("response ok");
333 deserializer(response).await
334 }
335
336 #[tracing::instrument(skip(self), fields(request.type = "get", request.url = %url))]
340 async fn get_json<T: serde::de::DeserializeOwned>(&self, url: Url) -> Result<T, Error> {
341 self.perform_request(
342 |c| c.get(url),
343 async |r| r.json().await.context(DecodeResponseSnafu),
344 )
345 .await
346 }
347
348 #[tracing::instrument(skip(self, body), fields(request.type = "post", request.url = %url))]
352 async fn post_json<Req: serde::Serialize, Res: serde::de::DeserializeOwned>(
353 &self,
354 url: Url,
355 body: &Req,
356 ) -> Result<Res, Error> {
357 self.perform_request(
358 |c| c.post(url).json(body),
359 async |r| r.json().await.context(DecodeResponseSnafu),
360 )
361 .await
362 }
363
364 #[instrument(skip_all, fields(
366 model,
367 messages.parts.count = request.contents.len(),
368 tools.present = request.tools.is_some(),
369 system.instruction.present = request.system_instruction.is_some(),
370 cached.content.present = request.cached_content.is_some(),
371 usage.prompt_tokens,
372 usage.candidates_tokens,
373 usage.thoughts_tokens,
374 usage.cached_content_tokens,
375 usage.total_tokens,
376 ), ret(level = Level::TRACE), err)]
377 pub(crate) async fn generate_content_raw(
378 &self,
379 request: GenerateContentRequest,
380 ) -> Result<GenerationResponse, Error> {
381 let url = self.build_url("generateContent")?;
382 let response: GenerationResponse = self.post_json(url, &request).await?;
383
384 if let Some(usage) = &response.usage_metadata {
386 #[rustfmt::skip]
387 Span::current()
388 .record("usage.prompt_tokens", usage.prompt_token_count)
389 .record("usage.candidates_tokens", usage.candidates_token_count)
390 .record("usage.thoughts_tokens", usage.thoughts_token_count)
391 .record("usage.cached_content_tokens", usage.cached_content_token_count)
392 .record("usage.total_tokens", usage.total_token_count);
393
394 tracing::debug!("generation usage evaluated");
395 }
396
397 Ok(response)
398 }
399
400 #[instrument(skip_all, fields(
402 model,
403 messages.parts.count = request.contents.len(),
404 tools.present = request.tools.is_some(),
405 system.instruction.present = request.system_instruction.is_some(),
406 cached.content.present = request.cached_content.is_some(),
407 ), err)]
408 pub(crate) async fn generate_content_stream(
409 &self,
410 request: GenerateContentRequest,
411 ) -> Result<GenerationStream, Error> {
412 let mut url = self.build_url("streamGenerateContent")?;
413 url.query_pairs_mut().append_pair("alt", "sse");
414
415 let stream = self
416 .perform_request(
417 |c| c.post(url).json(&request),
418 async |r| Ok(r.bytes_stream()),
419 )
420 .await?;
421
422 Ok(Box::pin(
423 stream
424 .eventsource()
425 .map(|event| event.context(BadPartSnafu))
426 .and_then(|event| async move {
427 serde_json::from_str::<GenerationResponse>(&event.data)
428 .context(DeserializeSnafu)
429 }),
430 ))
431 }
432
433 #[instrument(skip_all, fields(
435 model,
436 messages.parts.count = request.contents.len(),
437 ))]
438 pub(crate) async fn count_tokens(
439 &self,
440 request: GenerateContentRequest,
441 ) -> Result<crate::generation::CountTokensResponse, Error> {
442 let url = self.build_url("countTokens")?;
443 let body = json!({
446 "generateContentRequest": {
447 "model": self.model.as_str(),
448 "contents": request.contents,
449 "generationConfig": request.generation_config,
450 "safetySettings": request.safety_settings,
451 "tools": request.tools,
452 "toolConfig": request.tool_config,
453 "systemInstruction": request.system_instruction,
454 "cachedContent": request.cached_content,
455 }
456 });
457 self.post_json(url, &body).await
458 }
459
460 #[instrument(skip_all, fields(
462 model,
463 task.type = request.task_type.as_ref().map(|t| format!("{t:?}")),
464 task.title = request.title,
465 task.output.dimensionality = request.output_dimensionality,
466 ))]
467 pub(crate) async fn embed_content(
468 &self,
469 request: EmbedContentRequest,
470 ) -> Result<ContentEmbeddingResponse, Error> {
471 let url = self.build_url("embedContent")?;
472 self.post_json(url, &request).await
473 }
474
475 #[instrument(skip_all, fields(batch.size = request.requests.len()))]
477 pub(crate) async fn embed_content_batch(
478 &self,
479 request: BatchEmbedContentsRequest,
480 ) -> Result<BatchContentEmbeddingResponse, Error> {
481 let url = self.build_url("batchEmbedContents")?;
482 self.post_json(url, &request).await
483 }
484
485 #[instrument(skip_all, fields(
487 batch.display_name = request.batch.display_name,
488 batch.size = request.batch.input_config.batch_size(),
489 ))]
490 pub(crate) async fn batch_generate_content(
491 &self,
492 request: BatchGenerateContentRequest,
493 ) -> Result<BatchGenerateContentResponse, Error> {
494 let url = self.build_url("batchGenerateContent")?;
495 self.post_json(url, &request).await
496 }
497
498 #[instrument(skip_all, fields(
500 operation.name = name,
501 ))]
502 pub(crate) async fn get_batch_operation<T: serde::de::DeserializeOwned>(
503 &self,
504 name: &str,
505 ) -> Result<T, Error> {
506 let url = self.build_batch_url(name, None)?;
507 self.get_json(url).await
508 }
509
510 #[instrument(skip_all, fields(
512 page.size = page_size,
513 page.token.present = page_token.is_some(),
514 ))]
515 pub(crate) async fn list_batch_operations(
516 &self,
517 page_size: Option<u32>,
518 page_token: Option<String>,
519 ) -> Result<ListBatchesResponse, Error> {
520 let mut url = self.build_batch_url("batches", None)?;
521
522 if let Some(size) = page_size {
523 url.query_pairs_mut()
524 .append_pair("pageSize", &size.to_string());
525 }
526 if let Some(token) = page_token {
527 url.query_pairs_mut().append_pair("pageToken", &token);
528 }
529
530 self.get_json(url).await
531 }
532
533 #[instrument(skip_all, fields(
535 page.size = page_size,
536 page.token.present = page_token.is_some(),
537 ))]
538 pub(crate) async fn list_files(
539 &self,
540 page_size: Option<u32>,
541 page_token: Option<String>,
542 ) -> Result<ListFilesResponse, Error> {
543 let mut url = self.build_files_url(None)?;
544
545 if let Some(size) = page_size {
546 url.query_pairs_mut()
547 .append_pair("pageSize", &size.to_string());
548 }
549 if let Some(token) = page_token {
550 url.query_pairs_mut().append_pair("pageToken", &token);
551 }
552
553 self.get_json(url).await
554 }
555
556 #[instrument(skip_all, fields(
558 operation.name = name,
559 ))]
560 pub(crate) async fn cancel_batch_operation(&self, name: &str) -> Result<(), Error> {
561 let url = self.build_batch_url(name, Some("cancel"))?;
562 self.perform_request(|c| c.post(url).json(&json!({})), async |_r| Ok(()))
563 .await
564 }
565
566 #[instrument(skip_all, fields(
568 operation.name = name,
569 ))]
570 pub(crate) async fn delete_batch_operation(&self, name: &str) -> Result<(), Error> {
571 let url = self.build_batch_url(name, None)?;
572 self.perform_request(|c| c.delete(url), async |_r| Ok(()))
573 .await
574 }
575
576 async fn create_upload(
577 &self,
578 bytes: usize,
579 display_name: Option<String>,
580 mime_type: Mime,
581 ) -> Result<Url, Error> {
582 let url = self
583 .base_url
584 .join("/upload/v1beta/files")
585 .context(ConstructUrlSnafu {
586 suffix: "/upload/v1beta/files".to_string(),
587 })?;
588
589 self.perform_request(
590 |c| {
591 c.post(url)
592 .header("X-Goog-Upload-Protocol", "resumable")
593 .header("X-Goog-Upload-Command", "start")
594 .header("X-Goog-Upload-Content-Length", bytes.to_string())
595 .header("X-Goog-Upload-Header-Content-Type", mime_type.to_string())
596 .json(&json!({"file": {"displayName": display_name}}))
597 },
598 async |r| {
599 r.headers()
600 .get("X-Goog-Upload-URL")
601 .context(MissingResponseHeaderSnafu {
602 header: "X-Goog-Upload-URL",
603 })
604 .and_then(|upload_url| {
605 upload_url
606 .to_str()
607 .map(str::to_string)
608 .map_err(|_| Error::BadResponse {
609 code: 500,
610 description: Some("Missing upload URL in response".to_string()),
611 })
612 })
613 .and_then(|url| Url::parse(&url).context(UrlParseSnafu))
614 },
615 )
616 .await
617 }
618
619 #[instrument(skip_all, fields(
621 file.size = file_bytes.len(),
622 mime.type = mime_type.to_string(),
623 file.display_name = display_name.as_deref(),
624 ))]
625 pub(crate) async fn upload_file(
626 &self,
627 display_name: Option<String>,
628 file_bytes: Vec<u8>,
629 mime_type: Mime,
630 ) -> Result<File, Error> {
631 let upload_url = self
633 .create_upload(file_bytes.len(), display_name, mime_type)
634 .await?;
635
636 let upload_response = self
638 .http_client
639 .post(upload_url.clone())
640 .header("X-Goog-Upload-Command", "upload, finalize")
641 .header("X-Goog-Upload-Offset", "0")
642 .body(file_bytes)
643 .send()
644 .await
645 .map_err(|e| Error::PerformRequest {
646 source: e,
647 url: upload_url,
648 })?;
649
650 let final_response = Self::check_response(upload_response).await?;
651
652 #[derive(serde::Deserialize)]
653 struct UploadResponse {
654 file: File,
655 }
656
657 let upload_response: UploadResponse =
658 final_response.json().await.context(DecodeResponseSnafu)?;
659 Ok(upload_response.file)
660 }
661
662 #[instrument(skip_all, fields(
664 file.name = name,
665 ))]
666 pub(crate) async fn get_file(&self, name: &str) -> Result<File, Error> {
667 let url = self.build_files_url(Some(name))?;
668 self.get_json(url).await
669 }
670
671 #[instrument(skip_all, fields(
673 file.name = name,
674 ))]
675 pub(crate) async fn delete_file(&self, name: &str) -> Result<(), Error> {
676 let url = self.build_files_url(Some(name))?;
677 self.perform_request(|c| c.delete(url), async |_r| Ok(()))
678 .await
679 }
680
681 #[instrument(skip_all, fields(
683 file.name = name,
684 ))]
685 pub(crate) async fn download_file(&self, name: &str) -> Result<Vec<u8>, Error> {
686 let mut url = self
687 .base_url
688 .join(&format!("/download/v1beta/{name}:download"))
689 .context(ConstructUrlSnafu {
690 suffix: format!("/download/v1beta/{name}:download"),
691 })?;
692 url.query_pairs_mut().append_pair("alt", "media");
693
694 self.perform_request(
695 |c| c.get(url),
696 async |r| {
697 r.bytes()
698 .await
699 .context(DecodeResponseSnafu)
700 .map(|bytes| bytes.to_vec())
701 },
702 )
703 .await
704 }
705
706 pub(crate) async fn create_cached_content(
708 &self,
709 cached_content: CreateCachedContentRequest,
710 ) -> Result<CachedContent, Error> {
711 let url = self.build_cache_url(None)?;
712 self.post_json(url, &cached_content).await
713 }
714
715 pub(crate) async fn get_cached_content(&self, name: &str) -> Result<CachedContent, Error> {
717 let url = self.build_cache_url(Some(name))?;
718 self.get_json(url).await
719 }
720
721 pub(crate) async fn update_cached_content(
723 &self,
724 name: &str,
725 expiration: CacheExpirationRequest,
726 ) -> Result<CachedContent, Error> {
727 let url = self.build_cache_url(Some(name))?;
728
729 let update_payload = match expiration {
731 CacheExpirationRequest::Ttl { ttl } => json!({ "ttl": ttl }),
732 CacheExpirationRequest::ExpireTime { expire_time } => {
733 json!({ "expireTime": expire_time.format(&time::format_description::well_known::Rfc3339).unwrap() })
734 }
735 };
736
737 self.perform_request(
738 |c| c.patch(url.clone()).json(&update_payload),
739 async |r| r.json().await.context(DecodeResponseSnafu),
740 )
741 .await
742 }
743
744 pub(crate) async fn delete_cached_content(&self, name: &str) -> Result<(), Error> {
746 let url = self.build_cache_url(Some(name))?;
747 self.perform_request(|c| c.delete(url.clone()), async |_r| Ok(()))
748 .await
749 }
750
751 pub(crate) async fn list_cached_contents(
753 &self,
754 page_size: Option<i32>,
755 page_token: Option<String>,
756 ) -> Result<ListCachedContentsResponse, Error> {
757 let mut url = self.build_cache_url(None)?;
758
759 if let Some(size) = page_size {
760 url.query_pairs_mut()
761 .append_pair("pageSize", &size.to_string());
762 }
763 if let Some(token) = page_token {
764 url.query_pairs_mut().append_pair("pageToken", &token);
765 }
766
767 self.get_json(url).await
768 }
769
770 #[tracing::instrument(skip(self), ret(level = Level::DEBUG))]
772 fn build_url_with_suffix(&self, suffix: &str) -> Result<Url, Error> {
773 self.base_url.join(suffix).context(ConstructUrlSnafu {
774 suffix: suffix.to_string(),
775 })
776 }
777
778 #[tracing::instrument(skip(self), ret(level = Level::DEBUG))]
780 fn build_url(&self, endpoint: &str) -> Result<Url, Error> {
781 let suffix = format!("{}:{endpoint}", self.model);
782 self.build_url_with_suffix(&suffix)
783 }
784
785 fn build_batch_url(&self, name: &str, action: Option<&str>) -> Result<Url, Error> {
787 let suffix = action
788 .map(|a| format!("{name}:{a}"))
789 .unwrap_or_else(|| name.to_string());
790 self.build_url_with_suffix(&suffix)
791 }
792
793 fn build_files_url(&self, name: Option<&str>) -> Result<Url, Error> {
795 let suffix = name
796 .map(|n| format!("files/{}", n.strip_prefix("files/").unwrap_or(n)))
797 .unwrap_or_else(|| "files".to_string());
798 self.build_url_with_suffix(&suffix)
799 }
800
801 fn build_cache_url(&self, name: Option<&str>) -> Result<Url, Error> {
803 let suffix = name
804 .map(|n| {
805 if n.starts_with("cachedContents/") {
806 n.to_string()
807 } else {
808 format!("cachedContents/{n}")
809 }
810 })
811 .unwrap_or_else(|| "cachedContents".to_string());
812 self.build_url_with_suffix(&suffix)
813 }
814
815 #[instrument(skip_all, fields(display_name = request.display_name.as_deref()))]
818 pub async fn create_file_search_store(
819 &self,
820 request: crate::file_search::CreateFileSearchStoreRequest,
821 ) -> Result<crate::file_search::FileSearchStore, Error> {
822 let url = self.build_url_with_suffix("fileSearchStores")?;
823 self.post_json(url, &request).await
824 }
825
826 #[instrument(skip_all, fields(store.name = %name))]
827 pub async fn get_file_search_store(
828 &self,
829 name: &str,
830 ) -> Result<crate::file_search::FileSearchStore, Error> {
831 let url = self.build_url_with_suffix(name)?;
832 self.get_json(url).await
833 }
834
835 #[instrument(skip_all, fields(
836 page.size = page_size,
837 page.token.present = page_token.is_some(),
838 ))]
839 pub async fn list_file_search_stores(
840 &self,
841 page_size: Option<u32>,
842 page_token: Option<&str>,
843 ) -> Result<crate::file_search::ListFileSearchStoresResponse, Error> {
844 let mut url = self.build_url_with_suffix("fileSearchStores")?;
845 if let Some(size) = page_size {
846 url.query_pairs_mut()
847 .append_pair("pageSize", &size.to_string());
848 }
849 if let Some(token) = page_token {
850 url.query_pairs_mut().append_pair("pageToken", token);
851 }
852 self.get_json(url).await
853 }
854
855 #[instrument(skip_all, fields(store.name = %name, force))]
856 pub async fn delete_file_search_store(&self, name: &str, force: bool) -> Result<(), Error> {
857 let mut url = self.build_url_with_suffix(name)?;
858 if force {
859 url.query_pairs_mut().append_pair("force", "true");
860 }
861 self.perform_request(|c| c.delete(url.clone()), async |_r| Ok(()))
862 .await
863 }
864
865 #[instrument(skip_all, fields(
868 store.name = %store_name,
869 file.size = file_data.len(),
870 display_name = display_name.as_deref(),
871 mime.type = mime_type.as_ref().map(|m| m.to_string()),
872 ))]
873 pub async fn upload_to_file_search_store(
874 &self,
875 store_name: &str,
876 file_data: Vec<u8>,
877 display_name: Option<String>,
878 mime_type: Option<mime::Mime>,
879 custom_metadata: Option<Vec<crate::file_search::CustomMetadata>>,
880 chunking_config: Option<crate::file_search::ChunkingConfig>,
881 ) -> Result<crate::file_search::Operation, Error> {
882 use crate::file_search::UploadToFileSearchStoreRequest;
883
884 let metadata_request = UploadToFileSearchStoreRequest {
885 display_name,
886 custom_metadata,
887 chunking_config,
888 mime_type: mime_type.clone(),
889 };
890
891 let mime = mime_type.unwrap_or(mime::APPLICATION_OCTET_STREAM);
892
893 let init_url = format!("/upload/v1beta/{}:uploadToFileSearchStore", store_name);
894 let upload_url = self
895 .initiate_resumable_upload(&init_url, file_data.len(), &mime, Some(&metadata_request))
896 .await?;
897
898 let operation: crate::file_search::Operation =
899 self.upload_file_data(&upload_url, file_data).await?;
900 Ok(operation)
901 }
902
903 #[instrument(skip_all, fields(
906 store.name = %store_name,
907 file.name = %request.file_name,
908 ))]
909 pub async fn import_file_to_search_store(
910 &self,
911 store_name: &str,
912 request: crate::file_search::ImportFileRequest,
913 ) -> Result<crate::file_search::Operation, Error> {
914 let url = self.build_url_with_suffix(&format!("{}:importFile", store_name))?;
915 self.post_json(url, &request).await
916 }
917
918 #[instrument(skip_all, fields(
921 store.name = %store_name,
922 document.id = %document_id,
923 ))]
924 pub async fn get_document(
925 &self,
926 store_name: &str,
927 document_id: &str,
928 ) -> Result<crate::file_search::Document, Error> {
929 let url =
930 self.build_url_with_suffix(&format!("{}/documents/{}", store_name, document_id))?;
931 self.get_json(url).await
932 }
933
934 #[instrument(skip_all, fields(
935 store.name = %store_name,
936 page.size = page_size,
937 page.token.present = page_token.is_some(),
938 ))]
939 pub async fn list_documents(
940 &self,
941 store_name: &str,
942 page_size: Option<u32>,
943 page_token: Option<&str>,
944 ) -> Result<crate::file_search::ListDocumentsResponse, Error> {
945 let mut url = self.build_url_with_suffix(&format!("{}/documents", store_name))?;
946 if let Some(size) = page_size {
947 url.query_pairs_mut()
948 .append_pair("pageSize", &size.to_string());
949 }
950 if let Some(token) = page_token {
951 url.query_pairs_mut().append_pair("pageToken", token);
952 }
953 self.get_json(url).await
954 }
955
956 #[instrument(skip_all, fields(
957 store.name = %store_name,
958 document.id = %document_id,
959 force,
960 ))]
961 pub async fn delete_document(
962 &self,
963 store_name: &str,
964 document_id: &str,
965 force: bool,
966 ) -> Result<(), Error> {
967 let mut url =
968 self.build_url_with_suffix(&format!("{}/documents/{}", store_name, document_id))?;
969 if force {
970 url.query_pairs_mut().append_pair("force", "true");
971 }
972 self.perform_request(|c| c.delete(url.clone()), async |_r| Ok(()))
973 .await
974 }
975
976 #[instrument(skip_all, fields(operation.name = %name))]
979 pub async fn get_operation(&self, name: &str) -> Result<crate::file_search::Operation, Error> {
980 let url = self.build_url_with_suffix(name)?;
981 self.get_json(url).await
982 }
983
984 #[instrument(skip(self, metadata))]
987 async fn initiate_resumable_upload<T: Serialize>(
988 &self,
989 path: &str,
990 total_bytes: usize,
991 mime_type: &Mime,
992 metadata: Option<&T>,
993 ) -> Result<String, Error> {
994 let url = self.build_url_with_suffix(path)?;
995
996 tracing::debug!("initiating resumable upload to {}", url);
997
998 let mut request = self
999 .http_client
1000 .post(url.clone())
1001 .header("X-Goog-Upload-Protocol", "resumable")
1002 .header("X-Goog-Upload-Command", "start")
1003 .header(
1004 "X-Goog-Upload-Header-Content-Length",
1005 total_bytes.to_string(),
1006 )
1007 .header("X-Goog-Upload-Header-Content-Type", mime_type.to_string())
1008 .header("Content-Type", "application/json");
1009
1010 if let Some(metadata) = metadata {
1012 request = request.json(metadata);
1013 } else {
1014 request = request.body("{}");
1015 }
1016
1017 let response = request.send().await.context(PerformRequestNewSnafu)?;
1018
1019 let response = Self::check_response(response).await?;
1021
1022 let upload_url = response
1023 .headers()
1024 .get("x-goog-upload-url")
1025 .and_then(|v| v.to_str().ok())
1026 .ok_or(Error::MissingResponseHeader {
1027 header: "x-goog-upload-url".to_string(),
1028 })?;
1029
1030 tracing::debug!("received upload url: {}", upload_url);
1031 Ok(upload_url.to_string())
1032 }
1033
1034 #[instrument(skip(self, data), fields(data.len = data.len()))]
1035 async fn upload_file_data<T: serde::de::DeserializeOwned>(
1036 &self,
1037 upload_url: &str,
1038 data: Vec<u8>,
1039 ) -> Result<T, Error> {
1040 tracing::debug!("uploading file data to {}", upload_url);
1041
1042 let data_len = data.len();
1043 let response = self
1044 .http_client
1045 .post(upload_url)
1046 .header("Content-Length", data_len.to_string())
1047 .header("X-Goog-Upload-Offset", "0")
1048 .header("X-Goog-Upload-Command", "upload, finalize")
1049 .body(data)
1050 .send()
1051 .await
1052 .context(PerformRequestNewSnafu)?;
1053
1054 tracing::debug!("upload response status: {}", response.status());
1055 let response = Self::check_response(response).await?;
1056
1057 response.json().await.context(DecodeResponseSnafu)
1059 }
1060}
1061
1062pub struct GeminiBuilder {
1096 key: String,
1097 model: Model,
1098 client_builder: ClientBuilder,
1099 base_url: Url,
1100}
1101
1102impl GeminiBuilder {
1103 pub fn new<K: Into<String>>(key: K) -> Self {
1105 Self {
1106 key: key.into(),
1107 model: Model::default(),
1108 client_builder: ClientBuilder::default(),
1109 base_url: DEFAULT_BASE_URL.clone(),
1110 }
1111 }
1112
1113 pub fn with_model<M: Into<Model>>(mut self, model: M) -> Self {
1115 self.model = model.into();
1116 self
1117 }
1118
1119 pub fn with_http_client(mut self, client_builder: ClientBuilder) -> Self {
1121 self.client_builder = client_builder;
1122 self
1123 }
1124
1125 pub fn with_base_url(mut self, base_url: Url) -> Self {
1127 self.base_url = base_url;
1128 self
1129 }
1130
1131 pub fn build(self) -> Result<Gemini, Error> {
1133 Ok(Gemini {
1134 client: Arc::new(GeminiClient::with_base_url(
1135 self.client_builder,
1136 self.key,
1137 self.model,
1138 self.base_url,
1139 )?),
1140 })
1141 }
1142}
1143
1144#[derive(Clone)]
1146pub struct Gemini {
1147 client: Arc<GeminiClient>,
1148}
1149
1150impl Gemini {
1151 pub fn new<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1153 Self::with_model(api_key, Model::default())
1154 }
1155
1156 pub fn pro<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1158 Self::with_model(api_key, Model::Gemini25Pro)
1159 }
1160
1161 pub fn pro_image<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1163 Self::with_model(api_key, Model::Gemini3ProImage)
1164 }
1165
1166 pub fn with_model<K: AsRef<str>, M: Into<Model>>(api_key: K, model: M) -> Result<Self, Error> {
1168 Self::with_model_and_base_url(api_key, model, DEFAULT_BASE_URL.clone())
1169 }
1170
1171 pub fn with_base_url<K: AsRef<str>>(api_key: K, base_url: Url) -> Result<Self, Error> {
1173 Self::with_model_and_base_url(api_key, Model::default(), base_url)
1174 }
1175
1176 pub fn with_model_and_base_url<K: AsRef<str>, M: Into<Model>>(
1178 api_key: K,
1179 model: M,
1180 base_url: Url,
1181 ) -> Result<Self, Error> {
1182 let client =
1183 GeminiClient::with_base_url(Default::default(), api_key, model.into(), base_url)?;
1184 Ok(Self {
1185 client: Arc::new(client),
1186 })
1187 }
1188
1189 pub fn generate_content(&self) -> ContentBuilder {
1191 ContentBuilder::new(self.client.clone())
1192 }
1193
1194 pub fn embed_content(&self) -> EmbedBuilder {
1196 EmbedBuilder::new(self.client.clone())
1197 }
1198
1199 pub fn batch_generate_content(&self) -> BatchBuilder {
1201 BatchBuilder::new(self.client.clone())
1202 }
1203
1204 pub fn get_batch(&self, name: &str) -> BatchHandle {
1206 BatchHandle::new(name.to_string(), self.client.clone())
1207 }
1208
1209 pub fn list_batches(
1213 &self,
1214 page_size: impl Into<Option<u32>>,
1215 ) -> impl Stream<Item = Result<BatchOperation, Error>> + Send {
1216 let client = self.client.clone();
1217 let page_size = page_size.into();
1218 async_stream::try_stream! {
1219 let mut page_token: Option<String> = None;
1220 loop {
1221 let response = client
1222 .list_batch_operations(page_size, page_token.clone())
1223 .await?;
1224
1225 for operation in response.operations {
1226 yield operation;
1227 }
1228
1229 if let Some(next_page_token) = response.next_page_token {
1230 page_token = Some(next_page_token);
1231 } else {
1232 break;
1233 }
1234 }
1235 }
1236 }
1237
1238 pub fn create_cache(&self) -> CacheBuilder {
1240 CacheBuilder::new(self.client.clone())
1241 }
1242
1243 pub fn get_cached_content(&self, name: &str) -> CachedContentHandle {
1245 CachedContentHandle::new(name.to_string(), self.client.clone())
1246 }
1247
1248 pub fn list_cached_contents(
1252 &self,
1253 page_size: impl Into<Option<i32>>,
1254 ) -> impl Stream<Item = Result<CachedContentSummary, Error>> + Send {
1255 let client = self.client.clone();
1256 let page_size = page_size.into();
1257 async_stream::try_stream! {
1258 let mut page_token: Option<String> = None;
1259 loop {
1260 let response = client
1261 .list_cached_contents(page_size, page_token.clone())
1262 .await?;
1263
1264 for cached_content in response.cached_contents {
1265 yield cached_content;
1266 }
1267
1268 if let Some(next_page_token) = response.next_page_token {
1269 page_token = Some(next_page_token);
1270 } else {
1271 break;
1272 }
1273 }
1274 }
1275 }
1276
1277 pub fn create_file<B: Into<Vec<u8>>>(&self, bytes: B) -> crate::files::builder::FileBuilder {
1279 crate::files::builder::FileBuilder::new(self.client.clone(), bytes)
1280 }
1281
1282 pub async fn get_file(&self, name: &str) -> Result<FileHandle, Error> {
1284 let file = self.client.get_file(name).await?;
1285 Ok(FileHandle::new(self.client.clone(), file))
1286 }
1287
1288 pub fn list_files(
1292 &self,
1293 page_size: impl Into<Option<u32>>,
1294 ) -> impl Stream<Item = Result<FileHandle, Error>> + Send {
1295 let client = self.client.clone();
1296 let page_size = page_size.into();
1297 async_stream::try_stream! {
1298 let mut page_token: Option<String> = None;
1299 loop {
1300 let response = client
1301 .list_files(page_size, page_token.clone())
1302 .await?;
1303
1304 for file in response.files {
1305 yield FileHandle::new(client.clone(), file);
1306 }
1307
1308 if let Some(next_page_token) = response.next_page_token {
1309 page_token = Some(next_page_token);
1310 } else {
1311 break;
1312 }
1313 }
1314 }
1315 }
1316
1317 pub fn create_file_search_store(&self) -> crate::file_search::FileSearchStoreBuilder {
1319 crate::file_search::FileSearchStoreBuilder {
1320 client: self.client.clone(),
1321 display_name: None,
1322 }
1323 }
1324
1325 pub async fn get_file_search_store(
1327 &self,
1328 name: &str,
1329 ) -> Result<crate::file_search::FileSearchStoreHandle, Error> {
1330 let store = self.client.get_file_search_store(name).await?;
1331 Ok(crate::file_search::FileSearchStoreHandle::new(
1332 self.client.clone(),
1333 store,
1334 ))
1335 }
1336
1337 pub fn list_file_search_stores(
1341 &self,
1342 page_size: impl Into<Option<u32>>,
1343 ) -> impl Stream<Item = Result<crate::file_search::FileSearchStoreHandle, Error>> + Send {
1344 let client = self.client.clone();
1345 let page_size = page_size.into();
1346 async_stream::try_stream! {
1347 let mut page_token: Option<String> = None;
1348 loop {
1349 let response = client
1350 .list_file_search_stores(page_size, page_token.as_deref())
1351 .await?;
1352
1353 for store in response.file_search_stores {
1354 yield crate::file_search::FileSearchStoreHandle::new(client.clone(), store);
1355 }
1356
1357 if let Some(next_page_token) = response.next_page_token {
1358 page_token = Some(next_page_token);
1359 } else {
1360 break;
1361 }
1362 }
1363 }
1364 }
1365}