rig_core/providers/gemini/
client.rs1use 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
7const GEMINI_API_BASE_URL: &str = "https://generativelanguage.googleapis.com";
11
12#[derive(Debug, Default, Clone)]
14pub struct GeminiExt {
15 api_key: String,
16}
17
18#[derive(Debug, Default, Clone)]
20pub struct GeminiBuilder;
21
22#[derive(Debug, Default, Clone)]
24pub struct GeminiInteractionsExt {
25 api_key: String,
26}
27
28#[derive(Debug, Default, Clone)]
30pub struct GeminiInteractionsBuilder;
31
32pub 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
44pub type Client<H = reqwest::Client> = client::Client<GeminiExt, H>;
46pub type ClientBuilder<H = crate::markers::Missing> =
48 client::ClientBuilder<GeminiBuilder, GeminiApiKey, H>;
49pub 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 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 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#[derive(Debug, Deserialize)]
193pub struct ApiErrorResponse {
194 pub error: ApiError,
196}
197
198#[derive(Debug, Deserialize)]
200pub struct ApiError {
201 pub message: String,
203}
204
205#[derive(Debug, Deserialize)]
207#[serde(untagged)]
208pub enum ApiResponse<T> {
209 Err(ApiErrorResponse),
212 Ok(T),
213}
214
215#[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}