Skip to main content

rig_core/providers/gemini/
client.rs

1use crate::client::{self, ApiKey, DebugExt, Provider, ProviderBuilder, Transport};
2use crate::http_client::{self};
3use crate::providers::gemini::model_listing::{GeminiInteractionsModelLister, GeminiModelLister};
4use serde::Deserialize;
5use std::fmt::Debug;
6
7// ================================================================
8// Google Gemini Client
9// ================================================================
10const GEMINI_API_BASE_URL: &str = "https://generativelanguage.googleapis.com";
11
12/// Provider extension for the Gemini GenerateContent API.
13#[derive(Debug, Default, Clone)]
14pub struct GeminiExt {
15    api_key: String,
16}
17
18/// Builder marker for the Gemini GenerateContent client.
19#[derive(Debug, Default, Clone)]
20pub struct GeminiBuilder;
21
22/// Provider extension for the Gemini Interactions API.
23#[derive(Debug, Default, Clone)]
24pub struct GeminiInteractionsExt {
25    api_key: String,
26}
27
28/// Builder marker for the Gemini Interactions client.
29#[derive(Debug, Default, Clone)]
30pub struct GeminiInteractionsBuilder;
31
32/// Wrapper type for Gemini API keys.
33pub struct GeminiApiKey(String);
34
35impl<S> From<S> for GeminiApiKey
36where
37    S: Into<String>,
38{
39    fn from(value: S) -> Self {
40        Self(value.into())
41    }
42}
43
44/// Gemini GenerateContent client.
45pub type Client<H = reqwest::Client> = client::Client<GeminiExt, H>;
46/// Builder for the Gemini GenerateContent client.
47pub type ClientBuilder<H = crate::markers::Missing> =
48    client::ClientBuilder<GeminiBuilder, GeminiApiKey, H>;
49/// Gemini Interactions API client.
50pub type InteractionsClient<H = reqwest::Client> = client::Client<GeminiInteractionsExt, H>;
51
52impl ApiKey for GeminiApiKey {}
53
54impl DebugExt for GeminiExt {
55    fn fields(&self) -> impl Iterator<Item = (&'static str, &dyn Debug)> {
56        std::iter::once(("api_key", (&"******") as &dyn Debug))
57    }
58}
59
60impl DebugExt for GeminiInteractionsExt {
61    fn fields(&self) -> impl Iterator<Item = (&'static str, &dyn Debug)> {
62        std::iter::once(("api_key", (&"******") as &dyn Debug))
63    }
64}
65
66impl Provider for GeminiExt {
67    type Builder = GeminiBuilder;
68
69    const VERIFY_PATH: &'static str = "/v1beta/models";
70
71    fn build_uri(&self, base_url: &str, path: &str, transport: Transport) -> String {
72        let trimmed = path.trim_start_matches('/');
73        let separator = if trimmed.contains('?') { "&" } else { "?" };
74
75        match transport {
76            Transport::Sse => format!(
77                "{base_url}/{trimmed}{separator}alt=sse&key={}",
78                self.api_key
79            ),
80            _ => format!("{base_url}/{trimmed}{separator}key={}", self.api_key),
81        }
82    }
83}
84
85impl Provider for GeminiInteractionsExt {
86    type Builder = GeminiInteractionsBuilder;
87
88    const VERIFY_PATH: &'static str = "/v1beta/models";
89
90    fn build_uri(&self, base_url: &str, path: &str, transport: Transport) -> String {
91        let trimmed = path.trim_start_matches('/');
92        match transport {
93            Transport::Sse => {
94                if trimmed.contains('?') {
95                    format!("{}/{}&alt=sse", base_url, trimmed)
96                } else {
97                    format!("{}/{}?alt=sse", base_url, trimmed)
98                }
99            }
100            _ => format!("{}/{}", base_url, trimmed),
101        }
102    }
103
104    fn with_custom(&self, req: http_client::Builder) -> http_client::Result<http_client::Builder> {
105        Ok(req.header("x-goog-api-key", self.api_key.clone()))
106    }
107}
108
109client::impl_capabilities!(
110    GeminiExt,
111    completion = super::completion::CompletionModel<H>,
112    embeddings = super::embedding::EmbeddingModel<H>,
113    transcription = super::transcription::TranscriptionModel<H>,
114    model_listing = GeminiModelLister<H>,
115    image_generation = super::image_generation::ImageGenerationModel<H>,
116);
117
118client::impl_capabilities!(
119    GeminiInteractionsExt,
120    completion = super::interactions_api::InteractionsCompletionModel<H>,
121    embeddings = super::embedding::EmbeddingModel<H>,
122    transcription = super::transcription::TranscriptionModel<H>,
123    model_listing = GeminiInteractionsModelLister<H>,
124);
125
126impl ProviderBuilder for GeminiBuilder {
127    type Extension<H>
128        = GeminiExt
129    where
130        H: http_client::HttpClientExt;
131    type ApiKey = GeminiApiKey;
132
133    const BASE_URL: &'static str = GEMINI_API_BASE_URL;
134
135    fn build<H>(
136        builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
137    ) -> http_client::Result<Self::Extension<H>>
138    where
139        H: http_client::HttpClientExt,
140    {
141        Ok(GeminiExt {
142            api_key: builder.get_api_key().0.clone(),
143        })
144    }
145}
146
147impl ProviderBuilder for GeminiInteractionsBuilder {
148    type Extension<H>
149        = GeminiInteractionsExt
150    where
151        H: http_client::HttpClientExt;
152    type ApiKey = GeminiApiKey;
153
154    const BASE_URL: &'static str = GEMINI_API_BASE_URL;
155
156    fn build<H>(
157        builder: &client::ClientBuilder<Self, Self::ApiKey, H>,
158    ) -> http_client::Result<Self::Extension<H>>
159    where
160        H: http_client::HttpClientExt,
161    {
162        Ok(GeminiInteractionsExt {
163            api_key: builder.get_api_key().0.clone(),
164        })
165    }
166}
167
168client::impl_provider_client!(Client, input = GeminiApiKey, api_key_env = "GEMINI_API_KEY",);
169client::impl_provider_client!(
170    InteractionsClient,
171    input = GeminiApiKey,
172    api_key_env = "GEMINI_API_KEY",
173);
174
175impl<H> Client<H> {
176    /// Create an Interactions API client from this GenerateContent client.
177    pub fn interactions_api(self) -> InteractionsClient<H> {
178        let api_key = self.ext().api_key.clone();
179        self.with_ext(GeminiInteractionsExt { api_key })
180    }
181}
182
183impl<H> InteractionsClient<H> {
184    /// Create a GenerateContent API client from this Interactions client.
185    pub fn generate_content_api(self) -> Client<H> {
186        let api_key = self.ext().api_key.clone();
187        self.with_ext(GeminiExt { api_key })
188    }
189}
190
191/// Error response payload returned by Gemini.
192#[derive(Debug, Deserialize)]
193pub struct ApiErrorResponse {
194    /// Structured error details.
195    pub error: ApiError,
196}
197
198/// Error details returned in a Gemini API error response.
199#[derive(Debug, Deserialize)]
200pub struct ApiError {
201    /// Human-readable description of the error.
202    pub message: String,
203}
204
205/// Wrapper for successful or error Gemini API responses.
206#[derive(Debug, Deserialize)]
207#[serde(untagged)]
208pub enum ApiResponse<T> {
209    // Untagged variants are tried in order, and some Gemini success response
210    // types contain only defaulted or optional fields that accept error objects.
211    Err(ApiErrorResponse),
212    Ok(T),
213}
214
215// ================================================================
216// Tests
217// ================================================================
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn api_response_detects_nested_error_before_permissive_success() {
225        #[derive(Debug, Deserialize)]
226        struct PermissiveResponse {
227            #[serde(default)]
228            candidates: Vec<serde_json::Value>,
229        }
230
231        let response: ApiResponse<PermissiveResponse> = serde_json::from_str(
232            r#"{"error":{"code":503,"message":"boom","status":"UNAVAILABLE"}}"#,
233        )
234        .expect("nested Gemini error should deserialize");
235
236        match response {
237            ApiResponse::Err(err) => assert_eq!(err.error.message, "boom"),
238            ApiResponse::Ok(response) => panic!(
239                "expected nested error, got success with {} candidates",
240                response.candidates.len()
241            ),
242        }
243    }
244
245    #[test]
246    fn api_response_allows_top_level_message_in_success() {
247        #[derive(Debug, Deserialize)]
248        struct MessageResponse {
249            message: String,
250        }
251
252        let response: ApiResponse<MessageResponse> =
253            serde_json::from_str(r#"{"message":"success"}"#)
254                .expect("success response should deserialize");
255
256        match response {
257            ApiResponse::Ok(response) => assert_eq!(response.message, "success"),
258            ApiResponse::Err(err) => panic!("expected success, got error: {err:?}"),
259        }
260    }
261
262    #[test]
263    fn test_client_initialization() {
264        let _client: Client = Client::new("dummy-key").expect("Client::new() failed");
265        let _client_from_builder: Client = Client::builder()
266            .api_key("dummy-key")
267            .build()
268            .expect("Client::builder() failed");
269    }
270}