Skip to main content

rig_core/providers/gemini/
streaming.rs

1use async_stream::stream;
2use futures::StreamExt;
3use serde::{Deserialize, Serialize};
4use tracing::{Level, enabled, info_span};
5use tracing_futures::Instrument;
6
7use super::completion::gemini_api_types::{
8    ContentCandidate, FinishReason, ModalityTokenCount, Part, PartKind, TrafficType,
9};
10use super::completion::{
11    CompletionModel, create_request_body, function_call_finish_reason_error, resolve_request_model,
12    streaming_endpoint,
13};
14use crate::completion::message::ReasoningContent;
15use crate::completion::{CompletionError, CompletionRequest, GetTokenUsage};
16use crate::http_client::HttpClientExt;
17use crate::http_client::sse::{Event, GenericEventSource};
18use crate::streaming;
19use crate::telemetry::SpanCombinator;
20
21#[derive(Debug, Deserialize, Serialize, Default, Clone)]
22#[serde(rename_all = "camelCase")]
23pub struct PartialUsage {
24    #[serde(default)]
25    pub total_token_count: i32,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub cached_content_token_count: Option<i32>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    pub candidates_token_count: Option<i32>,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub thoughts_token_count: Option<i32>,
32    #[serde(default)]
33    pub prompt_token_count: i32,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
36    #[serde(default, skip_serializing_if = "Option::is_none")]
37    pub cache_tokens_details: Option<Vec<ModalityTokenCount>>,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub candidates_tokens_details: Option<Vec<ModalityTokenCount>>,
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub tool_use_prompt_token_count: Option<i32>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub tool_use_prompt_tokens_details: Option<Vec<ModalityTokenCount>>,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub traffic_type: Option<TrafficType>,
46}
47
48impl GetTokenUsage for PartialUsage {
49    fn token_usage(&self) -> crate::completion::Usage {
50        let mut usage = crate::completion::Usage::new();
51
52        usage.input_tokens = self.prompt_token_count as u64;
53        usage.output_tokens = self.candidates_token_count.unwrap_or_default() as u64;
54        usage.cached_input_tokens = self.cached_content_token_count.unwrap_or_default() as u64;
55        usage.reasoning_tokens = self.thoughts_token_count.unwrap_or_default() as u64;
56        usage.tool_use_prompt_tokens = self.tool_use_prompt_token_count.unwrap_or_default() as u64;
57        usage.total_tokens = self.total_token_count as u64;
58
59        usage
60    }
61}
62
63#[derive(Debug, Deserialize)]
64#[serde(rename_all = "camelCase")]
65pub struct StreamGenerateContentResponse {
66    pub response_id: Option<String>,
67    /// Candidate responses from the model.
68    #[serde(default)]
69    pub candidates: Vec<ContentCandidate>,
70    pub model_version: Option<String>,
71    pub usage_metadata: Option<PartialUsage>,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct StreamingCompletionResponse {
76    pub usage_metadata: PartialUsage,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub finish_reason: Option<FinishReason>,
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub finish_message: Option<String>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub model_version: Option<String>,
83}
84
85impl GetTokenUsage for StreamingCompletionResponse {
86    fn token_usage(&self) -> crate::completion::Usage {
87        self.usage_metadata.token_usage()
88    }
89}
90
91fn tool_protocol_finish_reason_error(choice: &ContentCandidate) -> Option<CompletionError> {
92    let reason = choice.finish_reason.as_ref()?;
93    function_call_finish_reason_error(reason, choice.finish_message.as_deref())
94}
95
96impl<T> CompletionModel<T>
97where
98    T: HttpClientExt + Clone + 'static,
99{
100    pub(crate) async fn stream(
101        &self,
102        completion_request: CompletionRequest,
103    ) -> Result<streaming::StreamingCompletionResponse<StreamingCompletionResponse>, CompletionError>
104    {
105        let request_model = resolve_request_model(&self.model, &completion_request);
106        let span = if tracing::Span::current().is_disabled() {
107            info_span!(
108                target: "rig::completions",
109                "chat_streaming",
110                gen_ai.operation.name = "chat_streaming",
111                gen_ai.provider.name = "gcp.gemini",
112                gen_ai.request.model = &request_model,
113                gen_ai.system_instructions = &completion_request.preamble,
114                gen_ai.response.id = tracing::field::Empty,
115                gen_ai.response.model = &request_model,
116                gen_ai.usage.output_tokens = tracing::field::Empty,
117                gen_ai.usage.input_tokens = tracing::field::Empty,
118                gen_ai.usage.cache_read.input_tokens = tracing::field::Empty,
119                gen_ai.usage.cache_creation.input_tokens = tracing::field::Empty,
120                gen_ai.usage.tool_use_prompt_tokens = tracing::field::Empty,
121                gen_ai.usage.reasoning_tokens = tracing::field::Empty,
122            )
123        } else {
124            tracing::Span::current()
125        };
126        let request = create_request_body(completion_request)?;
127
128        if enabled!(Level::TRACE) {
129            tracing::trace!(
130                target: "rig::streaming",
131                "Gemini streaming completion request: {}",
132                serde_json::to_string_pretty(&request)?
133            );
134        }
135
136        let body = serde_json::to_vec(&request)?;
137
138        let req = self
139            .client
140            .post_sse(streaming_endpoint(&request_model))?
141            .header("Content-Type", "application/json")
142            .body(body)
143            .map_err(|e| CompletionError::HttpError(e.into()))?;
144
145        let mut event_source = GenericEventSource::new(self.client.clone(), req);
146
147        let stream = stream! {
148            let mut final_usage = None;
149            let mut final_finish_reason: Option<FinishReason> = None;
150            let mut final_finish_message: Option<String> = None;
151            let mut final_model_version: Option<String> = None;
152            let mut stream_failed = false;
153            while let Some(event_result) = event_source.next().await {
154                match event_result {
155                    Ok(Event::Open) => {
156                        tracing::debug!("SSE connection opened");
157                        continue;
158                    }
159                    Ok(Event::Message(message)) => {
160                        // Skip heartbeat messages or empty data
161                        if message.data.trim().is_empty() {
162                            continue;
163                        }
164
165                        let data = match serde_json::from_str::<StreamGenerateContentResponse>(&message.data) {
166                            Ok(d) => d,
167                            Err(error) => {
168                                tracing::error!(?error, message = message.data, "Failed to parse SSE message");
169                                stream_failed = true;
170                                yield Err(CompletionError::JsonError(error));
171                                break;
172                            }
173                        };
174
175                        let span = tracing::Span::current();
176                        if let Some(response_id) = data.response_id.as_deref() {
177                            span.record("gen_ai.response.id", response_id);
178                        }
179                        if let Some(model_version) = &data.model_version {
180                            span.record("gen_ai.response.model", model_version.as_str());
181                            final_model_version = Some(model_version.clone());
182                        }
183                        if let Some(usage) = data.usage_metadata.as_ref() {
184                            span.record_token_usage(usage);
185                            final_usage = Some(usage.clone());
186                        }
187
188                        // Process the response data
189                        let Some(choice) = data.candidates.into_iter().next() else {
190                            tracing::debug!("There is no content candidate");
191                            continue;
192                        };
193
194                        // Capture before partial moves of choice fields
195                        let should_stop = choice.finish_reason.is_some();
196                        if let Some(fr) = &choice.finish_reason {
197                            final_finish_reason = Some(fr.clone());
198                        }
199                        if let Some(message) = &choice.finish_message {
200                            final_finish_message = Some(message.clone());
201                        }
202
203                        if let Some(err) = tool_protocol_finish_reason_error(&choice) {
204                            stream_failed = true;
205                            yield Err(err);
206                            break;
207                        }
208
209                        let Some(content) = choice.content else {
210                            tracing::debug!(finish_reason = ?final_finish_reason, "Streaming candidate missing content");
211                            // Gemini's final chunk may carry finishReason with no content — break instead of skip
212                            if should_stop {
213                                break;
214                            }
215                            continue;
216                        };
217
218                        if content.parts.is_empty() {
219                            tracing::trace!(reason = ?choice.finish_reason, "There is no part in the streaming content");
220                        }
221
222                        for part in content.parts {
223                            match part {
224                                Part {
225                                    part: PartKind::Text(text),
226                                    thought: Some(true),
227                                    thought_signature,
228                                    ..
229                                } => {
230                                    if !text.is_empty() {
231                                        if thought_signature.is_some() {
232                                            // Signature arrives on the final chunk of a
233                                            // thinking block; emit a full Reasoning so the
234                                            // core accumulator captures the signature for
235                                            // Gemini 3+ roundtrip.
236                                            yield Ok(streaming::RawStreamingChoice::Reasoning {
237                                                id: None,
238                                                content: ReasoningContent::Text {
239                                                    text,
240                                                    signature: thought_signature,
241                                                },
242                                            });
243                                        } else {
244                                            yield Ok(streaming::RawStreamingChoice::ReasoningDelta {
245                                                id: None,
246                                                reasoning: text,
247                                            });
248                                        }
249                                    }
250                                },
251                                Part {
252                                    part: PartKind::Text(text),
253                                    ..
254                                } => {
255                                    if !text.is_empty() {
256                                        yield Ok(streaming::RawStreamingChoice::Message(text));
257                                    }
258                                },
259                                Part {
260                                    part: PartKind::FunctionCall(function_call),
261                                    thought_signature,
262                                    ..
263                                } => {
264                                    yield Ok(streaming::RawStreamingChoice::ToolCall(
265                                        streaming::RawStreamingToolCall::new(function_call.name.clone(), function_call.name.clone(), function_call.args.clone())
266                                            .with_signature(thought_signature)
267                                    ));
268                                },
269                                part => {
270                                    tracing::warn!(?part, "Unsupported response type with streaming");
271                                }
272                            }
273                        }
274
275                        // Check if this is the final response
276                        if should_stop {
277                            break;
278                        }
279                    }
280                    Err(crate::http_client::Error::StreamEnded) => {
281                        break;
282                    }
283                    Err(error) => {
284                        tracing::error!(?error, "SSE error");
285                        stream_failed = true;
286                        yield Err(CompletionError::from_stream_transport(error));
287                        break;
288                    }
289                }
290            }
291
292            // Ensure event source is closed when stream ends
293            event_source.close();
294
295            if !stream_failed {
296                yield Ok(streaming::RawStreamingChoice::FinalResponse(StreamingCompletionResponse {
297                    usage_metadata: final_usage.unwrap_or_default(),
298                    finish_reason: final_finish_reason,
299                    finish_message: final_finish_message,
300                    model_version: final_model_version,
301                }));
302            }
303        }.instrument(span);
304
305        Ok(streaming::StreamingCompletionResponse::stream(Box::pin(
306            stream,
307        )))
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314    use serde_json::json;
315
316    #[test]
317    fn test_deserialize_stream_response_with_single_text_part() {
318        let json_data = json!({
319            "candidates": [{
320                "content": {
321                    "parts": [
322                        {"text": "Hello, world!"}
323                    ],
324                    "role": "model"
325                },
326                "finishReason": "STOP",
327                "index": 0
328            }],
329            "usageMetadata": {
330                "promptTokenCount": 10,
331                "candidatesTokenCount": 5,
332                "totalTokenCount": 15
333            }
334        });
335
336        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
337        assert_eq!(response.candidates.len(), 1);
338        assert!(matches!(
339            response.candidates[0].finish_reason,
340            Some(FinishReason::Stop)
341        ));
342        let content = response.candidates[0]
343            .content
344            .as_ref()
345            .expect("candidate should contain content");
346        assert_eq!(content.parts.len(), 1);
347
348        if let Part {
349            part: PartKind::Text(text),
350            ..
351        } = &content.parts[0]
352        {
353            assert_eq!(text, "Hello, world!");
354        } else {
355            panic!("Expected text part");
356        }
357    }
358
359    #[test]
360    fn test_streaming_tool_protocol_finish_reason_returns_response_error() {
361        for (finish_reason, reason_name, finish_message) in [
362            (
363                "MALFORMED_FUNCTION_CALL",
364                "MalformedFunctionCall",
365                "malformed function call: default_api",
366            ),
367            (
368                "UNEXPECTED_TOOL_CALL",
369                "UnexpectedToolCall",
370                "unexpected tool call: default_api",
371            ),
372            (
373                "MISSING_THOUGHT_SIGNATURE",
374                "MissingThoughtSignature",
375                "missing thought signature for tool call",
376            ),
377            (
378                "TOO_MANY_TOOL_CALLS",
379                "TooManyToolCalls",
380                "too many tool calls in response",
381            ),
382            (
383                "MALFORMED_RESPONSE",
384                "MalformedResponse",
385                "malformed response from provider",
386            ),
387        ] {
388            let json_data = json!({
389                "candidates": [{
390                    "finishReason": finish_reason,
391                    "finishMessage": finish_message,
392                    "index": 0
393                }]
394            });
395
396            let response: StreamGenerateContentResponse =
397                serde_json::from_value(json_data).unwrap();
398            let candidate = response
399                .candidates
400                .first()
401                .expect("expected terminal candidate");
402            let err = tool_protocol_finish_reason_error(candidate)
403                .expect("tool protocol finish reason should be an error");
404
405            assert!(matches!(
406                err,
407                CompletionError::ResponseError(message)
408                    if message.contains(reason_name)
409                        && message.contains(finish_message)
410            ));
411        }
412    }
413
414    #[test]
415    fn test_deserialize_stream_response_with_usage_only_chunk() {
416        let json_data = json!({
417            "responseId": "response-123",
418            "modelVersion": "gemini-2.0-flash-001",
419            "usageMetadata": {
420                "promptTokenCount": 10,
421                "candidatesTokenCount": 5,
422                "totalTokenCount": 15
423            }
424        });
425
426        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
427        assert_eq!(response.response_id.as_deref(), Some("response-123"));
428        assert_eq!(
429            response.model_version.as_deref(),
430            Some("gemini-2.0-flash-001")
431        );
432        assert!(response.candidates.is_empty());
433
434        let usage = response
435            .usage_metadata
436            .as_ref()
437            .map(GetTokenUsage::token_usage)
438            .unwrap();
439        assert_eq!(usage.input_tokens, 10);
440        assert_eq!(usage.output_tokens, 5);
441        assert_eq!(usage.total_tokens, 15);
442    }
443
444    #[test]
445    fn test_deserialize_stream_response_with_multiple_text_parts() {
446        let json_data = json!({
447            "candidates": [{
448                "content": {
449                    "parts": [
450                        {"text": "Hello, "},
451                        {"text": "world!"},
452                        {"text": " How are you?"}
453                    ],
454                    "role": "model"
455                },
456                "finishReason": "STOP",
457                "index": 0
458            }],
459            "usageMetadata": {
460                "promptTokenCount": 10,
461                "candidatesTokenCount": 8,
462                "totalTokenCount": 18
463            }
464        });
465
466        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
467        assert_eq!(response.candidates.len(), 1);
468        let content = response.candidates[0]
469            .content
470            .as_ref()
471            .expect("candidate should contain content");
472        assert_eq!(content.parts.len(), 3);
473
474        // Verify all three text parts are present
475        for (i, expected_text) in ["Hello, ", "world!", " How are you?"].iter().enumerate() {
476            if let Part {
477                part: PartKind::Text(text),
478                ..
479            } = &content.parts[i]
480            {
481                assert_eq!(text, expected_text);
482            } else {
483                panic!("Expected text part at index {}", i);
484            }
485        }
486    }
487
488    #[test]
489    fn test_deserialize_stream_response_with_multiple_tool_calls() {
490        let json_data = json!({
491            "candidates": [{
492                "content": {
493                    "parts": [
494                        {
495                            "functionCall": {
496                                "name": "get_weather",
497                                "args": {"city": "San Francisco"}
498                            }
499                        },
500                        {
501                            "functionCall": {
502                                "name": "get_temperature",
503                                "args": {"location": "New York"}
504                            }
505                        }
506                    ],
507                    "role": "model"
508                },
509                "finishReason": "STOP",
510                "index": 0
511            }],
512            "usageMetadata": {
513                "promptTokenCount": 50,
514                "candidatesTokenCount": 20,
515                "totalTokenCount": 70
516            }
517        });
518
519        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
520        let content = response.candidates[0]
521            .content
522            .as_ref()
523            .expect("candidate should contain content");
524        assert_eq!(content.parts.len(), 2);
525
526        // Verify first tool call
527        if let Part {
528            part: PartKind::FunctionCall(call),
529            ..
530        } = &content.parts[0]
531        {
532            assert_eq!(call.name, "get_weather");
533        } else {
534            panic!("Expected function call at index 0");
535        }
536
537        // Verify second tool call
538        if let Part {
539            part: PartKind::FunctionCall(call),
540            ..
541        } = &content.parts[1]
542        {
543            assert_eq!(call.name, "get_temperature");
544        } else {
545            panic!("Expected function call at index 1");
546        }
547    }
548
549    #[test]
550    fn test_deserialize_stream_response_with_mixed_parts() {
551        let json_data = json!({
552            "candidates": [{
553                "content": {
554                    "parts": [
555                        {
556                            "text": "Let me think about this...",
557                            "thought": true
558                        },
559                        {
560                            "text": "Here's my response: "
561                        },
562                        {
563                            "functionCall": {
564                                "name": "search",
565                                "args": {"query": "rust async"}
566                            }
567                        },
568                        {
569                            "text": "I found the answer!"
570                        }
571                    ],
572                    "role": "model"
573                },
574                "finishReason": "STOP",
575                "index": 0
576            }],
577            "usageMetadata": {
578                "promptTokenCount": 100,
579                "candidatesTokenCount": 50,
580                "thoughtsTokenCount": 15,
581                "totalTokenCount": 165
582            }
583        });
584
585        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
586        let content = response.candidates[0]
587            .content
588            .as_ref()
589            .expect("candidate should contain content");
590        let parts = &content.parts;
591        assert_eq!(parts.len(), 4);
592
593        // Verify reasoning (thought) part
594        if let Part {
595            part: PartKind::Text(text),
596            thought: Some(true),
597            ..
598        } = &parts[0]
599        {
600            assert_eq!(text, "Let me think about this...");
601        } else {
602            panic!("Expected thought part at index 0");
603        }
604
605        // Verify regular text
606        if let Part {
607            part: PartKind::Text(text),
608            thought,
609            ..
610        } = &parts[1]
611        {
612            assert_eq!(text, "Here's my response: ");
613            assert!(thought.is_none() || thought == &Some(false));
614        } else {
615            panic!("Expected text part at index 1");
616        }
617
618        // Verify tool call
619        if let Part {
620            part: PartKind::FunctionCall(call),
621            ..
622        } = &parts[2]
623        {
624            assert_eq!(call.name, "search");
625        } else {
626            panic!("Expected function call at index 2");
627        }
628
629        // Verify final text
630        if let Part {
631            part: PartKind::Text(text),
632            ..
633        } = &parts[3]
634        {
635            assert_eq!(text, "I found the answer!");
636        } else {
637            panic!("Expected text part at index 3");
638        }
639    }
640
641    #[test]
642    fn test_deserialize_stream_response_with_empty_parts() {
643        let json_data = json!({
644            "candidates": [{
645                "content": {
646                    "parts": [],
647                    "role": "model"
648                },
649                "finishReason": "STOP",
650                "index": 0
651            }],
652            "usageMetadata": {
653                "promptTokenCount": 10,
654                "candidatesTokenCount": 0,
655                "totalTokenCount": 10
656            }
657        });
658
659        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
660        let content = response.candidates[0]
661            .content
662            .as_ref()
663            .expect("candidate should contain content");
664        assert_eq!(content.parts.len(), 0);
665    }
666
667    #[test]
668    fn test_partial_usage_token_calculation() {
669        let usage = PartialUsage {
670            total_token_count: 100,
671            cached_content_token_count: Some(20),
672            candidates_token_count: Some(30),
673            thoughts_token_count: Some(10),
674            prompt_token_count: 40,
675            prompt_tokens_details: None,
676            cache_tokens_details: None,
677            candidates_tokens_details: None,
678            tool_use_prompt_token_count: Some(12),
679            tool_use_prompt_tokens_details: None,
680            traffic_type: None,
681        };
682
683        let token_usage = usage.token_usage();
684        assert_eq!(token_usage.input_tokens, 40);
685        assert_eq!(token_usage.cached_input_tokens, 20);
686        assert_eq!(token_usage.output_tokens, 30);
687        assert_eq!(token_usage.reasoning_tokens, 10);
688        assert_eq!(token_usage.tool_use_prompt_tokens, 12);
689        assert_eq!(token_usage.total_tokens, 100);
690    }
691
692    #[test]
693    fn test_partial_usage_with_missing_counts() {
694        let usage = PartialUsage {
695            total_token_count: 50,
696            cached_content_token_count: None,
697            candidates_token_count: Some(30),
698            thoughts_token_count: None,
699            prompt_token_count: 20,
700            prompt_tokens_details: None,
701            cache_tokens_details: None,
702            candidates_tokens_details: None,
703            tool_use_prompt_token_count: None,
704            tool_use_prompt_tokens_details: None,
705            traffic_type: None,
706        };
707
708        let token_usage = usage.token_usage();
709        assert_eq!(token_usage.input_tokens, 20);
710        assert_eq!(token_usage.cached_input_tokens, 0);
711        assert_eq!(token_usage.output_tokens, 30);
712        assert_eq!(token_usage.reasoning_tokens, 0);
713        assert_eq!(token_usage.total_tokens, 50);
714    }
715
716    #[test]
717    fn test_partial_usage_deserializes_without_total_token_count() {
718        // Gemini's proto3-JSON encoding omits fields whose value is the default (0),
719        // so `totalTokenCount` is absent on short/empty/blocked generations.
720        let usage: PartialUsage =
721            serde_json::from_str(r#"{"promptTokenCount": 12}"#).expect("should deserialize");
722        assert_eq!(usage.total_token_count, 0);
723        assert_eq!(usage.prompt_token_count, 12);
724    }
725
726    #[test]
727    fn test_streaming_completion_response_has_finish_reason_and_model_version() {
728        use super::super::completion::gemini_api_types::FinishReason;
729
730        let response = StreamingCompletionResponse {
731            usage_metadata: PartialUsage::default(),
732            finish_reason: Some(FinishReason::Stop),
733            finish_message: None,
734            model_version: Some("gemini-2.5-pro-preview-05-06".to_string()),
735        };
736
737        assert!(matches!(response.finish_reason, Some(FinishReason::Stop)));
738        assert_eq!(
739            response.model_version.as_deref(),
740            Some("gemini-2.5-pro-preview-05-06")
741        );
742
743        let json = serde_json::to_string(&response).unwrap();
744        let deserialized: StreamingCompletionResponse = serde_json::from_str(&json).unwrap();
745        assert!(matches!(
746            deserialized.finish_reason,
747            Some(FinishReason::Stop)
748        ));
749        assert_eq!(
750            deserialized.model_version.as_deref(),
751            Some("gemini-2.5-pro-preview-05-06")
752        );
753    }
754
755    #[test]
756    fn test_streaming_completion_response_token_usage() {
757        let response = StreamingCompletionResponse {
758            usage_metadata: PartialUsage {
759                total_token_count: 150,
760                cached_content_token_count: None,
761                candidates_token_count: Some(75),
762                thoughts_token_count: None,
763                prompt_token_count: 75,
764                prompt_tokens_details: None,
765                cache_tokens_details: None,
766                candidates_tokens_details: None,
767                tool_use_prompt_token_count: None,
768                tool_use_prompt_tokens_details: None,
769                traffic_type: None,
770            },
771            finish_reason: Some(FinishReason::Stop),
772            finish_message: None,
773            model_version: Some("gemini-2.0-flash-001".to_string()),
774        };
775
776        let token_usage = response.token_usage();
777        assert_eq!(token_usage.input_tokens, 75);
778        assert_eq!(token_usage.output_tokens, 75);
779        assert_eq!(token_usage.reasoning_tokens, 0);
780        assert_eq!(token_usage.cached_input_tokens, 0);
781        assert_eq!(token_usage.total_tokens, 150);
782        assert!(matches!(response.finish_reason, Some(FinishReason::Stop)));
783        assert_eq!(
784            response.model_version.as_deref(),
785            Some("gemini-2.0-flash-001")
786        );
787    }
788
789    #[test]
790    fn test_partial_usage_serde_roundtrip_with_all_optional_fields() {
791        let json_data = serde_json::json!({
792            "promptTokenCount": 100,
793            "cachedContentTokenCount": 25,
794            "candidatesTokenCount": 50,
795            "thoughtsTokenCount": 15,
796            "totalTokenCount": 190,
797            "promptTokensDetails": [
798                { "modality": "TEXT", "tokenCount": 80 },
799                { "modality": "IMAGE", "tokenCount": 20 }
800            ],
801            "cacheTokensDetails": [
802                { "modality": "TEXT", "tokenCount": 25 }
803            ],
804            "candidatesTokensDetails": [
805                { "modality": "TEXT", "tokenCount": 50 }
806            ],
807            "toolUsePromptTokenCount": 12,
808            "toolUsePromptTokensDetails": [
809                { "modality": "TEXT", "tokenCount": 12 }
810            ],
811            "trafficType": "PROVISIONED_THROUGHPUT"
812        });
813
814        let usage: PartialUsage = serde_json::from_value(json_data).unwrap();
815        assert_eq!(usage.prompt_token_count, 100);
816        assert_eq!(usage.cached_content_token_count, Some(25));
817        assert_eq!(usage.candidates_token_count, Some(50));
818        assert_eq!(usage.thoughts_token_count, Some(15));
819        assert_eq!(usage.total_token_count, 190);
820        assert!(usage.prompt_tokens_details.is_some());
821        assert_eq!(usage.prompt_tokens_details.as_ref().unwrap().len(), 2);
822        assert!(usage.cache_tokens_details.is_some());
823        assert!(usage.candidates_tokens_details.is_some());
824        assert_eq!(usage.tool_use_prompt_token_count, Some(12));
825        assert!(usage.tool_use_prompt_tokens_details.is_some());
826        assert!(matches!(
827            usage.traffic_type,
828            Some(TrafficType::ProvisionedThroughput)
829        ));
830
831        let token_usage = usage.token_usage();
832        assert_eq!(token_usage.input_tokens, 100);
833        assert_eq!(token_usage.cached_input_tokens, 25);
834        assert_eq!(token_usage.output_tokens, 50);
835        assert_eq!(token_usage.reasoning_tokens, 15);
836        assert_eq!(token_usage.tool_use_prompt_tokens, 12);
837        assert_eq!(token_usage.total_tokens, 190);
838    }
839}