Skip to main content

gemini_rust/
client.rs

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/// Type alias for streaming generation responses
43///
44/// A pinned, boxed stream that yields `GenerationResponse` chunks as they arrive
45/// from the API. Used for streaming content generation to receive partial results
46/// before the complete response is ready.
47#[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        /// HTTP status code
146        code: u16,
147        /// HTTP error description
148        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/// Internal client for making requests to the Gemini API
199#[derive(Debug)]
200pub struct GeminiClient {
201    http_client: Client,
202    pub model: Model,
203    base_url: Url,
204}
205
206impl GeminiClient {
207    /// Create a new client with custom base URL
208    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    /// Check the response status code and return an error if it is not successful
232    #[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    /// Performs an HTTP request to the Gemini API with standardized error handling.
248    ///
249    /// This method provides a generic way to make HTTP requests to the Gemini API with
250    /// consistent error handling, response checking, and deserialization. It handles:
251    /// - Building the HTTP request using a provided builder function
252    /// - Sending the request and handling network errors
253    /// - Checking the response status code for errors
254    /// - Deserializing the response using a provided deserializer function
255    ///
256    /// # Type Parameters
257    /// * `B` - A function that takes a `&Client` and returns a `RequestBuilder`
258    /// * `D` - An async function that takes ownership of a `Response` and returns a `Result<T, Error>`
259    /// * `T` - The type of the deserialized response
260    ///
261    /// # Note
262    /// The `AsyncFn` trait is a standard Rust feature (stabilized in v1.85) and does not
263    /// require any additional imports or feature flags.
264    ///
265    /// # Parameters
266    /// * `builder` - A function that constructs the HTTP request using the client
267    /// * `deserializer` - An async function that processes the response into the desired type
268    ///
269    /// # Examples
270    ///
271    /// Basic HTTP operations:
272    /// ```no_run
273    /// # use gemini_rust::client::*;
274    /// # use reqwest::Response;
275    /// # use url::Url;
276    /// # use serde_json::Value;
277    /// # use snafu::ResultExt;
278    /// # async fn examples(client: &GeminiClient) -> Result<(), Box<dyn std::error::Error>> {
279    /// # let url: Url = "https://example.com".parse()?;
280    /// # let request = Value::Null;
281    ///
282    /// // POST request with JSON payload
283    /// let _response : () = client
284    ///     .perform_request(
285    ///         |c| c.post(url.clone()).json(&request),
286    ///         async |r| r.json().await.context(DecodeResponseSnafu),
287    ///     )
288    ///     .await?;
289    ///
290    /// // GET request with JSON response
291    /// let _response : () = client
292    ///     .perform_request(
293    ///         |c| c.get(url.clone()),
294    ///         async |r| r.json().await.context(DecodeResponseSnafu),
295    ///     )
296    ///     .await?;
297    ///
298    /// // DELETE request with no response body
299    /// let _response = client
300    ///     .perform_request(|c| c.delete(url), async |_r| Ok(()))
301    ///     .await?;
302    /// # Ok(())
303    /// # }
304    /// ```
305    ///
306    /// Request returning a stream:
307    /// ```no_run
308    /// # use gemini_rust::client::*;
309    /// # use reqwest::Response;
310    /// # use url::Url;
311    /// # use serde_json::Value;
312    /// # async fn example(client: &GeminiClient) -> Result<(), Box<dyn std::error::Error>> {
313    /// # let url: Url = "https://example.com".parse()?;
314    /// # let request = Value::Null;
315    /// let _stream = client
316    ///     .perform_request(
317    ///         |c| c.post(url).json(&request),
318    ///         async |r| Ok(r.bytes_stream()),
319    ///     )
320    ///     .await?;
321    /// # Ok(())
322    /// # }
323    /// ```
324    #[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    /// Perform a GET request and deserialize the JSON response.
345    ///
346    /// This is a convenience wrapper around [`perform_request`](Self::perform_request).
347    #[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    /// Perform a POST request with JSON body and deserialize the JSON response.
357    ///
358    /// This is a convenience wrapper around [`perform_request`](Self::perform_request).
359    #[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    /// Generate content
373    #[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        // Record usage metadata
394        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    /// Generate content with streaming
410    #[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    /// Count tokens for content
445    #[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        // Wrap the request in a "generateContentRequest" field and explicitly add the model.
456        // The countTokens API requires the model to be specified within generateContentRequest.
457        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    /// Embed content
473    #[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    /// Batch Embed content
488    #[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    /// Batch generate content (synchronous API that returns results immediately)
498    #[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    /// Get a batch operation
511    #[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    /// List batch operations
523    #[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    /// List files
546    #[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    /// Cancel a batch operation
569    #[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    /// Delete a batch operation
579    #[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    /// Upload a file using the resumable upload protocol.
632    #[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        // Step 1: Create resumable upload session
644        let upload_url = self
645            .create_upload(file_bytes.len(), display_name, mime_type)
646            .await?;
647
648        // Step 2: Upload file content
649        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    /// Get a file resource
675    #[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    /// Delete a file resource
684    #[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    /// Download a file resource
694    #[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    /// Create cached content
719    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    /// Get cached content
728    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    /// Update cached content (typically to update TTL)
734    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        // Create a minimal update payload with just the expiration
742        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    /// Delete cached content
757    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    /// List cached contents
764    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    // ========== Interactions API ==========
783
784    /// Create an interaction (non-streaming).
785    #[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    /// Create an interaction (streaming).
811    #[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    /// Get an interaction by ID.
843    #[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    /// Get an interaction in streaming mode (resume from last_event_id).
852    #[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    /// Cancel an interaction.
882    #[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    /// Delete an interaction.
895    #[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    /// Build a URL with the given suffix
905    #[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    /// Build a URL for the API
913    #[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    /// Build a URL for a batch operation
920    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    /// Build a URL for file operations
928    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    /// Build a URL for cache operations
936    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    // File Search Store operations
950
951    #[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    // Upload operation (resumable protocol)
1000
1001    #[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    // Import operation
1038
1039    #[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    // Document operations
1053
1054    #[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    // Operation operations
1111
1112    #[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    // Resumable upload helpers
1119
1120    #[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        // Always send metadata as JSON body, even if it's empty
1145        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        // Check response status
1154        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        // The finalize response contains the result
1192        response.json().await.context(DecodeResponseSnafu)
1193    }
1194}
1195
1196/// A builder for the `Gemini` client.
1197///
1198/// # Examples
1199///
1200/// ## Basic usage
1201///
1202/// ```no_run
1203/// use gemini_rust::{GeminiBuilder, Model};
1204///
1205/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1206/// let gemini = GeminiBuilder::new("YOUR_API_KEY")
1207///     .with_model(Model::Gemini25Pro)
1208///     .build()?;
1209/// # Ok(())
1210/// # }
1211/// ```
1212///
1213/// ## With proxy configuration
1214///
1215/// ```no_run
1216/// use gemini_rust::{GeminiBuilder, Model};
1217/// use reqwest::{ClientBuilder, Proxy};
1218///
1219/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
1220/// let proxy = Proxy::https("https://my.proxy")?;
1221/// let http_client = ClientBuilder::new().proxy(proxy);
1222///
1223/// let gemini = GeminiBuilder::new("YOUR_API_KEY")
1224///     .with_http_client(http_client)
1225///     .build()?;
1226/// # Ok(())
1227/// # }
1228/// ```
1229pub struct GeminiBuilder {
1230    key: String,
1231    model: Model,
1232    client_builder: ClientBuilder,
1233    base_url: Url,
1234}
1235
1236impl GeminiBuilder {
1237    /// Creates a new `GeminiBuilder` with the given API key.
1238    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    /// Sets the model for the client.
1248    pub fn with_model<M: Into<Model>>(mut self, model: M) -> Self {
1249        self.model = model.into();
1250        self
1251    }
1252
1253    /// Sets a custom `reqwest::ClientBuilder`.
1254    pub fn with_http_client(mut self, client_builder: ClientBuilder) -> Self {
1255        self.client_builder = client_builder;
1256        self
1257    }
1258
1259    /// Sets a custom base URL for the API.
1260    pub fn with_base_url(mut self, base_url: Url) -> Self {
1261        self.base_url = base_url;
1262        self
1263    }
1264
1265    /// Builds the `Gemini` client.
1266    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/// Client for the Gemini API
1279#[derive(Clone)]
1280pub struct Gemini {
1281    client: Arc<GeminiClient>,
1282}
1283
1284impl Gemini {
1285    /// Create a new client with the specified API key
1286    pub fn new<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1287        Self::with_model(api_key, Model::default())
1288    }
1289
1290    /// Create a new client for the Gemini Pro model
1291    pub fn pro<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1292        Self::with_model(api_key, Model::Gemini25Pro)
1293    }
1294
1295    /// Create a new client for the Gemini Pro 3 image model
1296    pub fn pro_image<K: AsRef<str>>(api_key: K) -> Result<Self, Error> {
1297        Self::with_model(api_key, Model::Gemini3ProImage)
1298    }
1299
1300    /// Create a new client with the specified API key and model
1301    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    /// Create a new client with custom base URL
1306    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    /// Create a new client with the specified API key, model, and base URL
1311    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    /// Start building a content generation request
1324    #[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    /// Start building an interaction request.
1334    ///
1335    /// The Interactions API is the recommended way to use Gemini models and agents.
1336    /// It provides server-side state management, observable steps, background execution,
1337    /// and unified support for models and agents.
1338    pub fn create_interaction(&self) -> InteractionBuilder {
1339        InteractionBuilder::new(self.client.clone())
1340    }
1341
1342    /// Get a handle to an interaction by its ID.
1343    ///
1344    /// The handle can be used for get, cancel, delete, and poll operations.
1345    pub fn interaction(&self, id: &str) -> InteractionHandle {
1346        InteractionHandle::new(id.to_string(), self.client.clone())
1347    }
1348
1349    /// Get the full interaction resource by ID.
1350    pub async fn get_interaction(&self, id: &str) -> Result<Interaction, Error> {
1351        self.client.get_interaction(id).await
1352    }
1353
1354    /// Start building a content embedding request
1355    pub fn embed_content(&self) -> EmbedBuilder {
1356        EmbedBuilder::new(self.client.clone())
1357    }
1358
1359    /// Start building a batch content generation request
1360    pub fn batch_generate_content(&self) -> BatchBuilder {
1361        BatchBuilder::new(self.client.clone())
1362    }
1363
1364    /// Get a handle to a batch operation by its name.
1365    pub fn get_batch(&self, name: &str) -> BatchHandle {
1366        BatchHandle::new(name.to_string(), self.client.clone())
1367    }
1368
1369    /// Lists batch operations.
1370    ///
1371    /// This method returns a stream that handles pagination automatically.
1372    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    /// Create cached content with a fluent API.
1399    pub fn create_cache(&self) -> CacheBuilder {
1400        CacheBuilder::new(self.client.clone())
1401    }
1402
1403    /// Get a handle to cached content by its name.
1404    pub fn get_cached_content(&self, name: &str) -> CachedContentHandle {
1405        CachedContentHandle::new(name.to_string(), self.client.clone())
1406    }
1407
1408    /// Lists cached contents.
1409    ///
1410    /// This method returns a stream that handles pagination automatically.
1411    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    /// Start building a file resource
1438    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    /// Get a handle to a file by its name.
1443    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    /// Lists files.
1449    ///
1450    /// This method returns a stream that handles pagination automatically.
1451    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    /// Start building a file search store
1478    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    /// Get a handle to a file search store by its name.
1486    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    /// Lists file search stores.
1498    ///
1499    /// This method returns a stream that handles pagination automatically.
1500    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}