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 #[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 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 let Some(choice) = data.candidates.into_iter().next() else {
190 tracing::debug!("There is no content candidate");
191 continue;
192 };
193
194 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 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 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 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 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 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 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 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 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 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 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 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 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}