genai_rs/
client.rs

1use crate::GenaiError;
2use reqwest::Client as ReqwestClient;
3use std::time::Duration;
4
5/// Logs a request body at debug level, preferring JSON format when possible.
6fn log_request_body<T: std::fmt::Debug + serde::Serialize>(body: &T) {
7    match serde_json::to_string_pretty(body) {
8        Ok(json) => tracing::debug!("Request Body (JSON):\n{json}"),
9        Err(_) => tracing::debug!("Request Body: {body:#?}"),
10    }
11}
12
13/// Logs a response body at debug level, preferring JSON format when possible.
14fn log_response_body<T: std::fmt::Debug + serde::Serialize>(body: &T) {
15    match serde_json::to_string_pretty(body) {
16        Ok(json) => tracing::debug!("Response Body (JSON):\n{json}"),
17        Err(_) => tracing::debug!("Response Body: {body:#?}"),
18    }
19}
20
21/// The main client for interacting with the Google Generative AI API.
22#[derive(Clone)]
23pub struct Client {
24    pub(crate) api_key: String,
25    #[allow(clippy::struct_field_names)]
26    pub(crate) http_client: ReqwestClient,
27}
28
29// Custom Debug implementation that redacts the API key for security.
30// This prevents accidental exposure of credentials in logs, error messages, or debug output.
31impl std::fmt::Debug for Client {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        f.debug_struct("Client")
34            .field("api_key", &"[REDACTED]")
35            .field("http_client", &self.http_client)
36            .finish()
37    }
38}
39
40/// Builder for `Client` instances.
41///
42/// # Example
43///
44/// ```
45/// use genai_rs::Client;
46/// use std::time::Duration;
47///
48/// let client = Client::builder("api_key".to_string())
49///     .with_timeout(Duration::from_secs(120))
50///     .with_connect_timeout(Duration::from_secs(10))
51///     .build()?;
52/// # Ok::<(), genai_rs::GenaiError>(())
53/// ```
54pub struct ClientBuilder {
55    api_key: String,
56    timeout: Option<Duration>,
57    connect_timeout: Option<Duration>,
58}
59
60// Custom Debug implementation that redacts the API key for security.
61impl std::fmt::Debug for ClientBuilder {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        f.debug_struct("ClientBuilder")
64            .field("api_key", &"[REDACTED]")
65            .field("timeout", &self.timeout)
66            .field("connect_timeout", &self.connect_timeout)
67            .finish()
68    }
69}
70
71impl ClientBuilder {
72    /// Sets the total request timeout.
73    ///
74    /// This is the maximum time a request can take from start to finish,
75    /// including connection time, sending the request, and receiving the response.
76    ///
77    /// For LLM requests that may take a long time to generate responses,
78    /// consider setting a longer timeout (e.g., 120-300 seconds).
79    ///
80    /// If not set, requests will wait indefinitely (no timeout).
81    /// Connection-level timeouts like TCP keepalive may still apply at the OS level.
82    ///
83    /// # Example
84    ///
85    /// ```
86    /// use genai_rs::Client;
87    /// use std::time::Duration;
88    ///
89    /// let client = Client::builder("api_key".to_string())
90    ///     .with_timeout(Duration::from_secs(120))
91    ///     .build()?;
92    /// # Ok::<(), genai_rs::GenaiError>(())
93    /// ```
94    #[must_use]
95    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
96        self.timeout = Some(timeout);
97        self
98    }
99
100    /// Sets the connection timeout.
101    ///
102    /// This is the maximum time to wait for establishing a connection to the server.
103    /// A shorter timeout here can help fail fast if the network is unavailable.
104    ///
105    /// If not set, the connection phase will wait indefinitely (no timeout).
106    ///
107    /// # Example
108    ///
109    /// ```
110    /// use genai_rs::Client;
111    /// use std::time::Duration;
112    ///
113    /// let client = Client::builder("api_key".to_string())
114    ///     .with_connect_timeout(Duration::from_secs(10))
115    ///     .build()?;
116    /// # Ok::<(), genai_rs::GenaiError>(())
117    /// ```
118    #[must_use]
119    pub const fn with_connect_timeout(mut self, timeout: Duration) -> Self {
120        self.connect_timeout = Some(timeout);
121        self
122    }
123
124    /// Builds the `Client`.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if the underlying HTTP client cannot be constructed. This should only
129    /// happen in exceptional circumstances such as TLS backend initialization failures.
130    pub fn build(self) -> Result<Client, GenaiError> {
131        let mut builder = ReqwestClient::builder();
132
133        if let Some(timeout) = self.timeout {
134            builder = builder.timeout(timeout);
135        }
136
137        if let Some(connect_timeout) = self.connect_timeout {
138            builder = builder.connect_timeout(connect_timeout);
139        }
140
141        let http_client = builder
142            .build()
143            .map_err(|e| GenaiError::ClientBuild(e.to_string()))?;
144
145        Ok(Client {
146            api_key: self.api_key,
147            http_client,
148        })
149    }
150}
151
152impl Client {
153    /// Creates a new builder for `Client` instances.
154    ///
155    /// # Arguments
156    ///
157    /// * `api_key` - Your Google AI API key.
158    #[must_use]
159    pub const fn builder(api_key: String) -> ClientBuilder {
160        ClientBuilder {
161            api_key,
162            timeout: None,
163            connect_timeout: None,
164        }
165    }
166
167    /// Creates a new `GenAI` client.
168    ///
169    /// # Arguments
170    ///
171    /// * `api_key` - Your Google AI API key.
172    #[must_use]
173    pub fn new(api_key: String) -> Self {
174        Self {
175            api_key,
176            http_client: ReqwestClient::new(),
177        }
178    }
179
180    // --- Interactions API methods ---
181
182    /// Creates a builder for constructing an interaction request.
183    ///
184    /// This provides a fluent interface for building interactions with models or agents.
185    /// Use this method for a more ergonomic API compared to manually constructing
186    /// `InteractionRequest`.
187    ///
188    /// # Examples
189    ///
190    /// ```no_run
191    /// # use genai_rs::Client;
192    /// # #[tokio::main]
193    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
194    /// let client = Client::builder("api_key".to_string()).build()?;
195    ///
196    /// // Simple interaction
197    /// let response = client.interaction()
198    ///     .with_model("gemini-3-flash-preview")
199    ///     .with_text("Hello, world!")
200    ///     .create()
201    ///     .await?;
202    ///
203    /// // Stateful conversation (requires stored interaction)
204    /// let response2 = client.interaction()
205    ///     .with_model("gemini-3-flash-preview")
206    ///     .with_text("What did I just say?")
207    ///     .with_previous_interaction(response.id.as_ref().expect("stored interaction has id"))
208    ///     .create()
209    ///     .await?;
210    /// # Ok(())
211    /// # }
212    /// ```
213    #[must_use]
214    pub fn interaction(&self) -> crate::request_builder::InteractionBuilder<'_> {
215        crate::request_builder::InteractionBuilder::new(self)
216    }
217
218    /// Creates a new interaction using the Gemini Interactions API.
219    ///
220    /// The Interactions API provides a unified interface for working with models and agents,
221    /// with built-in support for stateful conversations, function calling, and long-running tasks.
222    ///
223    /// # Arguments
224    ///
225    /// * `request` - The interaction request with model/agent, input, and optional configuration.
226    ///
227    /// # Errors
228    ///
229    /// Returns an error if:
230    /// - The HTTP request fails
231    /// - Response parsing fails
232    /// - The API returns an error
233    ///
234    /// # Example
235    ///
236    /// ```no_run
237    /// use genai_rs::Client;
238    /// use genai_rs::{InteractionRequest, InteractionInput};
239    ///
240    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
241    /// let client = Client::new("your-api-key".to_string());
242    ///
243    /// let request = InteractionRequest {
244    ///     model: Some("gemini-3-flash-preview".to_string()),
245    ///     agent: None,
246    ///     agent_config: None,
247    ///     input: InteractionInput::Text("Hello, world!".to_string()),
248    ///     previous_interaction_id: None,
249    ///     tools: None,
250    ///     response_modalities: None,
251    ///     response_format: None,
252    ///     response_mime_type: None,
253    ///     generation_config: None,
254    ///     stream: None,
255    ///     background: None,
256    ///     store: None,
257    ///     system_instruction: None,
258    /// };
259    ///
260    /// let response = client.execute(request).await?;
261    /// println!("Interaction ID: {:?}", response.id);
262    /// # Ok(())
263    /// # }
264    /// ```
265    ///
266    /// # Streaming Example
267    ///
268    /// ```no_run
269    /// use genai_rs::{Client, StreamChunk};
270    /// use genai_rs::{InteractionRequest, InteractionInput};
271    /// use futures_util::StreamExt;
272    ///
273    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
274    /// let client = Client::builder("api_key".to_string()).build()?;
275    /// let request = InteractionRequest {
276    ///     model: Some("gemini-3-flash-preview".to_string()),
277    ///     agent: None,
278    ///     agent_config: None,
279    ///     input: InteractionInput::Text("Count to 5".to_string()),
280    ///     previous_interaction_id: None,
281    ///     tools: None,
282    ///     response_modalities: None,
283    ///     response_format: None,
284    ///     response_mime_type: None,
285    ///     generation_config: None,
286    ///     stream: Some(true),
287    ///     background: None,
288    ///     store: None,
289    ///     system_instruction: None,
290    /// };
291    ///
292    /// let mut last_event_id = None;
293    /// let mut stream = client.execute_stream(request);
294    /// while let Some(result) = stream.next().await {
295    ///     let event = result?;
296    ///     last_event_id = event.event_id.clone();  // Track for resume
297    ///     match event.chunk {
298    ///         StreamChunk::Delta(delta) => {
299    ///             if let Some(text) = delta.as_text() {
300    ///                 print!("{}", text);
301    ///             }
302    ///         }
303    ///         StreamChunk::Complete(response) => {
304    ///             println!("\nDone! ID: {:?}", response.id);
305    ///         }
306    ///         _ => {} // Handle unknown future variants
307    ///     }
308    /// }
309    /// # Ok(())
310    /// # }
311    /// ```
312    ///
313    /// # Retry Example
314    ///
315    /// ```no_run
316    /// use genai_rs::Client;
317    /// use std::time::Duration;
318    ///
319    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
320    /// let client = Client::new("api_key".to_string());
321    /// let request = client.interaction()
322    ///     .with_model("gemini-3-flash-preview")
323    ///     .with_text("Hello!")
324    ///     .build()?;
325    ///
326    /// // Retry loop with exponential backoff
327    /// let mut attempts = 0;
328    /// let response = loop {
329    ///     match client.execute(request.clone()).await {
330    ///         Ok(r) => break r,
331    ///         Err(e) if e.is_retryable() && attempts < 3 => {
332    ///             attempts += 1;
333    ///             tokio::time::sleep(Duration::from_millis(100 * 2u64.pow(attempts))).await;
334    ///         }
335    ///         Err(e) => return Err(e.into()),
336    ///     }
337    /// };
338    /// # Ok(())
339    /// # }
340    /// ```
341    #[tracing::instrument(skip(self), fields(model = ?request.model, agent = ?request.agent))]
342    pub async fn execute(
343        &self,
344        request: crate::InteractionRequest,
345    ) -> Result<crate::InteractionResponse, GenaiError> {
346        tracing::debug!("Creating interaction");
347        log_request_body(&request);
348
349        let response = crate::http::interactions::create_interaction(
350            &self.http_client,
351            &self.api_key,
352            request,
353        )
354        .await?;
355
356        log_response_body(&response);
357        tracing::debug!("Interaction created: ID={:?}", response.id);
358
359        Ok(response)
360    }
361
362    /// Executes a pre-built interaction request with streaming.
363    ///
364    /// This is the streaming variant of [`execute()`](Self::execute).
365    ///
366    /// Returns a stream of [`StreamEvent`](crate::StreamEvent) items as they arrive.
367    /// Each event contains:
368    /// - `chunk`: The content (delta or complete response)
369    /// - `event_id`: Optional ID for resuming interrupted streams
370    ///
371    /// # Example
372    ///
373    /// ```no_run
374    /// use genai_rs::{Client, StreamChunk};
375    /// use futures_util::StreamExt;
376    ///
377    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
378    /// let client = Client::new("api_key".to_string());
379    ///
380    /// let request = client.interaction()
381    ///     .with_model("gemini-3-flash-preview")
382    ///     .with_text("Count to 5")
383    ///     .build()?;
384    ///
385    /// let mut stream = client.execute_stream(request);
386    /// while let Some(result) = stream.next().await {
387    ///     let event = result?;
388    ///     match event.chunk {
389    ///         StreamChunk::Delta(delta) => {
390    ///             if let Some(text) = delta.as_text() {
391    ///                 print!("{}", text);
392    ///             }
393    ///         }
394    ///         StreamChunk::Complete(response) => {
395    ///             println!("\nDone!");
396    ///         }
397    ///         _ => {}
398    ///     }
399    /// }
400    /// # Ok(())
401    /// # }
402    /// ```
403    #[tracing::instrument(skip(self), fields(model = ?request.model, agent = ?request.agent))]
404    pub fn execute_stream(
405        &self,
406        request: crate::InteractionRequest,
407    ) -> futures_util::stream::BoxStream<'_, Result<crate::StreamEvent, GenaiError>> {
408        use futures_util::StreamExt;
409
410        tracing::debug!("Creating streaming interaction");
411        log_request_body(&request);
412
413        let stream = crate::http::interactions::create_interaction_stream(
414            &self.http_client,
415            &self.api_key,
416            request,
417        );
418
419        stream
420            .map(move |result| {
421                result.inspect(|event| {
422                    tracing::debug!(
423                        "Received stream event: chunk={:?}, event_id={:?}",
424                        event.chunk,
425                        event.event_id
426                    );
427                })
428            })
429            .boxed()
430    }
431
432    /// Retrieves an existing interaction by its ID.
433    ///
434    /// Useful for checking the status of long-running interactions or agents,
435    /// or for retrieving the full conversation history.
436    ///
437    /// # Arguments
438    ///
439    /// * `interaction_id` - The unique identifier of the interaction to retrieve.
440    ///
441    /// # Errors
442    ///
443    /// Returns an error if:
444    /// - The HTTP request fails
445    /// - Response parsing fails
446    /// - The API returns an error
447    pub async fn get_interaction(
448        &self,
449        interaction_id: &str,
450    ) -> Result<crate::InteractionResponse, GenaiError> {
451        tracing::debug!("Getting interaction: ID={interaction_id}");
452
453        let response = crate::http::interactions::get_interaction(
454            &self.http_client,
455            &self.api_key,
456            interaction_id,
457        )
458        .await?;
459
460        log_response_body(&response);
461        tracing::debug!("Retrieved interaction: status={:?}", response.status);
462
463        Ok(response)
464    }
465
466    /// Retrieves an existing interaction by its ID with streaming.
467    ///
468    /// Returns a stream of events for the interaction. This is useful for:
469    /// - Resuming an interrupted stream using `last_event_id`
470    /// - Streaming a long-running interaction's progress (e.g., deep research)
471    ///
472    /// Each event includes an `event_id` that can be used to resume the stream
473    /// from that point if the connection is interrupted.
474    ///
475    /// # Arguments
476    ///
477    /// * `interaction_id` - The unique identifier of the interaction to stream.
478    /// * `last_event_id` - Optional event ID to resume from. Pass the last received
479    ///   event's `event_id` to continue from where you left off.
480    ///
481    /// # Returns
482    /// A boxed stream that yields `StreamEvent` items.
483    ///
484    /// # Example
485    /// ```no_run
486    /// use genai_rs::{Client, StreamChunk};
487    /// use futures_util::StreamExt;
488    ///
489    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
490    /// let client = Client::builder("api_key".to_string()).build()?;
491    /// let interaction_id = "some-interaction-id";
492    ///
493    /// // Resume a stream from a previous event
494    /// let last_event_id = Some("evt_abc123");
495    /// let mut stream = client.get_interaction_stream(interaction_id, last_event_id);
496    ///
497    /// while let Some(result) = stream.next().await {
498    ///     let event = result?;
499    ///     println!("Event ID: {:?}", event.event_id);
500    ///     match event.chunk {
501    ///         StreamChunk::Delta(delta) => {
502    ///             if let Some(text) = delta.as_text() {
503    ///                 print!("{}", text);
504    ///             }
505    ///         }
506    ///         StreamChunk::Complete(response) => {
507    ///             println!("\nDone! Status: {:?}", response.status);
508    ///         }
509    ///         _ => {}
510    ///     }
511    /// }
512    /// # Ok(())
513    /// # }
514    /// ```
515    pub fn get_interaction_stream<'a>(
516        &'a self,
517        interaction_id: &'a str,
518        last_event_id: Option<&'a str>,
519    ) -> futures_util::stream::BoxStream<'a, Result<crate::StreamEvent, GenaiError>> {
520        use futures_util::StreamExt;
521
522        tracing::debug!(
523            "Getting interaction stream: ID={}, resume_from={:?}",
524            interaction_id,
525            last_event_id
526        );
527
528        let stream = crate::http::interactions::get_interaction_stream(
529            &self.http_client,
530            &self.api_key,
531            interaction_id,
532            last_event_id,
533        );
534
535        stream
536            .map(move |result| {
537                result.inspect(|event| {
538                    tracing::debug!(
539                        "Received stream event: chunk={:?}, event_id={:?}",
540                        event.chunk,
541                        event.event_id
542                    );
543                })
544            })
545            .boxed()
546    }
547
548    /// Deletes an interaction by its ID.
549    ///
550    /// Removes the interaction from the server, freeing up storage and making it
551    /// unavailable for future reference via `previous_interaction_id`.
552    ///
553    /// # Arguments
554    ///
555    /// * `interaction_id` - The unique identifier of the interaction to delete.
556    ///
557    /// # Errors
558    ///
559    /// Returns an error if:
560    /// - The HTTP request fails
561    /// - The API returns an error
562    pub async fn delete_interaction(&self, interaction_id: &str) -> Result<(), GenaiError> {
563        tracing::debug!("Deleting interaction: ID={interaction_id}");
564
565        crate::http::interactions::delete_interaction(
566            &self.http_client,
567            &self.api_key,
568            interaction_id,
569        )
570        .await?;
571
572        tracing::debug!("Interaction deleted successfully");
573
574        Ok(())
575    }
576
577    /// Cancels an in-progress background interaction.
578    ///
579    /// Only applicable to interactions created with `background: true` that are
580    /// still in `InProgress` status. Returns the updated interaction with
581    /// status `Cancelled`.
582    ///
583    /// This is useful for:
584    /// - Halting long-running agent tasks (e.g., deep-research) when requirements change
585    /// - Cost control by stopping interactions consuming significant tokens
586    /// - Implementing timeout handling in application logic
587    /// - Supporting user-initiated cancellation in UIs
588    ///
589    /// # Arguments
590    ///
591    /// * `interaction_id` - The unique identifier of the interaction to cancel.
592    ///
593    /// # Errors
594    ///
595    /// Returns an error if:
596    /// - The interaction doesn't exist
597    /// - The interaction is not in a cancellable state (not background or already complete)
598    /// - The HTTP request fails
599    /// - The API returns an error
600    ///
601    /// # Example
602    ///
603    /// ```no_run
604    /// use genai_rs::{Client, InteractionStatus};
605    ///
606    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
607    /// let client = Client::new("your-api-key".to_string());
608    ///
609    /// // Start a background agent interaction
610    /// let response = client.interaction()
611    ///     .with_agent("deep-research-pro-preview-12-2025")
612    ///     .with_text("Research AI safety")
613    ///     .with_background(true)
614    ///     .with_store_enabled()
615    ///     .create()
616    ///     .await?;
617    ///
618    /// let interaction_id = response.id.as_ref().expect("stored interaction has id");
619    ///
620    /// // Later, cancel if still in progress
621    /// if response.status == InteractionStatus::InProgress {
622    ///     let cancelled = client.cancel_interaction(interaction_id).await?;
623    ///     assert_eq!(cancelled.status, InteractionStatus::Cancelled);
624    ///     println!("Interaction cancelled");
625    /// }
626    /// # Ok(())
627    /// # }
628    /// ```
629    pub async fn cancel_interaction(
630        &self,
631        interaction_id: &str,
632    ) -> Result<crate::InteractionResponse, GenaiError> {
633        tracing::debug!("Cancelling interaction: ID={interaction_id}");
634
635        let response = crate::http::interactions::cancel_interaction(
636            &self.http_client,
637            &self.api_key,
638            interaction_id,
639        )
640        .await?;
641
642        log_response_body(&response);
643        tracing::debug!("Interaction cancelled: status={:?}", response.status);
644
645        Ok(response)
646    }
647
648    // --- Files API methods ---
649
650    /// Uploads a file from a path to the Files API.
651    ///
652    /// Files are stored for 48 hours and can be referenced in interactions by their URI.
653    /// This is more efficient than inline base64 encoding for large files or files
654    /// that will be used across multiple interactions.
655    ///
656    /// # Arguments
657    ///
658    /// * `path` - Path to the file to upload
659    ///
660    /// # Errors
661    ///
662    /// Returns an error if:
663    /// - The file cannot be read
664    /// - The MIME type cannot be determined
665    /// - The upload fails
666    ///
667    /// # Example
668    ///
669    /// ```no_run
670    /// use genai_rs::{Client, Content};
671    ///
672    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
673    /// let client = Client::new("api-key".to_string());
674    ///
675    /// // Upload a video file
676    /// let file = client.upload_file("video.mp4").await?;
677    /// println!("Uploaded: {} -> {}", file.name, file.uri);
678    ///
679    /// // Use in interaction
680    /// let response = client.interaction()
681    ///     .with_model("gemini-3-flash-preview")
682    ///     .with_content(vec![
683    ///         Content::text("Describe this video"),
684    ///         Content::from_file(&file),
685    ///     ])
686    ///     .create()
687    ///     .await?;
688    /// # Ok(())
689    /// # }
690    /// ```
691    pub async fn upload_file(
692        &self,
693        path: impl AsRef<std::path::Path>,
694    ) -> Result<crate::FileMetadata, GenaiError> {
695        let path = path.as_ref();
696
697        // Read file contents
698        let file_data = tokio::fs::read(path).await.map_err(|e| {
699            tracing::warn!("Failed to read file '{}': {}", path.display(), e);
700            GenaiError::InvalidInput(format!("Failed to read file '{}': {}", path.display(), e))
701        })?;
702
703        // Detect MIME type from extension
704        let mime_type = crate::multimodal::detect_mime_type(path).ok_or_else(|| {
705            tracing::warn!(
706                "Could not determine MIME type for '{}' - unknown extension",
707                path.display()
708            );
709            GenaiError::InvalidInput(format!(
710                "Could not determine MIME type for '{}'. Please use upload_file_with_mime() to specify explicitly.",
711                path.display()
712            ))
713        })?;
714
715        // Use filename as display name
716        let display_name = path
717            .file_name()
718            .and_then(|s| s.to_str())
719            .map(|s| s.to_string());
720
721        tracing::debug!(
722            "Uploading file: path={}, size={} bytes, mime_type={}",
723            path.display(),
724            file_data.len(),
725            mime_type
726        );
727
728        crate::http::files::upload_file(
729            &self.http_client,
730            &self.api_key,
731            file_data,
732            mime_type,
733            display_name.as_deref(),
734        )
735        .await
736    }
737
738    /// Uploads a file with an explicit MIME type.
739    ///
740    /// Use this when automatic MIME type detection isn't suitable.
741    ///
742    /// # Arguments
743    ///
744    /// * `path` - Path to the file to upload
745    /// * `mime_type` - MIME type of the file (e.g., "video/mp4")
746    ///
747    /// # Example
748    ///
749    /// ```no_run
750    /// use genai_rs::Client;
751    ///
752    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
753    /// let client = Client::new("api-key".to_string());
754    ///
755    /// let file = client.upload_file_with_mime("data.bin", "application/octet-stream").await?;
756    /// # Ok(())
757    /// # }
758    /// ```
759    pub async fn upload_file_with_mime(
760        &self,
761        path: impl AsRef<std::path::Path>,
762        mime_type: &str,
763    ) -> Result<crate::FileMetadata, GenaiError> {
764        let path = path.as_ref();
765
766        let file_data = tokio::fs::read(path).await.map_err(|e| {
767            tracing::warn!("Failed to read file '{}': {}", path.display(), e);
768            GenaiError::InvalidInput(format!("Failed to read file '{}': {}", path.display(), e))
769        })?;
770
771        let display_name = path
772            .file_name()
773            .and_then(|s| s.to_str())
774            .map(|s| s.to_string());
775
776        tracing::debug!(
777            "Uploading file: path={}, size={} bytes, mime_type={}",
778            path.display(),
779            file_data.len(),
780            mime_type
781        );
782
783        crate::http::files::upload_file(
784            &self.http_client,
785            &self.api_key,
786            file_data,
787            mime_type,
788            display_name.as_deref(),
789        )
790        .await
791    }
792
793    /// Uploads file bytes directly with a specified MIME type.
794    ///
795    /// Use this when you already have file contents in memory.
796    ///
797    /// # Arguments
798    ///
799    /// * `data` - File contents as bytes
800    /// * `mime_type` - MIME type of the file
801    /// * `display_name` - Optional display name for the file
802    ///
803    /// # Example
804    ///
805    /// ```no_run
806    /// use genai_rs::Client;
807    ///
808    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
809    /// let client = Client::new("api-key".to_string());
810    ///
811    /// // Upload bytes from memory
812    /// let video_bytes = std::fs::read("video.mp4")?;
813    /// let file = client.upload_file_bytes(video_bytes, "video/mp4", Some("my-video")).await?;
814    /// # Ok(())
815    /// # }
816    /// ```
817    pub async fn upload_file_bytes(
818        &self,
819        data: Vec<u8>,
820        mime_type: &str,
821        display_name: Option<&str>,
822    ) -> Result<crate::FileMetadata, GenaiError> {
823        tracing::debug!(
824            "Uploading file bytes: size={} bytes, mime_type={}, display_name={:?}",
825            data.len(),
826            mime_type,
827            display_name
828        );
829
830        crate::http::files::upload_file(
831            &self.http_client,
832            &self.api_key,
833            data,
834            mime_type,
835            display_name,
836        )
837        .await
838    }
839
840    /// Gets metadata for an uploaded file.
841    ///
842    /// Use this to check the processing status of a recently uploaded file.
843    ///
844    /// # Arguments
845    ///
846    /// * `file_name` - The resource name of the file (e.g., "files/abc123")
847    ///
848    /// # Example
849    ///
850    /// ```no_run
851    /// use genai_rs::Client;
852    ///
853    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
854    /// let client = Client::new("api-key".to_string());
855    ///
856    /// let file = client.get_file("files/abc123").await?;
857    /// if file.is_active() {
858    ///     println!("File is ready to use");
859    /// } else if file.is_processing() {
860    ///     println!("File is still processing...");
861    /// }
862    /// # Ok(())
863    /// # }
864    /// ```
865    pub async fn get_file(&self, file_name: &str) -> Result<crate::FileMetadata, GenaiError> {
866        crate::http::files::get_file(&self.http_client, &self.api_key, file_name).await
867    }
868
869    /// Lists all uploaded files.
870    ///
871    /// # Example
872    ///
873    /// ```no_run
874    /// use genai_rs::Client;
875    ///
876    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
877    /// let client = Client::new("api-key".to_string());
878    ///
879    /// let response = client.list_files(None, None).await?;
880    /// for file in response.files {
881    ///     println!("{}: {} ({})", file.name, file.display_name.as_deref().unwrap_or(""), file.mime_type);
882    /// }
883    /// # Ok(())
884    /// # }
885    /// ```
886    pub async fn list_files(
887        &self,
888        page_size: Option<u32>,
889        page_token: Option<&str>,
890    ) -> Result<crate::ListFilesResponse, GenaiError> {
891        crate::http::files::list_files(&self.http_client, &self.api_key, page_size, page_token)
892            .await
893    }
894
895    /// Deletes an uploaded file.
896    ///
897    /// # Arguments
898    ///
899    /// * `file_name` - The resource name of the file to delete (e.g., "files/abc123")
900    ///
901    /// # Example
902    ///
903    /// ```no_run
904    /// use genai_rs::Client;
905    ///
906    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
907    /// let client = Client::new("api-key".to_string());
908    ///
909    /// // Upload, use, then delete
910    /// let file = client.upload_file("video.mp4").await?;
911    /// // ... use in interactions ...
912    /// client.delete_file(&file.name).await?;
913    /// # Ok(())
914    /// # }
915    /// ```
916    pub async fn delete_file(&self, file_name: &str) -> Result<(), GenaiError> {
917        crate::http::files::delete_file(&self.http_client, &self.api_key, file_name).await
918    }
919
920    /// Uploads a file using chunked transfer to minimize memory usage.
921    ///
922    /// Unlike `upload_file`, this method streams the file from disk in chunks,
923    /// never loading the entire file into memory. This is ideal for large files
924    /// (500MB-2GB) or memory-constrained environments.
925    ///
926    /// # Arguments
927    ///
928    /// * `path` - Path to the file to upload
929    ///
930    /// # Returns
931    ///
932    /// Returns a tuple of:
933    /// - `FileMetadata`: The uploaded file's metadata
934    /// - `ResumableUpload`: A handle that can be used to resume if the upload is interrupted
935    ///
936    /// # Memory Usage
937    ///
938    /// This method uses approximately 8MB of memory for buffering, regardless of
939    /// the file size. A 2GB file uses the same memory as a 10MB file.
940    ///
941    /// # Errors
942    ///
943    /// Returns an error if:
944    /// - The file cannot be read
945    /// - The MIME type cannot be determined
946    /// - The upload fails
947    ///
948    /// # Example
949    ///
950    /// ```no_run
951    /// use genai_rs::{Client, Content};
952    ///
953    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
954    /// let client = Client::new("api-key".to_string());
955    ///
956    /// // Upload a large video file without loading it all into memory
957    /// let (file, _upload_handle) = client.upload_file_chunked("large_video.mp4").await?;
958    /// println!("Uploaded: {} -> {}", file.name, file.uri);
959    ///
960    /// // Use in interaction
961    /// let response = client.interaction()
962    ///     .with_model("gemini-3-flash-preview")
963    ///     .with_content(vec![
964    ///         Content::text("Describe this video"),
965    ///         Content::from_file(&file),
966    ///     ])
967    ///     .create()
968    ///     .await?;
969    /// # Ok(())
970    /// # }
971    /// ```
972    pub async fn upload_file_chunked(
973        &self,
974        path: impl AsRef<std::path::Path>,
975    ) -> Result<(crate::FileMetadata, crate::ResumableUpload), GenaiError> {
976        let path = path.as_ref();
977
978        // Detect MIME type from extension
979        let mime_type = crate::multimodal::detect_mime_type(path).ok_or_else(|| {
980            tracing::warn!(
981                "Could not determine MIME type for '{}' - unknown extension",
982                path.display()
983            );
984            GenaiError::InvalidInput(format!(
985                "Could not determine MIME type for '{}'. Please use upload_file_chunked_with_mime() to specify explicitly.",
986                path.display()
987            ))
988        })?;
989
990        // Use filename as display name
991        let display_name = path
992            .file_name()
993            .and_then(|s| s.to_str())
994            .map(|s| s.to_string());
995
996        tracing::debug!(
997            "Chunked upload: path={}, mime_type={}",
998            path.display(),
999            mime_type
1000        );
1001
1002        crate::http::files::upload_file_chunked(
1003            &self.http_client,
1004            &self.api_key,
1005            path,
1006            mime_type,
1007            display_name.as_deref(),
1008        )
1009        .await
1010    }
1011
1012    /// Uploads a file using chunked transfer with an explicit MIME type.
1013    ///
1014    /// Use this when automatic MIME type detection isn't suitable.
1015    ///
1016    /// # Arguments
1017    ///
1018    /// * `path` - Path to the file to upload
1019    /// * `mime_type` - MIME type of the file (e.g., "video/mp4")
1020    ///
1021    /// # Example
1022    ///
1023    /// ```no_run
1024    /// use genai_rs::Client;
1025    ///
1026    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1027    /// let client = Client::new("api-key".to_string());
1028    ///
1029    /// let (file, _) = client.upload_file_chunked_with_mime(
1030    ///     "data.bin",
1031    ///     "application/octet-stream"
1032    /// ).await?;
1033    /// # Ok(())
1034    /// # }
1035    /// ```
1036    pub async fn upload_file_chunked_with_mime(
1037        &self,
1038        path: impl AsRef<std::path::Path>,
1039        mime_type: &str,
1040    ) -> Result<(crate::FileMetadata, crate::ResumableUpload), GenaiError> {
1041        let path = path.as_ref();
1042
1043        let display_name = path
1044            .file_name()
1045            .and_then(|s| s.to_str())
1046            .map(|s| s.to_string());
1047
1048        tracing::debug!(
1049            "Chunked upload: path={}, mime_type={}",
1050            path.display(),
1051            mime_type
1052        );
1053
1054        crate::http::files::upload_file_chunked(
1055            &self.http_client,
1056            &self.api_key,
1057            path,
1058            mime_type,
1059            display_name.as_deref(),
1060        )
1061        .await
1062    }
1063
1064    /// Uploads a file using chunked transfer with a custom chunk size.
1065    ///
1066    /// This is the same as `upload_file_chunked_with_mime` but allows
1067    /// specifying the chunk size for streaming. Larger chunks are more
1068    /// efficient for fast networks, while smaller chunks use less memory.
1069    ///
1070    /// # Arguments
1071    ///
1072    /// * `path` - Path to the file to upload
1073    /// * `mime_type` - MIME type of the file
1074    /// * `chunk_size` - Size of chunks to stream in bytes (default: 8MB)
1075    ///
1076    /// # Example
1077    ///
1078    /// ```no_run
1079    /// use genai_rs::Client;
1080    ///
1081    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1082    /// let client = Client::new("api-key".to_string());
1083    ///
1084    /// // Use 16MB chunks for faster upload on a fast network
1085    /// let chunk_size = 16 * 1024 * 1024;
1086    /// let (file, _) = client.upload_file_chunked_with_options(
1087    ///     "large_video.mp4",
1088    ///     "video/mp4",
1089    ///     chunk_size
1090    /// ).await?;
1091    /// # Ok(())
1092    /// # }
1093    /// ```
1094    pub async fn upload_file_chunked_with_options(
1095        &self,
1096        path: impl AsRef<std::path::Path>,
1097        mime_type: &str,
1098        chunk_size: usize,
1099    ) -> Result<(crate::FileMetadata, crate::ResumableUpload), GenaiError> {
1100        let path = path.as_ref();
1101
1102        let display_name = path
1103            .file_name()
1104            .and_then(|s| s.to_str())
1105            .map(|s| s.to_string());
1106
1107        tracing::debug!(
1108            "Chunked upload: path={}, mime_type={}, chunk_size={}",
1109            path.display(),
1110            mime_type,
1111            chunk_size
1112        );
1113
1114        crate::http::files::upload_file_chunked_with_chunk_size(
1115            &self.http_client,
1116            &self.api_key,
1117            path,
1118            mime_type,
1119            display_name.as_deref(),
1120            chunk_size,
1121        )
1122        .await
1123    }
1124
1125    /// Waits for a file to finish processing.
1126    ///
1127    /// Some files (especially videos) require processing before they can be used.
1128    /// This method polls the file status until it becomes active or fails.
1129    ///
1130    /// # Arguments
1131    ///
1132    /// * `file` - The file metadata to wait for
1133    /// * `poll_interval` - How often to check the status
1134    /// * `timeout` - Maximum time to wait
1135    ///
1136    /// # Returns
1137    ///
1138    /// Returns the updated file metadata when processing completes.
1139    ///
1140    /// # Errors
1141    ///
1142    /// Returns an error if:
1143    /// - The file processing fails
1144    /// - The timeout is exceeded
1145    ///
1146    /// # Example
1147    ///
1148    /// ```no_run
1149    /// use genai_rs::Client;
1150    /// use std::time::Duration;
1151    ///
1152    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1153    /// let client = Client::new("api-key".to_string());
1154    ///
1155    /// let file = client.upload_file("large_video.mp4").await?;
1156    ///
1157    /// // Wait for processing to complete
1158    /// let ready_file = client.wait_for_file_ready(
1159    ///     &file,
1160    ///     Duration::from_secs(2),
1161    ///     Duration::from_secs(120)
1162    /// ).await?;
1163    ///
1164    /// println!("File ready: {}", ready_file.uri);
1165    /// # Ok(())
1166    /// # }
1167    /// ```
1168    pub async fn wait_for_file_ready(
1169        &self,
1170        file: &crate::FileMetadata,
1171        poll_interval: std::time::Duration,
1172        timeout: std::time::Duration,
1173    ) -> Result<crate::FileMetadata, GenaiError> {
1174        use std::time::Instant;
1175
1176        let start = Instant::now();
1177
1178        loop {
1179            let current = self.get_file(&file.name).await?;
1180
1181            if current.is_active() {
1182                return Ok(current);
1183            }
1184
1185            if current.is_failed() {
1186                let error_code = current.error.as_ref().and_then(|e| e.code);
1187                let error_msg = current
1188                    .error
1189                    .as_ref()
1190                    .and_then(|e| e.message.as_deref())
1191                    .unwrap_or("File processing failed without details");
1192
1193                tracing::error!(
1194                    "File '{}' processing failed: code={:?}, message={}",
1195                    file.name,
1196                    error_code,
1197                    error_msg
1198                );
1199
1200                // Use Api error since this is a server-side processing failure
1201                return Err(GenaiError::Api {
1202                    status_code: error_code.map_or(500, |c| c as u16),
1203                    message: format!("File processing failed: {}", error_msg),
1204                    request_id: None,
1205                    retry_after: None,
1206                });
1207            }
1208
1209            // Log unknown states per Evergreen logging strategy
1210            if let Some(state) = &current.state
1211                && state.is_unknown()
1212            {
1213                tracing::warn!(
1214                    "File '{}' is in unknown state {:?}, continuing to poll. \
1215                     This may indicate API evolution - consider updating genai-rs.",
1216                    file.name,
1217                    state
1218                );
1219            }
1220
1221            if start.elapsed() > timeout {
1222                // Use Internal error since this is an operational issue, not invalid input
1223                let state_info = current
1224                    .state
1225                    .as_ref()
1226                    .map(|s| format!("{:?}", s))
1227                    .unwrap_or_else(|| "unknown".to_string());
1228                return Err(GenaiError::Internal(format!(
1229                    "Timeout waiting for file '{}' to be ready (waited {:?}, last state: {}). \
1230                     The file may still be processing - try again with a longer timeout.",
1231                    file.name,
1232                    start.elapsed(),
1233                    state_info
1234                )));
1235            }
1236
1237            tracing::debug!(
1238                "File '{}' still processing, waiting {:?}...",
1239                file.name,
1240                poll_interval
1241            );
1242            tokio::time::sleep(poll_interval).await;
1243        }
1244    }
1245}
1246
1247#[cfg(test)]
1248mod tests {
1249    use super::*;
1250
1251    #[test]
1252    fn test_client_builder_default() {
1253        let client = Client::builder("test_key".to_string()).build().unwrap();
1254        assert_eq!(client.api_key, "test_key");
1255    }
1256
1257    #[test]
1258    fn test_client_builder_with_timeout() {
1259        let client = Client::builder("test_key".to_string())
1260            .with_timeout(Duration::from_secs(120))
1261            .build()
1262            .unwrap();
1263        assert_eq!(client.api_key, "test_key");
1264        // Note: We can't easily inspect the reqwest client's timeout,
1265        // but this test verifies the builder chain works
1266    }
1267
1268    #[test]
1269    fn test_client_builder_with_connect_timeout() {
1270        let client = Client::builder("test_key".to_string())
1271            .with_connect_timeout(Duration::from_secs(10))
1272            .build()
1273            .unwrap();
1274        assert_eq!(client.api_key, "test_key");
1275    }
1276
1277    #[test]
1278    fn test_client_builder_with_both_timeouts() {
1279        let client = Client::builder("test_key".to_string())
1280            .with_timeout(Duration::from_secs(120))
1281            .with_connect_timeout(Duration::from_secs(10))
1282            .build()
1283            .unwrap();
1284        assert_eq!(client.api_key, "test_key");
1285    }
1286
1287    #[test]
1288    fn test_client_new() {
1289        let client = Client::new("test_key".to_string());
1290        assert_eq!(client.api_key, "test_key");
1291    }
1292
1293    #[test]
1294    fn test_client_debug_redacts_api_key() {
1295        let client = Client::new("super_secret_api_key_12345".to_string());
1296        let debug_output = format!("{:?}", client);
1297
1298        // API key should NOT appear in debug output
1299        assert!(
1300            !debug_output.contains("super_secret_api_key_12345"),
1301            "API key was exposed in debug output: {}",
1302            debug_output
1303        );
1304        // Should show [REDACTED] instead
1305        assert!(
1306            debug_output.contains("[REDACTED]"),
1307            "Debug output should contain [REDACTED]: {}",
1308            debug_output
1309        );
1310    }
1311
1312    #[test]
1313    fn test_client_builder_returns_result() {
1314        let result = Client::builder("test_key".to_string()).build();
1315        assert!(result.is_ok());
1316    }
1317
1318    #[test]
1319    fn test_client_builder_debug_redacts_api_key() {
1320        let builder = Client::builder("another_secret_key_67890".to_string())
1321            .with_timeout(Duration::from_secs(60));
1322        let debug_output = format!("{:?}", builder);
1323
1324        // API key should NOT appear in debug output
1325        assert!(
1326            !debug_output.contains("another_secret_key_67890"),
1327            "API key was exposed in builder debug output: {}",
1328            debug_output
1329        );
1330        // Should show [REDACTED] instead
1331        assert!(
1332            debug_output.contains("[REDACTED]"),
1333            "Builder debug output should contain [REDACTED]: {}",
1334            debug_output
1335        );
1336    }
1337
1338    #[tokio::test]
1339    async fn test_upload_file_unknown_extension_error() {
1340        let client = Client::new("test_key".to_string());
1341
1342        // Create a temp file with an unknown extension
1343        let temp_dir = tempfile::tempdir().unwrap();
1344        let file_path = temp_dir.path().join("data.xyz");
1345        std::fs::write(&file_path, b"test data").unwrap();
1346
1347        // upload_file should fail with InvalidInput for unknown MIME type
1348        let result = client.upload_file(&file_path).await;
1349        assert!(result.is_err(), "Should fail for unknown extension");
1350
1351        let err = result.unwrap_err();
1352        let err_string = err.to_string();
1353        assert!(
1354            err_string.contains("Could not determine MIME type"),
1355            "Error should mention MIME type issue: {}",
1356            err_string
1357        );
1358        assert!(
1359            err_string.contains("data.xyz"),
1360            "Error should include filename: {}",
1361            err_string
1362        );
1363    }
1364
1365    #[tokio::test]
1366    async fn test_upload_file_nonexistent_file_error() {
1367        let client = Client::new("test_key".to_string());
1368
1369        // Try to upload a file that doesn't exist
1370        let result = client.upload_file("/nonexistent/path/to/file.txt").await;
1371        assert!(result.is_err(), "Should fail for nonexistent file");
1372
1373        let err = result.unwrap_err();
1374        let err_string = err.to_string();
1375        assert!(
1376            err_string.contains("Failed to read file"),
1377            "Error should mention file read failure: {}",
1378            err_string
1379        );
1380    }
1381
1382    #[tokio::test]
1383    async fn test_upload_file_bytes_empty_file_error() {
1384        let client = Client::new("test_key".to_string());
1385
1386        // Try to upload empty bytes
1387        let result = client
1388            .upload_file_bytes(Vec::new(), "text/plain", Some("empty.txt"))
1389            .await;
1390        assert!(result.is_err(), "Should fail for empty file");
1391
1392        let err = result.unwrap_err();
1393        let err_string = err.to_string();
1394        assert!(
1395            err_string.contains("Cannot upload empty file"),
1396            "Error should mention empty file: {}",
1397            err_string
1398        );
1399    }
1400
1401    #[tokio::test]
1402    async fn test_upload_file_bytes_validates_before_network() {
1403        // This test verifies that validation happens before any network call
1404        // by using an invalid API key - if we reach the network, we'd get auth error
1405        let client = Client::new("invalid_key".to_string());
1406
1407        // Empty file should fail with validation error, not auth error
1408        let result = client
1409            .upload_file_bytes(Vec::new(), "text/plain", None)
1410            .await;
1411        assert!(result.is_err());
1412        let err_string = result.unwrap_err().to_string();
1413        assert!(
1414            err_string.contains("Cannot upload empty file"),
1415            "Should fail validation before hitting network: {}",
1416            err_string
1417        );
1418    }
1419}