1#[allow(deprecated)]
2use crate::{
3 batch::{BatchBuilder, BatchHandle},
4 cache::{CacheBuilder, CachedContentHandle},
5 embedding::{
6 BatchContentEmbeddingResponse, BatchEmbedContentsRequest, ContentEmbeddingResponse,
7 EmbedBuilder, EmbedContentRequest,
8 },
9 files::{
10 handle::FileHandle,
11 model::{File, ListFilesResponse},
12 },
13 generation::{ContentBuilder, GenerateContentRequest, GenerationResponse},
14 interactions::{
15 builder::InteractionBuilder,
16 handle::InteractionHandle,
17 model::{CreateInteractionRequest, Interaction},
18 stream::{InteractionEvent, InteractionStream},
19 },
20};
21use eventsource_stream::{EventStreamError, Eventsource};
22use futures::{Stream, StreamExt, TryStreamExt};
23use mime::Mime;
24use reqwest::{
25 header::{HeaderMap, HeaderName, HeaderValue, InvalidHeaderValue},
26 Client, ClientBuilder, RequestBuilder, Response,
27};
28use serde::{Deserialize, Serialize};
29use serde_json::json;
30use snafu::{OptionExt, ResultExt, Snafu};
31use std::{
32 fmt::{self, Formatter},
33 pin::Pin,
34 sync::{Arc, LazyLock},
35};
36use tracing::{instrument, Level, Span};
37use url::Url;
38
39use crate::batch::model::*;
40use crate::cache::model::*;
41
42#[allow(deprecated)]
48pub type GenerationStream = Pin<Box<dyn Stream<Item = Result<GenerationResponse, Error>> + Send>>;
49
50static DEFAULT_BASE_URL: LazyLock<Url> = LazyLock::new(|| {
51 Url::parse("https://generativelanguage.googleapis.com/v1beta/")
52 .expect("unreachable error: failed to parse default base URL")
53});
54
55#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
56pub enum Model {
57 #[default]
58 #[serde(rename = "models/gemini-2.5-flash")]
59 Gemini25Flash,
60 #[serde(rename = "models/gemini-2.5-flash-lite")]
61 Gemini25FlashLite,
62 #[serde(rename = "models/gemini-2.5-flash-image")]
63 Gemini25FlashImage,
64 #[serde(rename = "models/gemini-2.5-pro")]
65 Gemini25Pro,
66 #[serde(rename = "models/gemini-3-flash-preview")]
67 Gemini3Flash,
68 #[serde(rename = "models/gemini-3-pro-preview")]
69 Gemini3Pro,
70 #[serde(rename = "models/gemini-3-pro-image-preview")]
71 Gemini3ProImage,
72 #[serde(rename = "models/text-embedding-004")]
73 TextEmbedding004,
74 #[serde(untagged)]
75 Custom(String),
76}
77
78impl Model {
79 pub fn as_str(&self) -> &str {
80 match self {
81 Model::Gemini25Flash => "models/gemini-2.5-flash",
82 Model::Gemini25FlashLite => "models/gemini-2.5-flash-lite",
83 Model::Gemini25FlashImage => "models/gemini-2.5-flash-image",
84 Model::Gemini25Pro => "models/gemini-2.5-pro",
85 Model::Gemini3Flash => "models/gemini-3-flash-preview",
86 Model::Gemini3Pro => "models/gemini-3-pro-preview",
87 Model::Gemini3ProImage => "models/gemini-3-pro-image-preview",
88 Model::TextEmbedding004 => "models/text-embedding-004",
89 Model::Custom(model) => model,
90 }
91 }
92}
93
94impl From<String> for Model {
95 fn from(model: String) -> Self {
96 Self::Custom(model)
97 }
98}
99
100impl fmt::Display for Model {
101 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
102 match self {
103 Model::Gemini25Flash => write!(f, "models/gemini-2.5-flash"),
104 Model::Gemini25FlashLite => write!(f, "models/gemini-2.5-flash-lite"),
105 Model::Gemini25FlashImage => write!(f, "models/gemini-2.5-flash-image"),
106 Model::Gemini25Pro => write!(f, "models/gemini-2.5-pro"),
107 Model::Gemini3Flash => write!(f, "models/gemini-3-flash-preview"),
108 Model::Gemini3Pro => write!(f, "models/gemini-3-pro-preview"),
109 Model::Gemini3ProImage => write!(f, "models/gemini-3-pro-image-preview"),
110 Model::TextEmbedding004 => write!(f, "models/text-embedding-004"),
111 Model::Custom(model) => write!(f, "{model}"),
112 }
113 }
114}
115
116#[derive(Debug, Snafu)]
117#[snafu(visibility(pub))]
118pub enum Error {
119 #[snafu(display("failed to parse API key"))]
120 InvalidApiKey {
121 source: InvalidHeaderValue,
122 },
123
124 #[snafu(display("failed to construct URL (probably incorrect model name): {suffix}"))]
125 ConstructUrl {
126 source: url::ParseError,
127 suffix: String,
128 },
129
130 PerformRequestNew {
131 source: reqwest::Error,
132 },
133
134 #[snafu(display("failed to perform request to '{url}'"))]
135 PerformRequest {
136 source: reqwest::Error,
137 url: Url,
138 },
139
140 #[snafu(display(
141 "bad response from server; code {code}; description: {}",
142 description.as_deref().unwrap_or("none")
143 ))]
144 BadResponse {
145 code: u16,
147 description: Option<String>,
149 },
150
151 MissingResponseHeader {
152 header: String,
153 },
154
155 #[snafu(display("failed to obtain stream SSE part"))]
156 BadPart {
157 source: EventStreamError<reqwest::Error>,
158 },
159
160 #[snafu(display("failed to deserialize JSON response"))]
161 Deserialize {
162 source: serde_json::Error,
163 },
164
165 #[snafu(display("failed to generate content"))]
166 DecodeResponse {
167 source: reqwest::Error,
168 },
169
170 #[snafu(display("failed to parse URL"))]
171 UrlParse {
172 source: url::ParseError,
173 },
174
175 #[snafu(display("I/O error during file operations"))]
176 Io {
177 source: std::io::Error,
178 },
179
180 #[snafu(display("operation timed out: {name}"))]
181 OperationTimeout {
182 name: String,
183 },
184
185 #[snafu(display("operation failed: {name}, code: {code}, message: {message}"))]
186 OperationFailed {
187 name: String,
188 code: i32,
189 message: String,
190 },
191
192 #[snafu(display("invalid resource name: {name}"))]
193 InvalidResourceName {
194 name: String,
195 },
196}
197
198#[derive(Debug)]
200pub struct GeminiClient {
201 http_client: Client,
202 pub model: Model,
203 base_url: Url,
204}
205
206impl GeminiClient {
207 fn with_base_url<K: AsRef<str>, M: Into<Model>>(
209 client_builder: ClientBuilder,
210 api_key: K,
211 model: M,
212 base_url: Url,
213 ) -> Result<Self, Error> {
214 let headers = HeaderMap::from_iter([(
215 HeaderName::from_static("x-goog-api-key"),
216 HeaderValue::from_str(api_key.as_ref()).context(InvalidApiKeySnafu)?,
217 )]);
218
219 let http_client = client_builder
220 .default_headers(headers)
221 .build()
222 .expect("all parameters must be valid");
223
224 Ok(Self {
225 http_client,
226 model: model.into(),
227 base_url,
228 })
229 }
230
231 #[tracing::instrument(skip_all, err)]
233 async fn check_response(response: Response) -> Result<Response, Error> {
234 let status = response.status();
235 if !status.is_success() {
236 let description = response.text().await.ok();
237 BadResponseSnafu {
238 code: status.as_u16(),
239 description,
240 }
241 .fail()
242 } else {
243 Ok(response)
244 }
245 }
246
247 #[tracing::instrument(skip_all)]
325 #[doc(hidden)]
326 pub async fn perform_request<
327 B: FnOnce(&Client) -> RequestBuilder,
328 D: AsyncFn(Response) -> Result<T, Error>,
329 T,
330 >(
331 &self,
332 builder: B,
333 deserializer: D,
334 ) -> Result<T, Error> {
335 let request = builder(&self.http_client);
336 tracing::debug!("request built successfully");
337 let response = request.send().await.context(PerformRequestNewSnafu)?;
338 tracing::debug!("response received successfully");
339 let response = Self::check_response(response).await?;
340 tracing::debug!("response ok");
341 deserializer(response).await
342 }
343
344 #[tracing::instrument(skip(self), fields(request.type = "get", request.url = %url))]
348 async fn get_json<T: serde::de::DeserializeOwned>(&self, url: Url) -> Result<T, Error> {
349 self.perform_request(
350 |c| c.get(url),
351 async |r| r.json().await.context(DecodeResponseSnafu),
352 )
353 .await
354 }
355
356 #[tracing::instrument(skip(self, body), fields(request.type = "post", request.url = %url))]
360 async fn post_json<Req: serde::Serialize, Res: serde::de::DeserializeOwned>(
361 &self,
362 url: Url,
363 body: &Req,
364 ) -> Result<Res, Error> {
365 self.perform_request(
366 |c| c.post(url).json(body),
367 async |r| r.json().await.context(DecodeResponseSnafu),
368 )
369 .await
370 }
371
372 #[allow(deprecated)]
374 #[instrument(skip_all, fields(
375 model,
376 messages.parts.count = request.contents.len(),
377 tools.present = request.tools.is_some(),
378 system.instruction.present = request.system_instruction.is_some(),
379 cached.content.present = request.cached_content.is_some(),
380 usage.prompt_tokens,
381 usage.candidates_tokens,
382 usage.thoughts_tokens,
383 usage.cached_content_tokens,
384 usage.total_tokens,
385 ), ret(level = Level::TRACE), err)]
386 pub(crate) async fn generate_content_raw(
387 &self,
388 request: GenerateContentRequest,
389 ) -> Result<GenerationResponse, Error> {
390 let url = self.build_url("generateContent")?;
391 let response: GenerationResponse = self.post_json(url, &request).await?;
392
393 if let Some(usage) = &response.usage_metadata {
395 #[rustfmt::skip]
396 Span::current()
397 .record("usage.prompt_tokens", usage.prompt_token_count)
398 .record("usage.candidates_tokens", usage.candidates_token_count)
399 .record("usage.thoughts_tokens", usage.thoughts_token_count)
400 .record("usage.cached_content_tokens", usage.cached_content_token_count)
401 .record("usage.total_tokens", usage.total_token_count);
402
403 tracing::debug!("generation usage evaluated");
404 }
405
406 Ok(response)
407 }
408
409 #[allow(deprecated)]
411 #[instrument(skip_all, fields(
412 model,
413 messages.parts.count = request.contents.len(),
414 tools.present = request.tools.is_some(),
415 system.instruction.present = request.system_instruction.is_some(),
416 cached.content.present = request.cached_content.is_some(),
417 ), err)]
418 pub(crate) async fn generate_content_stream(
419 &self,
420 request: GenerateContentRequest,
421 ) -> Result<GenerationStream, Error> {
422 let mut url = self.build_url("streamGenerateContent")?;
423 url.query_pairs_mut().append_pair("alt", "sse");
424
425 let stream = self
426 .perform_request(
427 |c| c.post(url).json(&request),
428 async |r| Ok(r.bytes_stream()),
429 )
430 .await?;
431
432 Ok(Box::pin(
433 stream
434 .eventsource()
435 .map(|event| event.context(BadPartSnafu))
436 .try_filter(|event| std::future::ready(event.data != "[DONE]"))
437 .and_then(|event| async move {
438 serde_json::from_str::<GenerationResponse>(&event.data)
439 .context(DeserializeSnafu)
440 }),
441 ))
442 }
443
444 #[allow(deprecated)]
446 #[instrument(skip_all, fields(
447 model,
448 messages.parts.count = request.contents.len(),
449 ))]
450 pub(crate) async fn count_tokens(
451 &self,
452 request: GenerateContentRequest,
453 ) -> Result<crate::generation::CountTokensResponse, Error> {
454 let url = self.build_url("countTokens")?;
455 let body = json!({
458 "generateContentRequest": {
459 "model": self.model.as_str(),
460 "contents": request.contents,
461 "generationConfig": request.generation_config,
462 "safetySettings": request.safety_settings,
463 "tools": request.tools,
464 "toolConfig": request.tool_config,
465 "systemInstruction": request.system_instruction,
466 "cachedContent": request.cached_content,
467 }
468 });
469 self.post_json(url, &body).await
470 }
471
472 #[instrument(skip_all, fields(
474 model,
475 task.type = request.task_type.as_ref().map(|t| format!("{t:?}")),
476 task.title = request.title,
477 task.output.dimensionality = request.output_dimensionality,
478 ))]
479 pub(crate) async fn embed_content(
480 &self,
481 request: EmbedContentRequest,
482 ) -> Result<ContentEmbeddingResponse, Error> {
483 let url = self.build_url("embedContent")?;
484 self.post_json(url, &request).await
485 }
486
487 #[instrument(skip_all, fields(batch.size = request.requests.len()))]
489 pub(crate) async fn embed_content_batch(
490 &self,
491 request: BatchEmbedContentsRequest,
492 ) -> Result<BatchContentEmbeddingResponse, Error> {
493 let url = self.build_url("batchEmbedContents")?;
494 self.post_json(url, &request).await
495 }
496
497 #[instrument(skip_all, fields(
499 batch.display_name = request.batch.display_name,
500 batch.size = request.batch.input_config.batch_size(),
501 ))]
502 pub(crate) async fn batch_generate_content(
503 &self,
504 request: BatchGenerateContentRequest,
505 ) -> Result<BatchGenerateContentResponse, Error> {
506 let url = self.build_url("batchGenerateContent")?;
507 self.post_json(url, &request).await
508 }
509
510 #[instrument(skip_all, fields(
512 operation.name = name,
513 ))]
514 pub(crate) async fn get_batch_operation<T: serde::de::DeserializeOwned>(
515 &self,
516 name: &str,
517 ) -> Result<T, Error> {
518 let url = self.build_batch_url(name, None)?;
519 self.get_json(url).await
520 }
521
522 #[instrument(skip_all, fields(
524 page.size = page_size,
525 page.token.present = page_token.is_some(),
526 ))]
527 pub(crate) async fn list_batch_operations(
528 &self,
529 page_size: Option<u32>,
530 page_token: Option<String>,
531 ) -> Result<ListBatchesResponse, Error> {
532 let mut url = self.build_batch_url("batches", None)?;
533
534 if let Some(size) = page_size {
535 url.query_pairs_mut()
536 .append_pair("pageSize", &size.to_string());
537 }
538 if let Some(token) = page_token {
539 url.query_pairs_mut().append_pair("pageToken", &token);
540 }
541
542 self.get_json(url).await
543 }
544
545 #[instrument(skip_all, fields(
547 page.size = page_size,
548 page.token.present = page_token.is_some(),
549 ))]
550 pub(crate) async fn list_files(
551 &self,
552 page_size: Option<u32>,
553 page_token: Option<String>,
554 ) -> Result<ListFilesResponse, Error> {
555 let mut url = self.build_files_url(None)?;
556
557 if let Some(size) = page_size {
558 url.query_pairs_mut()
559 .append_pair("pageSize", &size.to_string());
560 }
561 if let Some(token) = page_token {
562 url.query_pairs_mut().append_pair("pageToken", &token);
563 }
564
565 self.get_json(url).await
566 }
567
568 #[instrument(skip_all, fields(
570 operation.name = name,
571 ))]
572 pub(crate) async fn cancel_batch_operation(&self, name: &str) -> Result<(), Error> {
573 let url = self.build_batch_url(name, Some("cancel"))?;
574 self.perform_request(|c| c.post(url).json(&json!({})), async |_r| Ok(()))
575 .await
576 }
577
578 #[instrument(skip_all, fields(
580 operation.name = name,
581 ))]
582 pub(crate) async fn delete_batch_operation(&self, name: &str) -> Result<(), Error> {
583 let url = self.build_batch_url(name, None)?;
584 self.perform_request(|c| c.delete(url), async |_r| Ok(()))
585 .await
586 }
587
588 async fn create_upload(
589 &self,
590 bytes: usize,
591 display_name: Option<String>,
592 mime_type: Mime,
593 ) -> Result<Url, Error> {
594 let url = self
595 .base_url
596 .join("/upload/v1beta/files")
597 .context(ConstructUrlSnafu {
598 suffix: "/upload/v1beta/files".to_string(),
599 })?;
600
601 self.perform_request(
602 |c| {
603 c.post(url)
604 .header("X-Goog-Upload-Protocol", "resumable")
605 .header("X-Goog-Upload-Command", "start")
606 .header("X-Goog-Upload-Content-Length", bytes.to_string())
607 .header("X-Goog-Upload-Header-Content-Type", mime_type.to_string())
608 .json(&json!({"file": {"displayName": display_name}}))
609 },
610 async |r| {
611 r.headers()
612 .get("X-Goog-Upload-URL")
613 .context(MissingResponseHeaderSnafu {
614 header: "X-Goog-Upload-URL",
615 })
616 .and_then(|upload_url| {
617 upload_url
618 .to_str()
619 .map(str::to_string)
620 .map_err(|_| Error::BadResponse {
621 code: 500,
622 description: Some("Missing upload URL in response".to_string()),
623 })
624 })
625 .and_then(|url| Url::parse(&url).context(UrlParseSnafu))
626 },
627 )
628 .await
629 }
630
631 #[instrument(skip_all, fields(
633 file.size = file_bytes.len(),
634 mime.type = mime_type.to_string(),
635 file.display_name = display_name.as_deref(),
636 ))]
637 pub(crate) async fn upload_file(
638 &self,
639 display_name: Option<String>,
640 file_bytes: Vec<u8>,
641 mime_type: Mime,
642 ) -> Result<File, Error> {
643 let upload_url = self
645 .create_upload(file_bytes.len(), display_name, mime_type)
646 .await?;
647
648 let upload_response = self
650 .http_client
651 .post(upload_url.clone())
652 .header("X-Goog-Upload-Command", "upload, finalize")
653 .header("X-Goog-Upload-Offset", "0")
654 .body(file_bytes)
655 .send()
656 .await
657 .map_err(|e| Error::PerformRequest {
658 source: e,
659 url: upload_url,
660 })?;
661
662 let final_response = Self::check_response(upload_response).await?;
663
664 #[derive(serde::Deserialize)]
665 struct UploadResponse {
666 file: File,
667 }
668
669 let upload_response: UploadResponse =
670 final_response.json().await.context(DecodeResponseSnafu)?;
671 Ok(upload_response.file)
672 }
673
674 #[instrument(skip_all, fields(
676 file.name = name,
677 ))]
678 pub(crate) async fn get_file(&self, name: &str) -> Result<File, Error> {
679 let url = self.build_files_url(Some(name))?;
680 self.get_json(url).await
681 }
682
683 #[instrument(skip_all, fields(
685 file.name = name,
686 ))]
687 pub(crate) async fn delete_file(&self, name: &str) -> Result<(), Error> {
688 let url = self.build_files_url(Some(name))?;
689 self.perform_request(|c| c.delete(url), async |_r| Ok(()))
690 .await
691 }
692
693 #[instrument(skip_all, fields(
695 file.name = name,
696 ))]
697 pub(crate) async fn download_file(&self, name: &str) -> Result<Vec<u8>, Error> {
698 let mut url = self
699 .base_url
700 .join(&format!("/download/v1beta/{name}:download"))
701 .context(ConstructUrlSnafu {
702 suffix: format!("/download/v1beta/{name}:download"),
703 })?;
704 url.query_pairs_mut().append_pair("alt", "media");
705
706 self.perform_request(
707 |c| c.get(url),
708 async |r| {
709 r.bytes()
710 .await
711 .context(DecodeResponseSnafu)
712 .map(|bytes| bytes.to_vec())
713 },
714 )
715 .await
716 }
717
718 pub(crate) async fn create_cached_content(
720 &self,
721 cached_content: CreateCachedContentRequest,
722 ) -> Result<CachedContent, Error> {
723 let url = self.build_cache_url(None)?;
724 self.post_json(url, &cached_content).await
725 }
726
727 pub(crate) async fn get_cached_content(&self, name: &str) -> Result<CachedContent, Error> {
729 let url = self.build_cache_url(Some(name))?;
730 self.get_json(url).await
731 }
732
733 pub(crate) async fn update_cached_content(
735 &self,
736 name: &str,
737 expiration: CacheExpirationRequest,
738 ) -> Result<CachedContent, Error> {
739 let url = self.build_cache_url(Some(name))?;
740
741 let update_payload = match expiration {
743 CacheExpirationRequest::Ttl { ttl } => json!({ "ttl": ttl }),
744 CacheExpirationRequest::ExpireTime { expire_time } => {
745 json!({ "expireTime": expire_time.format(&time::format_description::well_known::Rfc3339).unwrap() })
746 }
747 };
748
749 self.perform_request(
750 |c| c.patch(url.clone()).json(&update_payload),
751 async |r| r.json().await.context(DecodeResponseSnafu),
752 )
753 .await
754 }
755
756 pub(crate) async fn delete_cached_content(&self, name: &str) -> Result<(), Error> {
758 let url = self.build_cache_url(Some(name))?;
759 self.perform_request(|c| c.delete(url.clone()), async |_r| Ok(()))
760 .await
761 }
762
763 pub(crate) async fn list_cached_contents(
765 &self,
766 page_size: Option<i32>,
767 page_token: Option<String>,
768 ) -> Result<ListCachedContentsResponse, Error> {
769 let mut url = self.build_cache_url(None)?;
770
771 if let Some(size) = page_size {
772 url.query_pairs_mut()
773 .append_pair("pageSize", &size.to_string());
774 }
775 if let Some(token) = page_token {
776 url.query_pairs_mut().append_pair("pageToken", &token);
777 }
778
779 self.get_json(url).await
780 }
781
782 #[instrument(skip_all, fields(
786 model = request.model.as_deref().unwrap_or(""),
787 agent = request.agent.as_deref().unwrap_or(""),
788 tools.count = request.tools.len(),
789 background = request.background.unwrap_or(false),
790 previous.interaction.present = request.previous_interaction_id.is_some(),
791 status.code,
792 usage.total_tokens,
793 ))]
794 pub(crate) async fn create_interaction(
795 &self,
796 request: CreateInteractionRequest,
797 ) -> Result<Interaction, Error> {
798 let url = self.build_url_with_suffix("interactions")?;
799 let response: Interaction = self.post_json(url, &request).await?;
800
801 Span::current().record("status.code", response.status.as_ref());
802
803 if let Some(usage) = &response.usage {
804 Span::current().record("usage.total_tokens", usage.total_tokens);
805 }
806
807 Ok(response)
808 }
809
810 #[instrument(skip_all, fields(
812 model = request.model.as_deref().unwrap_or(""),
813 agent = request.agent.as_deref().unwrap_or(""),
814 tools.count = request.tools.len(),
815 ))]
816 pub(crate) async fn create_interaction_stream(
817 &self,
818 mut request: CreateInteractionRequest,
819 ) -> Result<InteractionStream, Error> {
820 let mut url = self.build_url_with_suffix("interactions")?;
821 url.query_pairs_mut().append_pair("alt", "sse");
822 request.stream = Some(true);
823
824 let stream = self
825 .perform_request(
826 |c| c.post(url).json(&request),
827 async |r| Ok(r.bytes_stream()),
828 )
829 .await?;
830
831 Ok(Box::pin(
832 stream
833 .eventsource()
834 .map(|event| event.context(BadPartSnafu))
835 .try_filter(|event| std::future::ready(event.data != "[DONE]"))
836 .and_then(|event| async move {
837 serde_json::from_str::<InteractionEvent>(&event.data).context(DeserializeSnafu)
838 }),
839 ))
840 }
841
842 #[instrument(skip_all, fields(
844 interaction.id = id,
845 ))]
846 pub(crate) async fn get_interaction(&self, id: &str) -> Result<Interaction, Error> {
847 let url = self.build_url_with_suffix(&format!("interactions/{id}"))?;
848 self.get_json(url).await
849 }
850
851 #[instrument(skip_all, fields(
853 interaction.id = id,
854 ))]
855 pub(crate) async fn get_interaction_stream(
856 &self,
857 id: &str,
858 last_event_id: Option<&str>,
859 ) -> Result<InteractionStream, Error> {
860 let mut url = self.build_url_with_suffix(&format!("interactions/{id}"))?;
861 url.query_pairs_mut().append_pair("stream", "true");
862 if let Some(event_id) = last_event_id {
863 url.query_pairs_mut().append_pair("last_event_id", event_id);
864 }
865
866 let stream = self
867 .perform_request(|c| c.get(url), async |r| Ok(r.bytes_stream()))
868 .await?;
869
870 Ok(Box::pin(
871 stream
872 .eventsource()
873 .map(|event| event.context(BadPartSnafu))
874 .try_filter(|event| std::future::ready(event.data != "[DONE]"))
875 .and_then(|event| async move {
876 serde_json::from_str::<InteractionEvent>(&event.data).context(DeserializeSnafu)
877 }),
878 ))
879 }
880
881 #[instrument(skip_all, fields(
883 interaction.id = id,
884 ))]
885 pub(crate) async fn cancel_interaction(&self, id: &str) -> Result<Interaction, Error> {
886 let url = self.build_url_with_suffix(&format!("interactions/{id}/cancel"))?;
887 self.perform_request(
888 |c| c.post(url).json(&json!({})),
889 async |r| r.json().await.context(DecodeResponseSnafu),
890 )
891 .await
892 }
893
894 #[instrument(skip_all, fields(
896 interaction.id = id,
897 ))]
898 pub(crate) async fn delete_interaction(&self, id: &str) -> Result<(), Error> {
899 let url = self.build_url_with_suffix(&format!("interactions/{id}"))?;
900 self.perform_request(|c| c.delete(url), async |_r| Ok(()))
901 .await
902 }
903
904 #[tracing::instrument(skip(self), ret(level = Level::DEBUG))]
906 fn build_url_with_suffix(&self, suffix: &str) -> Result<Url, Error> {
907 self.base_url.join(suffix).context(ConstructUrlSnafu {
908 suffix: suffix.to_string(),
909 })
910 }
911
912 #[tracing::instrument(skip(self), ret(level = Level::DEBUG))]
914 fn build_url(&self, endpoint: &str) -> Result<Url, Error> {
915 let suffix = format!("{}:{endpoint}", self.model);
916 self.build_url_with_suffix(&suffix)
917 }
918
919 fn build_batch_url(&self, name: &str, action: Option<&str>) -> Result<Url, Error> {
921 let suffix = action
922 .map(|a| format!("{name}:{a}"))
923 .unwrap_or_else(|| name.to_string());
924 self.build_url_with_suffix(&suffix)
925 }
926
927 fn build_files_url(&self, name: Option<&str>) -> Result<Url, Error> {
929 let suffix = name
930 .map(|n| format!("files/{}", n.strip_prefix("files/").unwrap_or(n)))
931 .unwrap_or_else(|| "files".to_string());
932 self.build_url_with_suffix(&suffix)
933 }
934
935 fn build_cache_url(&self, name: Option<&str>) -> Result<Url, Error> {
937 let suffix = name
938 .map(|n| {
939 if n.starts_with("cachedContents/") {
940 n.to_string()
941 } else {
942 format!("cachedContents/{n}")
943 }
944 })
945 .unwrap_or_else(|| "cachedContents".to_string());
946 self.build_url_with_suffix(&suffix)
947 }
948
949 #[instrument(skip_all, fields(display_name = request.display_name.as_deref()))]
952 pub async fn create_file_search_store(
953 &self,
954 request: crate::file_search::CreateFileSearchStoreRequest,
955 ) -> Result<crate::file_search::FileSearchStore, Error> {
956 let url = self.build_url_with_suffix("fileSearchStores")?;
957 self.post_json(url, &request).await
958 }
959
960 #[instrument(skip_all, fields(store.name = %name))]
961 pub async fn get_file_search_store(
962 &self,
963 name: &str,
964 ) -> Result<crate::file_search::FileSearchStore, Error> {
965 let url = self.build_url_with_suffix(name)?;
966 self.get_json(url).await
967 }
968
969 #[instrument(skip_all, fields(
970 page.size = page_size,
971 page.token.present = page_token.is_some(),
972 ))]
973 pub async fn list_file_search_stores(
974 &self,
975 page_size: Option<u32>,
976 page_token: Option<&str>,
977 ) -> Result<crate::file_search::ListFileSearchStoresResponse, Error> {
978 let mut url = self.build_url_with_suffix("fileSearchStores")?;
979 if let Some(size) = page_size {
980 url.query_pairs_mut()
981 .append_pair("pageSize", &size.to_string());
982 }
983 if let Some(token) = page_token {
984 url.query_pairs_mut().append_pair("pageToken", token);
985 }
986 self.get_json(url).await
987 }
988
989 #[instrument(skip_all, fields(store.name = %name, force))]
990 pub async fn delete_file_search_store(&self, name: &str, force: bool) -> Result<(), Error> {
991 let mut url = self.build_url_with_suffix(name)?;
992 if force {
993 url.query_pairs_mut().append_pair("force", "true");
994 }
995 self.perform_request(|c| c.delete(url.clone()), async |_r| Ok(()))
996 .await
997 }
998
999 #[instrument(skip_all, fields(
1002 store.name = %store_name,
1003 file.size = file_data.len(),
1004 display_name = display_name.as_deref(),
1005 mime.type = mime_type.as_ref().map(|m| m.to_string()),
1006 ))]
1007 pub async fn upload_to_file_search_store(
1008 &self,
1009 store_name: &str,
1010 file_data: Vec<u8>,
1011 display_name: Option<String>,
1012 mime_type: Option<mime::Mime>,
1013 custom_metadata: Option<Vec<crate::file_search::CustomMetadata>>,
1014 chunking_config: Option<crate::file_search::ChunkingConfig>,
1015 ) -> Result<crate::file_search::Operation, Error> {
1016 use crate::file_search::UploadToFileSearchStoreRequest;
1017
1018 let metadata_request = UploadToFileSearchStoreRequest {
1019 display_name,
1020 custom_metadata,
1021 chunking_config,
1022 mime_type: mime_type.clone(),
1023 };
1024
1025 let mime = mime_type.unwrap_or(mime::APPLICATION_OCTET_STREAM);
1026
1027 let init_url = format!("/upload/v1beta/{}:uploadToFileSearchStore", store_name);
1028 let upload_url = self
1029 .initiate_resumable_upload(&init_url, file_data.len(), &mime, Some(&metadata_request))
1030 .await?;
1031
1032 let operation: crate::file_search::Operation =
1033 self.upload_file_data(&upload_url, file_data).await?;
1034 Ok(operation)
1035 }
1036
1037 #[instrument(skip_all, fields(
1040 store.name = %store_name,
1041 file.name = %request.file_name,
1042 ))]
1043 pub async fn import_file_to_search_store(
1044 &self,
1045 store_name: &str,
1046 request: crate::file_search::ImportFileRequest,
1047 ) -> Result<crate::file_search::Operation, Error> {
1048 let url = self.build_url_with_suffix(&format!("{}:importFile", store_name))?;
1049 self.post_json(url, &request).await
1050 }
1051
1052 #[instrument(skip_all, fields(
1055 store.name = %store_name,
1056 document.id = %document_id,
1057 ))]
1058 pub async fn get_document(
1059 &self,
1060 store_name: &str,
1061 document_id: &str,
1062 ) -> Result<crate::file_search::Document, Error> {
1063 let url =
1064 self.build_url_with_suffix(&format!("{}/documents/{}", store_name, document_id))?;
1065 self.get_json(url).await
1066 }
1067
1068 #[instrument(skip_all, fields(
1069 store.name = %store_name,
1070 page.size = page_size,
1071 page.token.present = page_token.is_some(),
1072 ))]
1073 pub async fn list_documents(
1074 &self,
1075 store_name: &str,
1076 page_size: Option<u32>,
1077 page_token: Option<&str>,
1078 ) -> Result<crate::file_search::ListDocumentsResponse, Error> {
1079 let mut url = self.build_url_with_suffix(&format!("{}/documents", store_name))?;
1080 if let Some(size) = page_size {
1081 url.query_pairs_mut()
1082 .append_pair("pageSize", &size.to_string());
1083 }
1084 if let Some(token) = page_token {
1085 url.query_pairs_mut().append_pair("pageToken", token);
1086 }
1087 self.get_json(url).await
1088 }
1089
1090 #[instrument(skip_all, fields(
1091 store.name = %store_name,
1092 document.id = %document_id,
1093 force,
1094 ))]
1095 pub async fn delete_document(
1096 &self,
1097 store_name: &str,
1098 document_id: &str,
1099 force: bool,
1100 ) -> Result<(), Error> {
1101 let mut url =
1102 self.build_url_with_suffix(&format!("{}/documents/{}", store_name, document_id))?;
1103 if force {
1104 url.query_pairs_mut().append_pair("force", "true");
1105 }
1106 self.perform_request(|c| c.delete(url.clone()), async |_r| Ok(()))
1107 .await
1108 }
1109
1110 #[instrument(skip_all, fields(operation.name = %name))]
1113 pub async fn get_operation(&self, name: &str) -> Result<crate::file_search::Operation, Error> {
1114 let url = self.build_url_with_suffix(name)?;
1115 self.get_json(url).await
1116 }
1117
1118 #[instrument(skip(self, metadata))]
1121 async fn initiate_resumable_upload<T: Serialize>(
1122 &self,
1123 path: &str,
1124 total_bytes: usize,
1125 mime_type: &Mime,
1126 metadata: Option<&T>,
1127 ) -> Result<String, Error> {
1128 let url = self.build_url_with_suffix(path)?;
1129
1130 tracing::debug!("initiating resumable upload to {}", url);
1131
1132 let mut request = self
1133 .http_client
1134 .post(url.clone())
1135 .header("X-Goog-Upload-Protocol", "resumable")
1136 .header("X-Goog-Upload-Command", "start")
1137 .header(
1138 "X-Goog-Upload-Header-Content-Length",
1139 total_bytes.to_string(),
1140 )
1141 .header("X-Goog-Upload-Header-Content-Type", mime_type.to_string())
1142 .header("Content-Type", "application/json");
1143
1144 if let Some(metadata) = metadata {
1146 request = request.json(metadata);
1147 } else {
1148 request = request.body("{}");
1149 }
1150
1151 let response = request.send().await.context(PerformRequestNewSnafu)?;
1152
1153 let response = Self::check_response(response).await?;
1155
1156 let upload_url = response
1157 .headers()
1158 .get("x-goog-upload-url")
1159 .and_then(|v| v.to_str().ok())
1160 .ok_or(Error::MissingResponseHeader {
1161 header: "x-goog-upload-url".to_string(),
1162 })?;
1163
1164 tracing::debug!("received upload url: {}", upload_url);
1165 Ok(upload_url.to_string())
1166 }
1167
1168 #[instrument(skip(self, data), fields(data.len = data.len()))]
1169 async fn upload_file_data<T: serde::de::DeserializeOwned>(
1170 &self,
1171 upload_url: &str,
1172 data: Vec<u8>,
1173 ) -> Result<T, Error> {
1174 tracing::debug!("uploading file data to {}", upload_url);
1175
1176 let data_len = data.len();
1177 let response = self
1178 .http_client
1179 .post(upload_url)
1180 .header("Content-Length", data_len.to_string())
1181 .header("X-Goog-Upload-Offset", "0")
1182 .header("X-Goog-Upload-Command", "upload, finalize")
1183 .body(data)
1184 .send()
1185 .await
1186 .context(PerformRequestNewSnafu)?;
1187
1188 tracing::debug!("upload response status: {}", response.status());
1189 let response = Self::check_response(response).await?;
1190
1191 response.json().await.context(DecodeResponseSnafu)
1193 }
1194}
1195
1196pub struct GeminiBuilder {
1230 key: String,
1231 model: Model,
1232 client_builder: ClientBuilder,
1233 base_url: Url,
1234}
1235
1236impl GeminiBuilder {
1237 pub fn new<K: Into<String>>(key: K) -> Self {
1239 Self {
1240 key: key.into(),
1241 model: Model::default(),
1242 client_builder: ClientBuilder::default(),
1243 base_url: DEFAULT_BASE_URL.clone(),
1244 }
1245 }
1246
1247 pub fn with_model<M: Into<Model>>(mut self, model: M) -> Self {
1249 self.model = model.into();
1250 self
1251 }
1252
1253 pub fn with_http_client(mut self, client_builder: ClientBuilder) -> Self {
1255 self.client_builder = client_builder;
1256 self
1257 }
1258
1259 pub fn with_base_url(mut self, base_url: Url) -> Self {
1261 self.base_url = base_url;
1262 self
1263 }
1264
1265 pub fn build(self) -> Result<Gemini, Error> {
1267 Ok(Gemini {
1268 client: Arc::new(GeminiClient::with_base_url(
1269 self.client_builder,
1270 self.key,
1271 self.model,
1272 self.base_url,
1273 )?),
1274 })
1275 }
1276}
1277
1278#[derive(Clone)]
1280pub struct Gemini {
1281 client: Arc<GeminiClient>,
1282}
1283
1284impl Gemini {
1285 pub fn new<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1287 Self::with_model(api_key, Model::default())
1288 }
1289
1290 pub fn pro<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1292 Self::with_model(api_key, Model::Gemini25Pro)
1293 }
1294
1295 pub fn pro_image<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1297 Self::with_model(api_key, Model::Gemini3ProImage)
1298 }
1299
1300 pub fn with_model<K: AsRef<str>, M: Into<Model>>(api_key: K, model: M) -> Result<Self, Error> {
1302 Self::with_model_and_base_url(api_key, model, DEFAULT_BASE_URL.clone())
1303 }
1304
1305 pub fn with_base_url<K: AsRef<str>>(api_key: K, base_url: Url) -> Result<Self, Error> {
1307 Self::with_model_and_base_url(api_key, Model::default(), base_url)
1308 }
1309
1310 pub fn with_model_and_base_url<K: AsRef<str>, M: Into<Model>>(
1312 api_key: K,
1313 model: M,
1314 base_url: Url,
1315 ) -> Result<Self, Error> {
1316 let client =
1317 GeminiClient::with_base_url(Default::default(), api_key, model.into(), base_url)?;
1318 Ok(Self {
1319 client: Arc::new(client),
1320 })
1321 }
1322
1323 #[deprecated(
1325 since = "1.8.0",
1326 note = "Use Gemini::create_interaction() instead. See migration guide: interactions-api/migration-plan.md"
1327 )]
1328 #[allow(deprecated)]
1329 pub fn generate_content(&self) -> ContentBuilder {
1330 ContentBuilder::new(self.client.clone())
1331 }
1332
1333 pub fn create_interaction(&self) -> InteractionBuilder {
1339 InteractionBuilder::new(self.client.clone())
1340 }
1341
1342 pub fn interaction(&self, id: &str) -> InteractionHandle {
1346 InteractionHandle::new(id.to_string(), self.client.clone())
1347 }
1348
1349 pub async fn get_interaction(&self, id: &str) -> Result<Interaction, Error> {
1351 self.client.get_interaction(id).await
1352 }
1353
1354 pub fn embed_content(&self) -> EmbedBuilder {
1356 EmbedBuilder::new(self.client.clone())
1357 }
1358
1359 pub fn batch_generate_content(&self) -> BatchBuilder {
1361 BatchBuilder::new(self.client.clone())
1362 }
1363
1364 pub fn get_batch(&self, name: &str) -> BatchHandle {
1366 BatchHandle::new(name.to_string(), self.client.clone())
1367 }
1368
1369 pub fn list_batches(
1373 &self,
1374 page_size: impl Into<Option<u32>>,
1375 ) -> impl Stream<Item = Result<BatchOperation, Error>> + Send {
1376 let client = self.client.clone();
1377 let page_size = page_size.into();
1378 async_stream::try_stream! {
1379 let mut page_token: Option<String> = None;
1380 loop {
1381 let response = client
1382 .list_batch_operations(page_size, page_token.clone())
1383 .await?;
1384
1385 for operation in response.operations {
1386 yield operation;
1387 }
1388
1389 if let Some(next_page_token) = response.next_page_token {
1390 page_token = Some(next_page_token);
1391 } else {
1392 break;
1393 }
1394 }
1395 }
1396 }
1397
1398 pub fn create_cache(&self) -> CacheBuilder {
1400 CacheBuilder::new(self.client.clone())
1401 }
1402
1403 pub fn get_cached_content(&self, name: &str) -> CachedContentHandle {
1405 CachedContentHandle::new(name.to_string(), self.client.clone())
1406 }
1407
1408 pub fn list_cached_contents(
1412 &self,
1413 page_size: impl Into<Option<i32>>,
1414 ) -> impl Stream<Item = Result<CachedContentSummary, Error>> + Send {
1415 let client = self.client.clone();
1416 let page_size = page_size.into();
1417 async_stream::try_stream! {
1418 let mut page_token: Option<String> = None;
1419 loop {
1420 let response = client
1421 .list_cached_contents(page_size, page_token.clone())
1422 .await?;
1423
1424 for cached_content in response.cached_contents {
1425 yield cached_content;
1426 }
1427
1428 if let Some(next_page_token) = response.next_page_token {
1429 page_token = Some(next_page_token);
1430 } else {
1431 break;
1432 }
1433 }
1434 }
1435 }
1436
1437 pub fn create_file<B: Into<Vec<u8>>>(&self, bytes: B) -> crate::files::builder::FileBuilder {
1439 crate::files::builder::FileBuilder::new(self.client.clone(), bytes)
1440 }
1441
1442 pub async fn get_file(&self, name: &str) -> Result<FileHandle, Error> {
1444 let file = self.client.get_file(name).await?;
1445 Ok(FileHandle::new(self.client.clone(), file))
1446 }
1447
1448 pub fn list_files(
1452 &self,
1453 page_size: impl Into<Option<u32>>,
1454 ) -> impl Stream<Item = Result<FileHandle, Error>> + Send {
1455 let client = self.client.clone();
1456 let page_size = page_size.into();
1457 async_stream::try_stream! {
1458 let mut page_token: Option<String> = None;
1459 loop {
1460 let response = client
1461 .list_files(page_size, page_token.clone())
1462 .await?;
1463
1464 for file in response.files {
1465 yield FileHandle::new(client.clone(), file);
1466 }
1467
1468 if let Some(next_page_token) = response.next_page_token {
1469 page_token = Some(next_page_token);
1470 } else {
1471 break;
1472 }
1473 }
1474 }
1475 }
1476
1477 pub fn create_file_search_store(&self) -> crate::file_search::FileSearchStoreBuilder {
1479 crate::file_search::FileSearchStoreBuilder {
1480 client: self.client.clone(),
1481 display_name: None,
1482 }
1483 }
1484
1485 pub async fn get_file_search_store(
1487 &self,
1488 name: &str,
1489 ) -> Result<crate::file_search::FileSearchStoreHandle, Error> {
1490 let store = self.client.get_file_search_store(name).await?;
1491 Ok(crate::file_search::FileSearchStoreHandle::new(
1492 self.client.clone(),
1493 store,
1494 ))
1495 }
1496
1497 pub fn list_file_search_stores(
1501 &self,
1502 page_size: impl Into<Option<u32>>,
1503 ) -> impl Stream<Item = Result<crate::file_search::FileSearchStoreHandle, Error>> + Send {
1504 let client = self.client.clone();
1505 let page_size = page_size.into();
1506 async_stream::try_stream! {
1507 let mut page_token: Option<String> = None;
1508 loop {
1509 let response = client
1510 .list_file_search_stores(page_size, page_token.as_deref())
1511 .await?;
1512
1513 for store in response.file_search_stores {
1514 yield crate::file_search::FileSearchStoreHandle::new(client.clone(), store);
1515 }
1516
1517 if let Some(next_page_token) = response.next_page_token {
1518 page_token = Some(next_page_token);
1519 } else {
1520 break;
1521 }
1522 }
1523 }
1524 }
1525}