Skip to main content

llm_connector/protocols/adapters/google/
mod.rs

1//! Google Gemini Protocol Implementation
2//!
3//! This module provides the Google Gemini API protocol.
4
5use 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        // Populate reasoning content if present in usage_metadata or parts
77        // Note: Gemini 2.0 Thinking puts thoughts in usage_metadata.thoughts_token_count
78        // but the actual text is usually in a special part or handled by the provider.
79        // If the library users use `with_enable_thinking`, we should try to extract it if possible.
80        // Currently, our ChatResponse::from(GoogleResponse) handles token counts.
81
82        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                                // Extract incremental text, reasoning, and tool calls
171                                let (content, reasoning, tool_calls, finish_reason) = google_resp
172                                    .candidates
173                                    .as_ref()
174                                    .and_then(|c| c.first())
175                                    .map(|candidate| {
176                                        // Text content
177                                        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                                        // Reasoning (thought)
189                                        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                                        // Tool calls extraction
199                                        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// ============================================================================
307// Google API Types
308// ============================================================================
309
310#[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, // "AUTO", "ANY", "NONE"
343    #[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                // Handle tool calls in assistant messages
394                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                // Handle tool responses
412                if msg.role == Role::Tool
413                    && let Some(id) = &msg.tool_call_id
414                {
415                    // In Gemini, FunctionResponse name must match the call
416                    // We use tool_call_id as the name if possible, or we might need more context
417                    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(), // Use name as ID
619                                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        // Verify thinking_config is set
722        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        // Verify thinking_config is set to false
737        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        // Verify thinking_config is NOT set
750        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        // Verify tool calls
792        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}