1use crate::core::Protocol;
6use crate::error::LlmConnectorError;
7use crate::protocols::common::capabilities::ProviderCapabilities;
8use crate::types::{
9 ChatRequest, ChatResponse, Choice, DocumentSource, EmbedRequest, EmbedResponse, EmbeddingData,
10 FunctionCall, ImageSource, Message, MessageBlock, Role, ToolCall, Usage,
11};
12use async_trait::async_trait;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Default)]
16pub struct GoogleProtocol;
17
18impl GoogleProtocol {
19 pub fn new() -> Self {
20 Self
21 }
22}
23
24#[async_trait]
25impl Protocol for GoogleProtocol {
26 type Request = GoogleRequest;
27 type Response = GoogleResponse;
28
29 fn name(&self) -> &str {
30 "google"
31 }
32
33 fn capabilities(&self) -> ProviderCapabilities {
34 ProviderCapabilities::google()
35 }
36
37 fn chat_endpoint(&self, base_url: &str, model: &str) -> String {
38 format!(
39 "{}/models/{}:generateContent",
40 base_url.trim_end_matches('/'),
41 model
42 )
43 }
44
45 #[cfg(feature = "streaming")]
46 fn chat_stream_endpoint(&self, base_url: &str, model: &str) -> String {
47 format!(
48 "{}/models/{}:streamGenerateContent?alt=sse",
49 base_url.trim_end_matches('/'),
50 model
51 )
52 }
53
54 fn models_endpoint(&self, base_url: &str) -> Option<String> {
55 Some(format!("{}/models", base_url.trim_end_matches('/')))
56 }
57
58 fn embed_endpoint(&self, base_url: &str, model: &str) -> Option<String> {
59 Some(format!(
60 "{}/models/{}:batchEmbedContents",
61 base_url.trim_end_matches('/'),
62 model
63 ))
64 }
65
66 fn build_request(&self, request: &ChatRequest) -> Result<Self::Request, LlmConnectorError> {
67 Ok(GoogleRequest::from(request))
68 }
69
70 fn parse_response(&self, response: &str) -> Result<ChatResponse, LlmConnectorError> {
71 let google_response: GoogleResponse =
72 serde_json::from_str(response).map_err(LlmConnectorError::JsonError)?;
73
74 let chat_response: ChatResponse = google_response.into();
75
76 Ok(chat_response)
83 }
84
85 fn parse_models(&self, response: &str) -> Result<Vec<String>, LlmConnectorError> {
86 let models_response: GoogleModelsResponse =
87 serde_json::from_str(response).map_err(LlmConnectorError::JsonError)?;
88
89 Ok(models_response
90 .models
91 .into_iter()
92 .map(|m| m.name.replace("models/", ""))
93 .collect())
94 }
95
96 fn build_embed_request(
97 &self,
98 request: &EmbedRequest,
99 ) -> Result<serde_json::Value, LlmConnectorError> {
100 let requests: Vec<GoogleEmbedRequest> = request
101 .input
102 .iter()
103 .map(|text| GoogleEmbedRequest {
104 model: format!("models/{}", request.model),
105 content: GoogleContent {
106 role: String::new(),
107 parts: vec![GooglePart::Text { text: text.clone() }],
108 },
109 })
110 .collect();
111
112 let req_body = GoogleBatchEmbedRequest { requests };
113 serde_json::to_value(req_body).map_err(LlmConnectorError::JsonError)
114 }
115
116 fn parse_embed_response(&self, response: &str) -> Result<EmbedResponse, LlmConnectorError> {
117 let google_response: GoogleBatchEmbedResponse =
118 serde_json::from_str(response).map_err(LlmConnectorError::JsonError)?;
119
120 let mut data = Vec::new();
121 if let Some(embeddings) = google_response.embeddings {
122 for (index, emb) in embeddings.into_iter().enumerate() {
123 data.push(EmbeddingData {
124 object: "embedding".to_string(),
125 embedding: emb.values,
126 index: index as u32,
127 });
128 }
129 }
130
131 Ok(EmbedResponse {
132 object: "list".to_string(),
133 data,
134 model: "google".to_string(),
135 usage: Usage::default(),
136 })
137 }
138
139 fn map_error(&self, status: u16, body: &str) -> LlmConnectorError {
140 LlmConnectorError::ProviderError(format!("Google API error: {} - {}", status, body))
141 }
142
143 #[cfg(feature = "streaming")]
144 async fn parse_stream_response(
145 &self,
146 response: reqwest::Response,
147 ) -> Result<crate::types::ChatStream, LlmConnectorError> {
148 use crate::sse::sse_events;
149 use crate::types::{Delta, StreamingChoice, StreamingResponse};
150 use futures_util::StreamExt;
151
152 let stream = sse_events(response)
153 .scan(false, move |sent_role, event_result| {
154 let mapped: Result<Option<StreamingResponse>, LlmConnectorError> =
155 match event_result {
156 Ok(json_str) => {
157 if json_str.trim().is_empty() {
158 Ok(None)
159 } else {
160 let google_resp: GoogleResponse =
161 match serde_json::from_str(&json_str) {
162 Ok(v) => v,
163 Err(e) => {
164 return std::future::ready(Some(Err(
165 LlmConnectorError::JsonError(e),
166 )));
167 }
168 };
169
170 let (content, reasoning, tool_calls, finish_reason) = google_resp
172 .candidates
173 .as_ref()
174 .and_then(|c| c.first())
175 .map(|candidate| {
176 let text = candidate
178 .content
179 .as_ref()
180 .and_then(|c| {
181 c.parts.iter().find_map(|p| match p {
182 GooglePart::Text { text } => Some(text.clone()),
183 _ => None,
184 })
185 })
186 .unwrap_or_default();
187
188 let thought = candidate.content.as_ref().and_then(|c| {
190 c.parts.iter().find_map(|p| match p {
191 GooglePart::Thought { text, .. } => {
192 Some(text.clone())
193 }
194 _ => None,
195 })
196 });
197
198 let tools: Vec<ToolCall> = candidate
200 .content
201 .as_ref()
202 .map(|c| {
203 c.parts
204 .iter()
205 .filter_map(|p| match p {
206 GooglePart::FunctionCall {
207 function_call,
208 thought_signature,
209 } => Some(ToolCall {
210 id: function_call.name.clone(),
211 call_type: "function".to_string(),
212 function: FunctionCall {
213 name: function_call.name.clone(),
214 arguments: function_call
215 .args
216 .to_string(),
217 thought_signature:
218 thought_signature.clone(),
219 },
220 index: None,
221 thought_signature: thought_signature
222 .clone(),
223 }),
224 _ => None,
225 })
226 .collect()
227 })
228 .unwrap_or_default();
229
230 (text, thought, tools, candidate.finish_reason.clone())
231 })
232 .unwrap_or_default();
233
234 let usage = google_resp.usage_metadata.map(|u| Usage {
235 prompt_tokens: u.prompt_token_count.unwrap_or(0),
236 completion_tokens: u.candidates_token_count.unwrap_or(0)
237 + u.thoughts_token_count.unwrap_or(0),
238 total_tokens: u.total_token_count.unwrap_or(0),
239 ..Default::default()
240 });
241
242 if content.is_empty()
243 && reasoning.is_none()
244 && finish_reason.is_none()
245 && usage.is_none()
246 && tool_calls.is_empty()
247 {
248 Ok(None)
249 } else {
250 let role = if !*sent_role {
251 *sent_role = true;
252 Some(Role::Assistant)
253 } else {
254 None
255 };
256
257 Ok(Some(StreamingResponse {
258 id: "google".to_string(),
259 object: "chat.completion.chunk".to_string(),
260 created: chrono::Utc::now().timestamp() as u64,
261 model: "google".to_string(),
262 choices: vec![StreamingChoice {
263 index: 0,
264 delta: Delta {
265 role,
266 content: if content.is_empty() {
267 None
268 } else {
269 Some(content.clone())
270 },
271 reasoning_content: reasoning,
272 tool_calls: if tool_calls.is_empty() {
273 None
274 } else {
275 Some(tool_calls)
276 },
277 ..Default::default()
278 },
279 finish_reason,
280 logprobs: None,
281 }],
282 content,
283 usage,
284 ..Default::default()
285 }))
286 }
287 }
288 }
289 Err(e) => Err(e),
290 };
291
292 std::future::ready(Some(mapped))
293 })
294 .filter_map(|x| async move {
295 match x {
296 Ok(Some(v)) => Some(Ok(v)),
297 Ok(None) => None,
298 Err(e) => Some(Err(e)),
299 }
300 });
301
302 Ok(Box::pin(stream))
303 }
304}
305
306#[derive(Serialize, Deserialize)]
311pub struct GoogleRequest {
312 pub contents: Vec<GoogleContent>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub generation_config: Option<GoogleGenerationConfig>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub tools: Option<Vec<GoogleTool>>,
317 #[serde(skip_serializing_if = "Option::is_none", rename = "toolConfig")]
318 pub tool_config: Option<GoogleToolConfig>,
319}
320
321#[derive(Serialize, Deserialize)]
322pub struct GoogleTool {
323 #[serde(rename = "functionDeclarations")]
324 pub function_declarations: Vec<GoogleFunctionDeclaration>,
325}
326
327#[derive(Serialize, Deserialize)]
328pub struct GoogleFunctionDeclaration {
329 pub name: String,
330 pub description: Option<String>,
331 pub parameters: serde_json::Value,
332}
333
334#[derive(Serialize, Deserialize)]
335pub struct GoogleToolConfig {
336 #[serde(rename = "functionCallingConfig")]
337 pub function_calling_config: GoogleFunctionCallingConfig,
338}
339
340#[derive(Serialize, Deserialize)]
341pub struct GoogleFunctionCallingConfig {
342 pub mode: String, #[serde(skip_serializing_if = "Vec::is_empty", rename = "allowedFunctionNames")]
344 pub allowed_function_names: Vec<String>,
345}
346
347impl From<&ChatRequest> for GoogleRequest {
348 fn from(req: &ChatRequest) -> Self {
349 let reasoning_parts = crate::protocols::common::thinking::map_reasoning_request_parts(
350 req,
351 crate::protocols::common::capabilities::ProviderCapabilities::google(),
352 );
353
354 let contents = req
355 .messages
356 .iter()
357 .map(|msg| {
358 let parts = msg
359 .content
360 .iter()
361 .map(|block| match block {
362 MessageBlock::Text { text } => GooglePart::Text { text: text.clone() },
363 MessageBlock::Thinking { thinking, .. } => GooglePart::Text {
364 text: thinking.clone(),
365 },
366 MessageBlock::Image {
367 source: ImageSource::Base64 { media_type, data },
368 } => GooglePart::InlineData {
369 inline_data: GoogleInlineData {
370 mime_type: media_type.clone(),
371 data: data.clone(),
372 },
373 },
374 MessageBlock::Image { .. } => GooglePart::Text {
375 text: "".to_string(),
376 },
377 MessageBlock::Document { source } => match source {
378 DocumentSource::Base64 { media_type, data } => GooglePart::InlineData {
379 inline_data: GoogleInlineData {
380 mime_type: media_type.clone(),
381 data: data.clone(),
382 },
383 },
384 },
385 _ => GooglePart::Text {
386 text: "".to_string(),
387 },
388 })
389 .collect::<Vec<_>>();
390
391 let mut final_parts = parts;
392
393 if let Some(tool_calls) = &msg.tool_calls {
395 for tc in tool_calls {
396 final_parts.push(GooglePart::FunctionCall {
397 function_call: GoogleFunctionCall {
398 name: tc.function.name.clone(),
399 args: tc
400 .arguments_value()
401 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
402 },
403 thought_signature: tc
404 .thought_signature
405 .clone()
406 .or(tc.function.thought_signature.clone()),
407 });
408 }
409 }
410
411 if msg.role == Role::Tool
413 && let Some(id) = &msg.tool_call_id
414 {
415 final_parts.push(GooglePart::FunctionResponse {
418 function_response: GoogleFunctionResponse {
419 name: id.clone(),
420 response: serde_json::from_str(&msg.content_as_text())
421 .unwrap_or(serde_json::Value::Object(serde_json::Map::new())),
422 },
423 });
424 }
425
426 GoogleContent {
427 role: match msg.role {
428 Role::User => "user".to_string(),
429 Role::Assistant => "model".to_string(),
430 Role::System => "user".to_string(),
431 Role::Tool => "user".to_string(),
432 },
433 parts: final_parts,
434 }
435 })
436 .collect();
437
438 let tools = req.tools.as_ref().map(|t| {
439 vec![GoogleTool {
440 function_declarations: t
441 .iter()
442 .map(|tool| GoogleFunctionDeclaration {
443 name: tool.function.name.clone(),
444 description: tool.function.description.clone(),
445 parameters: tool.function.parameters.clone(),
446 })
447 .collect(),
448 }]
449 });
450
451 let tool_config = req.tool_choice.as_ref().map(|tc| {
452 let (mode, allowed) = match tc {
453 crate::types::ToolChoice::Mode(m) => match m.as_str() {
454 "none" => ("NONE", vec![]),
455 "auto" => ("AUTO", vec![]),
456 "required" => ("ANY", vec![]),
457 _ => ("AUTO", vec![]),
458 },
459 crate::types::ToolChoice::Function { function, .. } => {
460 ("ANY", vec![function.name.clone()])
461 }
462 };
463 GoogleToolConfig {
464 function_calling_config: GoogleFunctionCallingConfig {
465 mode: mode.to_string(),
466 allowed_function_names: allowed,
467 },
468 }
469 });
470
471 GoogleRequest {
472 contents,
473 tools,
474 tool_config,
475 generation_config: Some(GoogleGenerationConfig {
476 temperature: req.temperature,
477 top_p: req.top_p,
478 max_output_tokens: req.max_tokens,
479 thinking_config: reasoning_parts
480 .enable_thinking
481 .map(|b| GoogleThinkingConfig {
482 include_thoughts: b,
483 }),
484 }),
485 }
486 }
487}
488
489#[derive(Serialize, Deserialize)]
490pub struct GoogleContent {
491 #[serde(default)]
492 pub role: String,
493 #[serde(default)]
494 pub parts: Vec<GooglePart>,
495}
496
497#[derive(Serialize, Deserialize)]
498#[serde(untagged)]
499pub enum GooglePart {
500 Thought {
501 text: String,
502 thought: bool,
503 },
504 Text {
505 text: String,
506 },
507 InlineData {
508 inline_data: GoogleInlineData,
509 },
510 FunctionCall {
511 #[serde(rename = "functionCall")]
512 function_call: GoogleFunctionCall,
513 #[serde(skip_serializing_if = "Option::is_none", rename = "thoughtSignature")]
514 thought_signature: Option<String>,
515 },
516 FunctionResponse {
517 #[serde(rename = "functionResponse")]
518 function_response: GoogleFunctionResponse,
519 },
520}
521
522#[derive(Serialize, Deserialize)]
523pub struct GoogleFunctionCall {
524 pub name: String,
525 pub args: serde_json::Value,
526}
527
528#[derive(Serialize, Deserialize)]
529pub struct GoogleFunctionResponse {
530 pub name: String,
531 pub response: serde_json::Value,
532}
533
534impl GooglePart {
535 pub fn as_text(&self) -> Option<&str> {
536 match self {
537 Self::Text { text } => Some(text),
538 _ => None,
539 }
540 }
541}
542
543#[derive(Serialize, Deserialize)]
544pub struct GoogleInlineData {
545 #[serde(rename = "mimeType")]
546 pub mime_type: String,
547 pub data: String,
548}
549
550#[derive(Serialize, Deserialize)]
551pub struct GoogleGenerationConfig {
552 #[serde(skip_serializing_if = "Option::is_none")]
553 pub temperature: Option<f32>,
554 #[serde(skip_serializing_if = "Option::is_none")]
555 pub top_p: Option<f32>,
556 #[serde(skip_serializing_if = "Option::is_none")]
557 pub max_output_tokens: Option<u32>,
558 #[serde(skip_serializing_if = "Option::is_none")]
559 pub thinking_config: Option<GoogleThinkingConfig>,
560}
561
562#[derive(Serialize, Deserialize)]
563pub struct GoogleThinkingConfig {
564 pub include_thoughts: bool,
565}
566
567#[derive(Serialize, Deserialize)]
568pub struct GoogleResponse {
569 pub candidates: Option<Vec<GoogleCandidate>>,
570 #[serde(rename = "usageMetadata")]
571 pub usage_metadata: Option<GoogleUsageMetadata>,
572}
573
574#[derive(Serialize, Deserialize)]
575pub struct GoogleCandidate {
576 pub content: Option<GoogleContent>,
577 #[serde(rename = "finishReason")]
578 pub finish_reason: Option<String>,
579}
580
581#[derive(Serialize, Deserialize)]
582pub struct GoogleUsageMetadata {
583 #[serde(rename = "promptTokenCount")]
584 pub prompt_token_count: Option<u32>,
585 #[serde(rename = "candidatesTokenCount")]
586 pub candidates_token_count: Option<u32>,
587 #[serde(rename = "totalTokenCount")]
588 pub total_token_count: Option<u32>,
589 #[serde(rename = "thoughtsTokenCount")]
590 pub thoughts_token_count: Option<u32>,
591}
592
593impl From<GoogleResponse> for ChatResponse {
594 fn from(value: GoogleResponse) -> Self {
595 let mut tool_calls = Vec::new();
596 let mut reasoning_content = None;
597 let mut final_content = String::new();
598 let mut finish_reason = None;
599
600 if let Some(candidates) = value.candidates
601 && let Some(candidate) = candidates.into_iter().next()
602 {
603 finish_reason = candidate.finish_reason;
604 if let Some(content) = candidate.content {
605 for part in content.parts {
606 match part {
607 GooglePart::Text { text } => {
608 if !final_content.is_empty() {
609 final_content.push('\n');
610 }
611 final_content.push_str(&text);
612 }
613 GooglePart::FunctionCall {
614 function_call,
615 thought_signature,
616 } => {
617 tool_calls.push(crate::types::ToolCall {
618 id: function_call.name.clone(), call_type: "function".to_string(),
620 function: crate::types::FunctionCall {
621 name: function_call.name,
622 arguments: function_call.args.to_string(),
623 thought_signature: thought_signature.clone(),
624 },
625 index: Some(tool_calls.len()),
626 thought_signature,
627 });
628 }
629 GooglePart::Thought { text, .. } => {
630 reasoning_content = Some(text);
631 }
632 _ => {}
633 }
634 }
635 }
636 }
637
638 let choice = Choice {
639 index: 0,
640 message: Message {
641 role: Role::Assistant,
642 content: vec![crate::types::MessageBlock::text(final_content.clone())],
643 tool_calls: if tool_calls.is_empty() {
644 None
645 } else {
646 Some(tool_calls)
647 },
648 reasoning_content: reasoning_content.clone(),
649 ..Default::default()
650 },
651 finish_reason,
652 logprobs: None,
653 };
654
655 let usage = value.usage_metadata.map(|u| Usage {
656 prompt_tokens: u.prompt_token_count.unwrap_or(0),
657 completion_tokens: u.candidates_token_count.unwrap_or(0)
658 + u.thoughts_token_count.unwrap_or(0),
659 total_tokens: u.total_token_count.unwrap_or(0),
660 ..Default::default()
661 });
662
663 ChatResponse {
664 id: "google".to_string(),
665 object: "chat.completion".to_string(),
666 created: chrono::Utc::now().timestamp() as u64,
667 model: "google".to_string(),
668 choices: vec![choice],
669 content: final_content,
670 reasoning_content,
671 usage,
672 system_fingerprint: None,
673 }
674 }
675}
676
677#[derive(Deserialize)]
678pub struct GoogleModelsResponse {
679 pub models: Vec<GoogleModel>,
680}
681
682#[derive(Deserialize)]
683pub struct GoogleModel {
684 pub name: String,
685}
686
687#[derive(Serialize, Deserialize)]
688pub struct GoogleBatchEmbedRequest {
689 pub requests: Vec<GoogleEmbedRequest>,
690}
691
692#[derive(Serialize, Deserialize)]
693pub struct GoogleEmbedRequest {
694 pub model: String,
695 pub content: GoogleContent,
696}
697
698#[derive(Deserialize)]
699pub struct GoogleBatchEmbedResponse {
700 pub embeddings: Option<Vec<GoogleEmbedding>>,
701}
702
703#[derive(Deserialize)]
704pub struct GoogleEmbedding {
705 pub values: Vec<f32>,
706}
707
708#[cfg(test)]
709mod tests {
710 use super::*;
711 use crate::types::Message;
712
713 #[test]
714 fn test_google_thinking_config() {
715 let req = ChatRequest::new("gemini-2.0-flash")
716 .add_message(Message::user("test"))
717 .with_enable_thinking(true);
718
719 let google_req = GoogleRequest::from(&req);
720
721 assert!(google_req.generation_config.is_some());
723 let config = google_req.generation_config.unwrap();
724 assert!(config.thinking_config.is_some());
725 assert!(config.thinking_config.unwrap().include_thoughts);
726 }
727
728 #[test]
729 fn test_google_thinking_config_disabled() {
730 let req = ChatRequest::new("gemini-2.0-flash")
731 .add_message(Message::user("test"))
732 .with_enable_thinking(false);
733
734 let google_req = GoogleRequest::from(&req);
735
736 assert!(google_req.generation_config.is_some());
738 let config = google_req.generation_config.unwrap();
739 assert!(config.thinking_config.is_some());
740 assert!(!config.thinking_config.unwrap().include_thoughts);
741 }
742
743 #[test]
744 fn test_google_thinking_config_none() {
745 let req = ChatRequest::new("gemini-2.0-flash").add_message(Message::user("test"));
746
747 let google_req = GoogleRequest::from(&req);
748
749 assert!(google_req.generation_config.is_some());
751 let config = google_req.generation_config.unwrap();
752 assert!(config.thinking_config.is_none());
753 }
754
755 #[tokio::test]
756 async fn test_parse_stream_response_tool_call() {
757 use futures::stream::TryStreamExt;
758 use serde_json::json;
759
760 const TOOL_NAME: &str = "set_light_values";
761 let google_response = GoogleResponse {
762 candidates: Some(vec![GoogleCandidate {
763 content: Some(GoogleContent {
764 role: "model".to_string(),
765 parts: vec![GooglePart::FunctionCall {
766 function_call: GoogleFunctionCall {
767 name: TOOL_NAME.to_string(),
768 args: json!(r#"{"brightness": 25, "color_temp": "warm"}"#),
769 },
770 thought_signature: None,
771 }],
772 }),
773 finish_reason: None,
774 }]),
775 usage_metadata: None,
776 };
777 let google_response_serialized = serde_json::to_string(&google_response).unwrap();
778 let resp = reqwest::Response::from(http::response::Response::new(format!(
779 "data: {google_response_serialized}\n\n"
780 )));
781 let protocol = GoogleProtocol::new();
782 let streaming_response: Option<crate::types::StreamingResponse> =
783 match protocol.parse_stream_response(resp).await {
784 Ok(resp) => match resp.try_collect::<Vec<_>>().await {
785 Ok(v) => Some(v[0].clone()),
786 _ => None,
787 },
788 _ => None,
789 };
790
791 assert!(streaming_response.is_some_and(|sr| {
793 sr.choices[0]
794 .delta
795 .tool_calls
796 .as_ref()
797 .is_some_and(|tc| tc[0].function.name == TOOL_NAME)
798 }))
799 }
800}