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};
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::{CompletionOperation, CompletionSpanBuilder, 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 = CompletionSpanBuilder::new(
107            "gcp.gemini",
108            &request_model,
109            CompletionOperation::ChatStreaming,
110        )
111        .system_instructions(
112            completion_request.preamble.as_deref(),
113            completion_request.record_telemetry_content,
114        )
115        .build();
116        let request = create_request_body(completion_request)?;
117
118        if enabled!(Level::TRACE) {
119            tracing::trace!(
120                target: "rig::streaming",
121                "Gemini streaming completion request: {}",
122                serde_json::to_string_pretty(&request)?
123            );
124        }
125
126        let body = serde_json::to_vec(&request)?;
127
128        let req = self
129            .client
130            .post_sse(streaming_endpoint(&request_model))?
131            .header("Content-Type", "application/json")
132            .body(body)
133            .map_err(|e| CompletionError::HttpError(e.into()))?;
134
135        let mut event_source = GenericEventSource::new(self.client.clone(), req);
136
137        let stream = stream! {
138            let mut final_usage = None;
139            let mut final_finish_reason: Option<FinishReason> = None;
140            let mut final_finish_message: Option<String> = None;
141            let mut final_model_version: Option<String> = None;
142            let mut stream_failed = false;
143            while let Some(event_result) = event_source.next().await {
144                match event_result {
145                    Ok(Event::Open) => {
146                        tracing::debug!("SSE connection opened");
147                        continue;
148                    }
149                    Ok(Event::Message(message)) => {
150                        // Skip heartbeat messages or empty data
151                        if message.data.trim().is_empty() {
152                            continue;
153                        }
154
155                        let data = match serde_json::from_str::<StreamGenerateContentResponse>(&message.data) {
156                            Ok(d) => d,
157                            Err(error) => {
158                                tracing::error!(?error, message = message.data, "Failed to parse SSE message");
159                                stream_failed = true;
160                                yield Err(CompletionError::JsonError(error));
161                                break;
162                            }
163                        };
164
165                        let span = tracing::Span::current();
166                        if let Some(response_id) = data.response_id.as_deref() {
167                            span.record("gen_ai.response.id", response_id);
168                        }
169                        if let Some(model_version) = &data.model_version {
170                            span.record("gen_ai.response.model", model_version.as_str());
171                            final_model_version = Some(model_version.clone());
172                        }
173                        if let Some(usage) = data.usage_metadata.as_ref() {
174                            span.record_token_usage(usage);
175                            final_usage = Some(usage.clone());
176                        }
177
178                        // Process the response data
179                        let Some(choice) = data.candidates.into_iter().next() else {
180                            tracing::debug!("There is no content candidate");
181                            continue;
182                        };
183
184                        // Capture before partial moves of choice fields
185                        let should_stop = choice.finish_reason.is_some();
186                        if let Some(fr) = &choice.finish_reason {
187                            final_finish_reason = Some(fr.clone());
188                        }
189                        if let Some(message) = &choice.finish_message {
190                            final_finish_message = Some(message.clone());
191                        }
192
193                        if let Some(err) = tool_protocol_finish_reason_error(&choice) {
194                            stream_failed = true;
195                            yield Err(err);
196                            break;
197                        }
198
199                        let Some(content) = choice.content else {
200                            tracing::debug!(finish_reason = ?final_finish_reason, "Streaming candidate missing content");
201                            // Gemini's final chunk may carry finishReason with no content — break instead of skip
202                            if should_stop {
203                                break;
204                            }
205                            continue;
206                        };
207
208                        if content.parts.is_empty() {
209                            tracing::trace!(reason = ?choice.finish_reason, "There is no part in the streaming content");
210                        }
211
212                        for part in content.parts {
213                            match part {
214                                Part {
215                                    part: PartKind::Text(text),
216                                    thought: Some(true),
217                                    thought_signature,
218                                    ..
219                                } => {
220                                    if !text.is_empty() {
221                                        if thought_signature.is_some() {
222                                            // Signature arrives on the final chunk of a
223                                            // thinking block; emit a full Reasoning so the
224                                            // core accumulator captures the signature for
225                                            // Gemini 3+ roundtrip.
226                                            yield Ok(streaming::RawStreamingChoice::Reasoning {
227                                                id: None,
228                                                content: ReasoningContent::Text {
229                                                    text,
230                                                    signature: thought_signature,
231                                                },
232                                            });
233                                        } else {
234                                            yield Ok(streaming::RawStreamingChoice::ReasoningDelta {
235                                                id: None,
236                                                reasoning: text,
237                                            });
238                                        }
239                                    }
240                                },
241                                Part {
242                                    part: PartKind::Text(text),
243                                    ..
244                                } => {
245                                    if !text.is_empty() {
246                                        yield Ok(streaming::RawStreamingChoice::Message(text));
247                                    }
248                                },
249                                Part {
250                                    part: PartKind::FunctionCall(function_call),
251                                    thought_signature,
252                                    ..
253                                } => {
254                                    let tool_call = streaming::RawStreamingToolCall::new(
255                                        function_call.name.clone(),
256                                        function_call.name,
257                                        function_call.args,
258                                    )
259                                    .with_signature(thought_signature);
260                                    let tool_call = if let Some(id) = function_call.id {
261                                        tool_call.with_call_id(id)
262                                    } else {
263                                        tool_call
264                                    };
265                                    yield Ok(streaming::RawStreamingChoice::ToolCall(
266                                        tool_call
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                                "id": "call-weather"
499                            }
500                        },
501                        {
502                            "functionCall": {
503                                "name": "get_temperature",
504                                "args": {"location": "New York"},
505                                "id": "call-temperature"
506                            }
507                        }
508                    ],
509                    "role": "model"
510                },
511                "finishReason": "STOP",
512                "index": 0
513            }],
514            "usageMetadata": {
515                "promptTokenCount": 50,
516                "candidatesTokenCount": 20,
517                "totalTokenCount": 70
518            }
519        });
520
521        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
522        let content = response.candidates[0]
523            .content
524            .as_ref()
525            .expect("candidate should contain content");
526        assert_eq!(content.parts.len(), 2);
527
528        // Verify first tool call
529        if let Part {
530            part: PartKind::FunctionCall(call),
531            ..
532        } = &content.parts[0]
533        {
534            assert_eq!(call.name, "get_weather");
535            assert_eq!(call.id.as_deref(), Some("call-weather"));
536        } else {
537            panic!("Expected function call at index 0");
538        }
539
540        // Verify second tool call
541        if let Part {
542            part: PartKind::FunctionCall(call),
543            ..
544        } = &content.parts[1]
545        {
546            assert_eq!(call.name, "get_temperature");
547            assert_eq!(call.id.as_deref(), Some("call-temperature"));
548        } else {
549            panic!("Expected function call at index 1");
550        }
551    }
552
553    #[test]
554    fn test_deserialize_stream_response_with_mixed_parts() {
555        let json_data = json!({
556            "candidates": [{
557                "content": {
558                    "parts": [
559                        {
560                            "text": "Let me think about this...",
561                            "thought": true
562                        },
563                        {
564                            "text": "Here's my response: "
565                        },
566                        {
567                            "functionCall": {
568                                "name": "search",
569                                "args": {"query": "rust async"}
570                            }
571                        },
572                        {
573                            "text": "I found the answer!"
574                        }
575                    ],
576                    "role": "model"
577                },
578                "finishReason": "STOP",
579                "index": 0
580            }],
581            "usageMetadata": {
582                "promptTokenCount": 100,
583                "candidatesTokenCount": 50,
584                "thoughtsTokenCount": 15,
585                "totalTokenCount": 165
586            }
587        });
588
589        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
590        let content = response.candidates[0]
591            .content
592            .as_ref()
593            .expect("candidate should contain content");
594        let parts = &content.parts;
595        assert_eq!(parts.len(), 4);
596
597        // Verify reasoning (thought) part
598        if let Part {
599            part: PartKind::Text(text),
600            thought: Some(true),
601            ..
602        } = &parts[0]
603        {
604            assert_eq!(text, "Let me think about this...");
605        } else {
606            panic!("Expected thought part at index 0");
607        }
608
609        // Verify regular text
610        if let Part {
611            part: PartKind::Text(text),
612            thought,
613            ..
614        } = &parts[1]
615        {
616            assert_eq!(text, "Here's my response: ");
617            assert!(thought.is_none() || thought == &Some(false));
618        } else {
619            panic!("Expected text part at index 1");
620        }
621
622        // Verify tool call
623        if let Part {
624            part: PartKind::FunctionCall(call),
625            ..
626        } = &parts[2]
627        {
628            assert_eq!(call.name, "search");
629        } else {
630            panic!("Expected function call at index 2");
631        }
632
633        // Verify final text
634        if let Part {
635            part: PartKind::Text(text),
636            ..
637        } = &parts[3]
638        {
639            assert_eq!(text, "I found the answer!");
640        } else {
641            panic!("Expected text part at index 3");
642        }
643    }
644
645    #[test]
646    fn test_deserialize_stream_response_with_empty_parts() {
647        let json_data = json!({
648            "candidates": [{
649                "content": {
650                    "parts": [],
651                    "role": "model"
652                },
653                "finishReason": "STOP",
654                "index": 0
655            }],
656            "usageMetadata": {
657                "promptTokenCount": 10,
658                "candidatesTokenCount": 0,
659                "totalTokenCount": 10
660            }
661        });
662
663        let response: StreamGenerateContentResponse = serde_json::from_value(json_data).unwrap();
664        let content = response.candidates[0]
665            .content
666            .as_ref()
667            .expect("candidate should contain content");
668        assert_eq!(content.parts.len(), 0);
669    }
670
671    #[test]
672    fn test_partial_usage_token_calculation() {
673        let usage = PartialUsage {
674            total_token_count: 100,
675            cached_content_token_count: Some(20),
676            candidates_token_count: Some(30),
677            thoughts_token_count: Some(10),
678            prompt_token_count: 40,
679            prompt_tokens_details: None,
680            cache_tokens_details: None,
681            candidates_tokens_details: None,
682            tool_use_prompt_token_count: Some(12),
683            tool_use_prompt_tokens_details: None,
684            traffic_type: None,
685        };
686
687        let token_usage = usage.token_usage();
688        assert_eq!(token_usage.input_tokens, 40);
689        assert_eq!(token_usage.cached_input_tokens, 20);
690        assert_eq!(token_usage.output_tokens, 30);
691        assert_eq!(token_usage.reasoning_tokens, 10);
692        assert_eq!(token_usage.tool_use_prompt_tokens, 12);
693        assert_eq!(token_usage.total_tokens, 100);
694    }
695
696    #[test]
697    fn test_partial_usage_with_missing_counts() {
698        let usage = PartialUsage {
699            total_token_count: 50,
700            cached_content_token_count: None,
701            candidates_token_count: Some(30),
702            thoughts_token_count: None,
703            prompt_token_count: 20,
704            prompt_tokens_details: None,
705            cache_tokens_details: None,
706            candidates_tokens_details: None,
707            tool_use_prompt_token_count: None,
708            tool_use_prompt_tokens_details: None,
709            traffic_type: None,
710        };
711
712        let token_usage = usage.token_usage();
713        assert_eq!(token_usage.input_tokens, 20);
714        assert_eq!(token_usage.cached_input_tokens, 0);
715        assert_eq!(token_usage.output_tokens, 30);
716        assert_eq!(token_usage.reasoning_tokens, 0);
717        assert_eq!(token_usage.total_tokens, 50);
718    }
719
720    #[test]
721    fn test_partial_usage_deserializes_without_total_token_count() {
722        // Gemini's proto3-JSON encoding omits fields whose value is the default (0),
723        // so `totalTokenCount` is absent on short/empty/blocked generations.
724        let usage: PartialUsage =
725            serde_json::from_str(r#"{"promptTokenCount": 12}"#).expect("should deserialize");
726        assert_eq!(usage.total_token_count, 0);
727        assert_eq!(usage.prompt_token_count, 12);
728    }
729
730    #[test]
731    fn test_streaming_completion_response_has_finish_reason_and_model_version() {
732        use super::super::completion::gemini_api_types::FinishReason;
733
734        let response = StreamingCompletionResponse {
735            usage_metadata: PartialUsage::default(),
736            finish_reason: Some(FinishReason::Stop),
737            finish_message: None,
738            model_version: Some("gemini-2.5-pro-preview-05-06".to_string()),
739        };
740
741        assert!(matches!(response.finish_reason, Some(FinishReason::Stop)));
742        assert_eq!(
743            response.model_version.as_deref(),
744            Some("gemini-2.5-pro-preview-05-06")
745        );
746
747        let json = serde_json::to_string(&response).unwrap();
748        let deserialized: StreamingCompletionResponse = serde_json::from_str(&json).unwrap();
749        assert!(matches!(
750            deserialized.finish_reason,
751            Some(FinishReason::Stop)
752        ));
753        assert_eq!(
754            deserialized.model_version.as_deref(),
755            Some("gemini-2.5-pro-preview-05-06")
756        );
757    }
758
759    #[test]
760    fn test_streaming_completion_response_token_usage() {
761        let response = StreamingCompletionResponse {
762            usage_metadata: PartialUsage {
763                total_token_count: 150,
764                cached_content_token_count: None,
765                candidates_token_count: Some(75),
766                thoughts_token_count: None,
767                prompt_token_count: 75,
768                prompt_tokens_details: None,
769                cache_tokens_details: None,
770                candidates_tokens_details: None,
771                tool_use_prompt_token_count: None,
772                tool_use_prompt_tokens_details: None,
773                traffic_type: None,
774            },
775            finish_reason: Some(FinishReason::Stop),
776            finish_message: None,
777            model_version: Some("gemini-2.0-flash-001".to_string()),
778        };
779
780        let token_usage = response.token_usage();
781        assert_eq!(token_usage.input_tokens, 75);
782        assert_eq!(token_usage.output_tokens, 75);
783        assert_eq!(token_usage.reasoning_tokens, 0);
784        assert_eq!(token_usage.cached_input_tokens, 0);
785        assert_eq!(token_usage.total_tokens, 150);
786        assert!(matches!(response.finish_reason, Some(FinishReason::Stop)));
787        assert_eq!(
788            response.model_version.as_deref(),
789            Some("gemini-2.0-flash-001")
790        );
791    }
792
793    #[test]
794    fn test_partial_usage_serde_roundtrip_with_all_optional_fields() {
795        let json_data = serde_json::json!({
796            "promptTokenCount": 100,
797            "cachedContentTokenCount": 25,
798            "candidatesTokenCount": 50,
799            "thoughtsTokenCount": 15,
800            "totalTokenCount": 190,
801            "promptTokensDetails": [
802                { "modality": "TEXT", "tokenCount": 80 },
803                { "modality": "IMAGE", "tokenCount": 20 }
804            ],
805            "cacheTokensDetails": [
806                { "modality": "TEXT", "tokenCount": 25 }
807            ],
808            "candidatesTokensDetails": [
809                { "modality": "TEXT", "tokenCount": 50 }
810            ],
811            "toolUsePromptTokenCount": 12,
812            "toolUsePromptTokensDetails": [
813                { "modality": "TEXT", "tokenCount": 12 }
814            ],
815            "trafficType": "PROVISIONED_THROUGHPUT"
816        });
817
818        let usage: PartialUsage = serde_json::from_value(json_data).unwrap();
819        assert_eq!(usage.prompt_token_count, 100);
820        assert_eq!(usage.cached_content_token_count, Some(25));
821        assert_eq!(usage.candidates_token_count, Some(50));
822        assert_eq!(usage.thoughts_token_count, Some(15));
823        assert_eq!(usage.total_token_count, 190);
824        assert!(usage.prompt_tokens_details.is_some());
825        assert_eq!(usage.prompt_tokens_details.as_ref().unwrap().len(), 2);
826        assert!(usage.cache_tokens_details.is_some());
827        assert!(usage.candidates_tokens_details.is_some());
828        assert_eq!(usage.tool_use_prompt_token_count, Some(12));
829        assert!(usage.tool_use_prompt_tokens_details.is_some());
830        assert!(matches!(
831            usage.traffic_type,
832            Some(TrafficType::ProvisionedThroughput)
833        ));
834
835        let token_usage = usage.token_usage();
836        assert_eq!(token_usage.input_tokens, 100);
837        assert_eq!(token_usage.cached_input_tokens, 25);
838        assert_eq!(token_usage.output_tokens, 50);
839        assert_eq!(token_usage.reasoning_tokens, 15);
840        assert_eq!(token_usage.tool_use_prompt_tokens, 12);
841        assert_eq!(token_usage.total_tokens, 190);
842    }
843}