1use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "type")]
16pub enum Message {
17 #[serde(rename = "user")]
19 User(UserMessage),
20 #[serde(rename = "assistant")]
22 Assistant(AssistantMessage),
23 #[serde(rename = "system")]
25 System(SystemMessage),
26}
27
28impl Message {
29 pub fn uuid(&self) -> &Uuid {
30 match self {
31 Message::User(m) => &m.uuid,
32 Message::Assistant(m) => &m.uuid,
33 Message::System(m) => &m.uuid,
34 }
35 }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct UserMessage {
41 pub uuid: Uuid,
42 pub timestamp: String,
43 pub content: Vec<ContentBlock>,
44 #[serde(default)]
47 pub is_meta: bool,
48 #[serde(default)]
50 pub is_compact_summary: bool,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct AssistantMessage {
56 pub uuid: Uuid,
57 pub timestamp: String,
58 pub content: Vec<ContentBlock>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub model: Option<String>,
62 #[serde(skip_serializing_if = "Option::is_none")]
64 pub usage: Option<Usage>,
65 #[serde(skip_serializing_if = "Option::is_none")]
67 pub stop_reason: Option<StopReason>,
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub request_id: Option<String>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SystemMessage {
76 pub uuid: Uuid,
77 pub timestamp: String,
78 pub subtype: SystemMessageType,
79 pub content: String,
80 #[serde(default)]
81 pub level: MessageLevel,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
86#[serde(rename_all = "snake_case")]
87pub enum SystemMessageType {
88 Informational,
89 ApiError,
90 CompactBoundary,
91 TurnDuration,
92 MemorySaved,
93 ToolProgress,
94}
95
96#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
98#[serde(rename_all = "snake_case")]
99pub enum MessageLevel {
100 #[default]
101 Info,
102 Warning,
103 Error,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
112#[serde(tag = "type")]
113pub enum ContentBlock {
114 #[serde(rename = "text")]
116 Text { text: String },
117
118 #[serde(rename = "tool_use")]
120 ToolUse {
121 id: String,
122 name: String,
123 input: serde_json::Value,
124 },
125
126 #[serde(rename = "tool_result")]
130 ToolResult {
131 tool_use_id: String,
132 content: String,
133 #[serde(default)]
134 is_error: bool,
135 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 extra_content: Vec<ToolResultBlock>,
138 },
139
140 #[serde(rename = "thinking")]
142 Thinking {
143 thinking: String,
144 #[serde(skip_serializing_if = "Option::is_none")]
145 signature: Option<String>,
146 },
147
148 #[serde(rename = "image")]
150 Image {
151 #[serde(rename = "media_type")]
152 media_type: String,
153 data: String,
154 },
155
156 #[serde(rename = "document")]
158 Document {
159 #[serde(rename = "media_type")]
160 media_type: String,
161 data: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 title: Option<String>,
164 },
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(tag = "type")]
170pub enum ToolResultBlock {
171 #[serde(rename = "text")]
172 Text { text: String },
173 #[serde(rename = "image")]
174 Image {
175 #[serde(rename = "media_type")]
176 media_type: String,
177 data: String,
178 },
179}
180
181impl ContentBlock {
182 pub fn as_text(&self) -> Option<&str> {
184 match self {
185 ContentBlock::Text { text } => Some(text),
186 _ => None,
187 }
188 }
189
190 pub fn as_tool_use(&self) -> Option<(&str, &str, &serde_json::Value)> {
192 match self {
193 ContentBlock::ToolUse { id, name, input } => Some((id, name, input)),
194 _ => None,
195 }
196 }
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
205pub struct Usage {
206 pub input_tokens: u64,
207 pub output_tokens: u64,
208 #[serde(default)]
209 pub cache_creation_input_tokens: u64,
210 #[serde(default)]
211 pub cache_read_input_tokens: u64,
212}
213
214impl Usage {
215 pub fn total(&self) -> u64 {
217 self.input_tokens
218 + self.output_tokens
219 + self.cache_creation_input_tokens
220 + self.cache_read_input_tokens
221 }
222
223 pub fn merge(&mut self, other: &Usage) {
225 self.input_tokens = other.input_tokens;
226 self.output_tokens += other.output_tokens;
227 self.cache_creation_input_tokens = other.cache_creation_input_tokens;
228 self.cache_read_input_tokens = other.cache_read_input_tokens;
229 }
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
234#[serde(rename_all = "snake_case")]
235pub enum StopReason {
236 EndTurn,
237 MaxTokens,
238 ToolUse,
239 StopSequence,
240}
241
242pub fn user_message(text: impl Into<String>) -> Message {
244 Message::User(UserMessage {
245 uuid: Uuid::new_v4(),
246 timestamp: chrono::Utc::now().to_rfc3339(),
247 content: vec![ContentBlock::Text { text: text.into() }],
248 is_meta: false,
249 is_compact_summary: false,
250 })
251}
252
253pub fn image_block_from_file(path: &std::path::Path) -> Result<ContentBlock, String> {
258 let data = std::fs::read(path).map_err(|e| format!("Failed to read image: {e}"))?;
259
260 let media_type = match path.extension().and_then(|e| e.to_str()) {
261 Some("png") => "image/png",
262 Some("jpg" | "jpeg") => "image/jpeg",
263 Some("gif") => "image/gif",
264 Some("webp") => "image/webp",
265 Some("svg") => "image/svg+xml",
266 _ => "application/octet-stream",
267 };
268
269 use std::io::Write;
270 let mut encoded = String::new();
271 {
272 let mut encoder = base64_encode_writer(&mut encoded);
273 encoder
274 .write_all(&data)
275 .map_err(|e| format!("base64 error: {e}"))?;
276 }
277
278 Ok(ContentBlock::Image {
279 media_type: media_type.to_string(),
280 data: encoded,
281 })
282}
283
284fn base64_encode_writer(output: &mut String) -> Base64Writer<'_> {
286 Base64Writer {
287 output,
288 buffer: Vec::new(),
289 }
290}
291
292struct Base64Writer<'a> {
293 output: &'a mut String,
294 buffer: Vec<u8>,
295}
296
297impl<'a> std::io::Write for Base64Writer<'a> {
298 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
299 self.buffer.extend_from_slice(buf);
300 Ok(buf.len())
301 }
302 fn flush(&mut self) -> std::io::Result<()> {
303 const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
304 let mut i = 0;
305 while i + 2 < self.buffer.len() {
306 let b0 = self.buffer[i] as usize;
307 let b1 = self.buffer[i + 1] as usize;
308 let b2 = self.buffer[i + 2] as usize;
309 self.output.push(CHARS[b0 >> 2] as char);
310 self.output.push(CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
311 self.output
312 .push(CHARS[((b1 & 0xf) << 2) | (b2 >> 6)] as char);
313 self.output.push(CHARS[b2 & 0x3f] as char);
314 i += 3;
315 }
316 let remaining = self.buffer.len() - i;
317 if remaining == 1 {
318 let b0 = self.buffer[i] as usize;
319 self.output.push(CHARS[b0 >> 2] as char);
320 self.output.push(CHARS[(b0 & 3) << 4] as char);
321 self.output.push('=');
322 self.output.push('=');
323 } else if remaining == 2 {
324 let b0 = self.buffer[i] as usize;
325 let b1 = self.buffer[i + 1] as usize;
326 self.output.push(CHARS[b0 >> 2] as char);
327 self.output.push(CHARS[((b0 & 3) << 4) | (b1 >> 4)] as char);
328 self.output.push(CHARS[(b1 & 0xf) << 2] as char);
329 self.output.push('=');
330 }
331 Ok(())
332 }
333}
334
335pub fn image_message(path: &std::path::Path, caption: &str) -> Result<Message, String> {
337 let image = image_block_from_file(path)?;
338 Ok(Message::User(UserMessage {
339 uuid: Uuid::new_v4(),
340 timestamp: chrono::Utc::now().to_rfc3339(),
341 content: vec![
342 image,
343 ContentBlock::Text {
344 text: caption.to_string(),
345 },
346 ],
347 is_meta: false,
348 is_compact_summary: false,
349 }))
350}
351
352pub fn tool_result_message(tool_use_id: &str, content: &str, is_error: bool) -> Message {
354 Message::User(UserMessage {
355 uuid: Uuid::new_v4(),
356 timestamp: chrono::Utc::now().to_rfc3339(),
357 content: vec![ContentBlock::ToolResult {
358 tool_use_id: tool_use_id.to_string(),
359 content: content.to_string(),
360 is_error,
361 extra_content: vec![],
362 }],
363 is_meta: true,
364 is_compact_summary: false,
365 })
366}
367
368pub fn messages_to_api_params(messages: &[Message]) -> Vec<serde_json::Value> {
370 messages
371 .iter()
372 .filter_map(|msg| match msg {
373 Message::User(u) => Some(serde_json::json!({
374 "role": "user",
375 "content": content_blocks_to_api(&u.content),
376 })),
377 Message::Assistant(a) => Some(serde_json::json!({
378 "role": "assistant",
379 "content": content_blocks_to_api(&a.content),
380 })),
381 Message::System(_) => None,
383 })
384 .collect()
385}
386
387fn content_blocks_to_api(blocks: &[ContentBlock]) -> serde_json::Value {
388 let api_blocks: Vec<serde_json::Value> = blocks
389 .iter()
390 .map(|block| match block {
391 ContentBlock::Text { text } => serde_json::json!({
392 "type": "text",
393 "text": text,
394 }),
395 ContentBlock::ToolUse { id, name, input } => serde_json::json!({
396 "type": "tool_use",
397 "id": id,
398 "name": name,
399 "input": input,
400 }),
401 ContentBlock::ToolResult {
402 tool_use_id,
403 content,
404 is_error,
405 ..
406 } => serde_json::json!({
407 "type": "tool_result",
408 "tool_use_id": tool_use_id,
409 "content": content,
410 "is_error": is_error,
411 }),
412 ContentBlock::Thinking {
413 thinking,
414 signature,
415 } => serde_json::json!({
416 "type": "thinking",
417 "thinking": thinking,
418 "signature": signature,
419 }),
420 ContentBlock::Image { media_type, data } => serde_json::json!({
421 "type": "image",
422 "source": {
423 "type": "base64",
424 "media_type": media_type,
425 "data": data,
426 }
427 }),
428 ContentBlock::Document {
429 media_type,
430 data,
431 title,
432 } => {
433 let mut doc = serde_json::json!({
434 "type": "document",
435 "source": {
436 "type": "base64",
437 "media_type": media_type,
438 "data": data,
439 }
440 });
441 if let Some(t) = title {
442 doc["title"] = serde_json::json!(t);
443 }
444 doc
445 }
446 })
447 .collect();
448
449 if api_blocks.len() == 1
451 && let Some(text) = blocks[0].as_text()
452 {
453 return serde_json::Value::String(text.to_string());
454 }
455
456 serde_json::Value::Array(api_blocks)
457}
458
459pub fn messages_to_api_params_cached(messages: &[Message]) -> Vec<serde_json::Value> {
465 let user_indices: Vec<usize> = messages
467 .iter()
468 .enumerate()
469 .filter(|(_, m)| matches!(m, Message::User(u) if !u.is_meta))
470 .map(|(i, _)| i)
471 .collect();
472
473 let cache_index = if user_indices.len() >= 2 {
474 Some(user_indices[user_indices.len() - 2])
475 } else {
476 None
477 };
478
479 messages
480 .iter()
481 .enumerate()
482 .filter_map(|(i, msg)| match msg {
483 Message::User(u) => {
484 let mut content = content_blocks_to_api(&u.content);
485 if Some(i) == cache_index
487 && let serde_json::Value::Array(ref mut blocks) = content
488 && let Some(last) = blocks.last_mut()
489 {
490 last["cache_control"] = serde_json::json!({"type": "ephemeral"});
491 }
492 Some(serde_json::json!({
493 "role": "user",
494 "content": content,
495 }))
496 }
497 Message::Assistant(a) => Some(serde_json::json!({
498 "role": "assistant",
499 "content": content_blocks_to_api(&a.content),
500 })),
501 Message::System(_) => None,
502 })
503 .collect()
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_user_message_creates_text() {
512 let msg = user_message("hello");
513 if let Message::User(u) = &msg {
514 assert_eq!(u.content.len(), 1);
515 assert_eq!(u.content[0].as_text(), Some("hello"));
516 assert!(!u.is_meta);
517 } else {
518 panic!("Expected User");
519 }
520 }
521
522 #[test]
523 fn test_tool_result_message_success() {
524 let msg = tool_result_message("c1", "output", false);
525 if let Message::User(u) = &msg {
526 assert!(u.is_meta);
527 if let ContentBlock::ToolResult {
528 tool_use_id,
529 is_error,
530 ..
531 } = &u.content[0]
532 {
533 assert_eq!(tool_use_id, "c1");
534 assert!(!is_error);
535 }
536 }
537 }
538
539 #[test]
540 fn test_tool_result_message_error() {
541 let msg = tool_result_message("c2", "fail", true);
542 if let Message::User(u) = &msg
543 && let ContentBlock::ToolResult { is_error, .. } = &u.content[0]
544 {
545 assert!(is_error);
546 }
547 }
548
549 #[test]
550 fn test_as_text() {
551 assert_eq!(
552 ContentBlock::Text { text: "hi".into() }.as_text(),
553 Some("hi")
554 );
555 assert_eq!(
556 ContentBlock::ToolUse {
557 id: "1".into(),
558 name: "X".into(),
559 input: serde_json::json!({})
560 }
561 .as_text(),
562 None
563 );
564 }
565
566 #[test]
567 fn test_as_tool_use() {
568 let b = ContentBlock::ToolUse {
569 id: "a".into(),
570 name: "B".into(),
571 input: serde_json::json!(1),
572 };
573 let (id, name, _) = b.as_tool_use().unwrap();
574 assert_eq!(id, "a");
575 assert_eq!(name, "B");
576 assert!(
577 ContentBlock::Text { text: "x".into() }
578 .as_tool_use()
579 .is_none()
580 );
581 }
582
583 #[test]
584 fn test_usage_total() {
585 let u = Usage {
586 input_tokens: 10,
587 output_tokens: 20,
588 cache_creation_input_tokens: 3,
589 cache_read_input_tokens: 7,
590 };
591 assert_eq!(u.total(), 40);
592 }
593
594 #[test]
595 fn test_usage_merge() {
596 let mut u = Usage {
597 input_tokens: 100,
598 output_tokens: 50,
599 ..Default::default()
600 };
601 u.merge(&Usage {
602 input_tokens: 200,
603 output_tokens: 30,
604 cache_creation_input_tokens: 5,
605 cache_read_input_tokens: 10,
606 });
607 assert_eq!(u.input_tokens, 200);
608 assert_eq!(u.output_tokens, 80);
609 assert_eq!(u.cache_creation_input_tokens, 5);
610 }
611
612 #[test]
613 fn test_usage_default() {
614 assert_eq!(Usage::default().total(), 0);
615 }
616
617 #[test]
618 fn test_message_uuid_accessible() {
619 let _ = user_message("t").uuid();
620 }
621
622 #[test]
623 fn test_messages_to_api_params_filters_system() {
624 let messages = vec![user_message("hi")];
625 let params = messages_to_api_params(&messages);
626 assert_eq!(params.len(), 1);
627 assert_eq!(params[0]["role"], "user");
628 }
629
630 #[test]
631 fn test_serde_roundtrip_user_message() {
632 let msg = user_message("round trip test");
633 let json = serde_json::to_string(&msg).unwrap();
634 let deserialized: Message = serde_json::from_str(&json).unwrap();
635 if let Message::User(u) = &deserialized {
636 assert_eq!(u.content[0].as_text(), Some("round trip test"));
637 assert!(!u.is_meta);
638 assert!(!u.is_compact_summary);
639 } else {
640 panic!("Expected User after round-trip");
641 }
642 }
643
644 #[test]
645 fn test_serde_roundtrip_assistant_message() {
646 let msg = Message::Assistant(AssistantMessage {
647 uuid: Uuid::new_v4(),
648 timestamp: "2025-01-01T00:00:00Z".into(),
649 content: vec![ContentBlock::Text {
650 text: "hello".into(),
651 }],
652 model: Some("test-model".into()),
653 usage: Some(Usage {
654 input_tokens: 10,
655 output_tokens: 20,
656 ..Default::default()
657 }),
658 stop_reason: Some(StopReason::EndTurn),
659 request_id: None,
660 });
661 let json = serde_json::to_string(&msg).unwrap();
662 let deserialized: Message = serde_json::from_str(&json).unwrap();
663 if let Message::Assistant(a) = &deserialized {
664 assert_eq!(a.content[0].as_text(), Some("hello"));
665 assert_eq!(a.model.as_deref(), Some("test-model"));
666 assert_eq!(a.stop_reason, Some(StopReason::EndTurn));
667 } else {
668 panic!("Expected Assistant after round-trip");
669 }
670 }
671
672 #[test]
673 fn test_serde_roundtrip_system_message() {
674 let msg = Message::System(SystemMessage {
675 uuid: Uuid::new_v4(),
676 timestamp: "2025-01-01T00:00:00Z".into(),
677 subtype: SystemMessageType::Informational,
678 content: "info".into(),
679 level: MessageLevel::Warning,
680 });
681 let json = serde_json::to_string(&msg).unwrap();
682 let deserialized: Message = serde_json::from_str(&json).unwrap();
683 if let Message::System(s) = &deserialized {
684 assert_eq!(s.subtype, SystemMessageType::Informational);
685 assert_eq!(s.level, MessageLevel::Warning);
686 assert_eq!(s.content, "info");
687 } else {
688 panic!("Expected System after round-trip");
689 }
690 }
691
692 #[test]
693 fn test_as_text_returns_none_for_tool_result() {
694 let block = ContentBlock::ToolResult {
695 tool_use_id: "t1".into(),
696 content: "result".into(),
697 is_error: false,
698 extra_content: vec![],
699 };
700 assert!(block.as_text().is_none());
701 }
702
703 #[test]
704 fn test_as_text_returns_none_for_thinking() {
705 let block = ContentBlock::Thinking {
706 thinking: "deep thought".into(),
707 signature: None,
708 };
709 assert!(block.as_text().is_none());
710 }
711
712 #[test]
713 fn test_as_text_returns_none_for_image() {
714 let block = ContentBlock::Image {
715 media_type: "image/png".into(),
716 data: "abc".into(),
717 };
718 assert!(block.as_text().is_none());
719 }
720
721 #[test]
722 fn test_as_text_returns_none_for_document() {
723 let block = ContentBlock::Document {
724 media_type: "application/pdf".into(),
725 data: "abc".into(),
726 title: Some("doc".into()),
727 };
728 assert!(block.as_text().is_none());
729 }
730
731 #[test]
732 fn test_as_tool_use_returns_none_for_non_tool_use() {
733 assert!(
734 ContentBlock::ToolResult {
735 tool_use_id: "t".into(),
736 content: "c".into(),
737 is_error: false,
738 extra_content: vec![],
739 }
740 .as_tool_use()
741 .is_none()
742 );
743 assert!(
744 ContentBlock::Thinking {
745 thinking: "t".into(),
746 signature: None,
747 }
748 .as_tool_use()
749 .is_none()
750 );
751 assert!(
752 ContentBlock::Image {
753 media_type: "image/png".into(),
754 data: "d".into(),
755 }
756 .as_tool_use()
757 .is_none()
758 );
759 assert!(
760 ContentBlock::Document {
761 media_type: "application/pdf".into(),
762 data: "d".into(),
763 title: None,
764 }
765 .as_tool_use()
766 .is_none()
767 );
768 }
769
770 #[test]
771 fn test_user_message_sets_is_compact_summary_false() {
772 let msg = user_message("test");
773 if let Message::User(u) = &msg {
774 assert!(!u.is_compact_summary);
775 } else {
776 panic!("Expected User");
777 }
778 }
779
780 #[test]
781 fn test_tool_result_message_sets_is_meta_true() {
782 let msg = tool_result_message("id1", "output", false);
783 if let Message::User(u) = &msg {
784 assert!(u.is_meta);
785 } else {
786 panic!("Expected User");
787 }
788 }
789
790 #[test]
791 fn test_messages_to_api_params_mixed_filters_system() {
792 let messages = vec![
793 user_message("hello"),
794 Message::Assistant(AssistantMessage {
795 uuid: Uuid::new_v4(),
796 timestamp: String::new(),
797 content: vec![ContentBlock::Text {
798 text: "hi back".into(),
799 }],
800 model: None,
801 usage: None,
802 stop_reason: None,
803 request_id: None,
804 }),
805 Message::System(SystemMessage {
806 uuid: Uuid::new_v4(),
807 timestamp: String::new(),
808 subtype: SystemMessageType::Informational,
809 content: "should be filtered".into(),
810 level: MessageLevel::Info,
811 }),
812 user_message("follow up"),
813 ];
814 let params = messages_to_api_params(&messages);
815 assert_eq!(params.len(), 3);
817 assert_eq!(params[0]["role"], "user");
818 assert_eq!(params[1]["role"], "assistant");
819 assert_eq!(params[2]["role"], "user");
820 }
821
822 #[test]
823 fn test_messages_to_api_params_single_text_uses_string() {
824 let messages = vec![user_message("simple text")];
825 let params = messages_to_api_params(&messages);
826 assert!(params[0]["content"].is_string());
828 assert_eq!(params[0]["content"], "simple text");
829 }
830
831 #[test]
832 fn test_messages_to_api_params_multiple_blocks_uses_array() {
833 let msg = Message::User(UserMessage {
834 uuid: Uuid::new_v4(),
835 timestamp: String::new(),
836 content: vec![
837 ContentBlock::Text {
838 text: "block1".into(),
839 },
840 ContentBlock::Text {
841 text: "block2".into(),
842 },
843 ],
844 is_meta: false,
845 is_compact_summary: false,
846 });
847 let params = messages_to_api_params(&[msg]);
848 assert!(params[0]["content"].is_array());
849 assert_eq!(params[0]["content"].as_array().unwrap().len(), 2);
850 }
851
852 #[test]
853 fn test_messages_to_api_params_cached_adds_cache_control() {
854 let messages = vec![
856 user_message("first"),
857 Message::Assistant(AssistantMessage {
858 uuid: Uuid::new_v4(),
859 timestamp: String::new(),
860 content: vec![ContentBlock::Text {
861 text: "resp".into(),
862 }],
863 model: None,
864 usage: None,
865 stop_reason: None,
866 request_id: None,
867 }),
868 Message::User(UserMessage {
870 uuid: Uuid::new_v4(),
871 timestamp: String::new(),
872 content: vec![
873 ContentBlock::Text { text: "a".into() },
874 ContentBlock::Text { text: "b".into() },
875 ],
876 is_meta: false,
877 is_compact_summary: false,
878 }),
879 ];
880 let params = messages_to_api_params_cached(&messages);
881 assert_eq!(params.len(), 3); }
887
888 #[test]
889 fn test_usage_merge_accumulates_output_replaces_input() {
890 let mut u = Usage {
891 input_tokens: 100,
892 output_tokens: 50,
893 cache_creation_input_tokens: 10,
894 cache_read_input_tokens: 5,
895 };
896 u.merge(&Usage {
897 input_tokens: 200,
898 output_tokens: 30,
899 cache_creation_input_tokens: 20,
900 cache_read_input_tokens: 15,
901 });
902 assert_eq!(u.input_tokens, 200);
904 assert_eq!(u.output_tokens, 80);
906 assert_eq!(u.cache_creation_input_tokens, 20);
908 assert_eq!(u.cache_read_input_tokens, 15);
909 }
910
911 #[test]
912 fn test_stop_reason_serde_roundtrip() {
913 for variant in [
914 StopReason::EndTurn,
915 StopReason::MaxTokens,
916 StopReason::ToolUse,
917 StopReason::StopSequence,
918 ] {
919 let json = serde_json::to_string(&variant).unwrap();
920 let deserialized: StopReason = serde_json::from_str(&json).unwrap();
921 assert_eq!(deserialized, variant);
922 }
923 }
924
925 #[test]
926 fn test_system_message_type_serde_roundtrip() {
927 for variant in [
928 SystemMessageType::Informational,
929 SystemMessageType::ApiError,
930 SystemMessageType::CompactBoundary,
931 SystemMessageType::TurnDuration,
932 SystemMessageType::MemorySaved,
933 SystemMessageType::ToolProgress,
934 ] {
935 let json = serde_json::to_string(&variant).unwrap();
936 let deserialized: SystemMessageType = serde_json::from_str(&json).unwrap();
937 assert_eq!(deserialized, variant);
938 }
939 }
940
941 #[test]
942 fn test_message_level_default_is_info() {
943 let level: MessageLevel = Default::default();
944 assert_eq!(level, MessageLevel::Info);
945 }
946
947 #[test]
948 fn test_tool_result_block_variants_constructible() {
949 let text_block = ToolResultBlock::Text {
950 text: "hello".into(),
951 };
952 let image_block = ToolResultBlock::Image {
953 media_type: "image/png".into(),
954 data: "base64data".into(),
955 };
956 let _ = serde_json::to_string(&text_block).unwrap();
958 let _ = serde_json::to_string(&image_block).unwrap();
959 }
960}