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 #[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 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 let Some(choice) = data.candidates.into_iter().next() else {
180 tracing::debug!("There is no content candidate");
181 continue;
182 };
183
184 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 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 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 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 "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 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 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 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 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 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 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 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}