1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4
5pub(crate) fn deserialize_content_blocks<'de, D>(
7 deserializer: D,
8) -> Result<Vec<ContentBlock>, D::Error>
9where
10 D: Deserializer<'de>,
11{
12 let value: Value = Value::deserialize(deserializer)?;
13 match value {
14 Value::String(s) => Ok(vec![ContentBlock::Text(TextBlock {
15 text: s,
16 citations: Vec::new(),
17 })]),
18 Value::Array(_) => serde_json::from_value(value).map_err(serde::de::Error::custom),
19 _ => Err(serde::de::Error::custom(
20 "content must be a string or array",
21 )),
22 }
23}
24
25#[derive(Debug, Clone)]
30pub enum ContentBlock {
31 Text(TextBlock),
32 Image(ImageBlock),
33 Thinking(ThinkingBlock),
34 ToolUse(ToolUseBlock),
35 ToolResult(ToolResultBlock),
36 ServerToolUse(ServerToolUseBlock),
38 WebSearchToolResult(WebSearchToolResultBlock),
40 CodeExecutionToolResult(CodeExecutionToolResultBlock),
42 McpToolUse(McpToolUseBlock),
44 McpToolResult(McpToolResultBlock),
46 ContainerUpload(ContainerUploadBlock),
48 Fallback(FallbackBlock),
51 Unknown(Value),
54}
55
56impl ContentBlock {
57 pub fn block_type(&self) -> &str {
59 match self {
60 Self::Text(_) => "text",
61 Self::Image(_) => "image",
62 Self::Thinking(_) => "thinking",
63 Self::ToolUse(_) => "tool_use",
64 Self::ToolResult(_) => "tool_result",
65 Self::ServerToolUse(_) => "server_tool_use",
66 Self::WebSearchToolResult(_) => "web_search_tool_result",
67 Self::CodeExecutionToolResult(_) => "code_execution_tool_result",
68 Self::McpToolUse(_) => "mcp_tool_use",
69 Self::McpToolResult(_) => "mcp_tool_result",
70 Self::ContainerUpload(_) => "container_upload",
71 Self::Fallback(_) => "fallback",
72 Self::Unknown(v) => v.get("type").and_then(|t| t.as_str()).unwrap_or("unknown"),
73 }
74 }
75
76 pub fn is_unknown(&self) -> bool {
78 matches!(self, Self::Unknown(_))
79 }
80}
81
82impl Serialize for ContentBlock {
83 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
84 match self {
85 Self::Text(v) => serialize_tagged("text", v, serializer),
86 Self::Image(v) => serialize_tagged("image", v, serializer),
87 Self::Thinking(v) => serialize_tagged("thinking", v, serializer),
88 Self::ToolUse(v) => serialize_tagged("tool_use", v, serializer),
89 Self::ToolResult(v) => serialize_tagged("tool_result", v, serializer),
90 Self::ServerToolUse(v) => serialize_tagged("server_tool_use", v, serializer),
91 Self::WebSearchToolResult(v) => {
92 serialize_tagged("web_search_tool_result", v, serializer)
93 }
94 Self::CodeExecutionToolResult(v) => {
95 serialize_tagged("code_execution_tool_result", v, serializer)
96 }
97 Self::McpToolUse(v) => serialize_tagged("mcp_tool_use", v, serializer),
98 Self::McpToolResult(v) => serialize_tagged("mcp_tool_result", v, serializer),
99 Self::ContainerUpload(v) => serialize_tagged("container_upload", v, serializer),
100 Self::Fallback(v) => serialize_tagged("fallback", v, serializer),
101 Self::Unknown(v) => v.serialize(serializer),
102 }
103 }
104}
105
106impl<'de> Deserialize<'de> for ContentBlock {
107 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108 let value = Value::deserialize(deserializer)?;
109 let type_str = value
110 .get("type")
111 .and_then(|v| v.as_str())
112 .ok_or_else(|| serde::de::Error::missing_field("type"))?;
113
114 match type_str {
115 "text" => serde_json::from_value(value)
116 .map(ContentBlock::Text)
117 .map_err(serde::de::Error::custom),
118 "image" => serde_json::from_value(value)
119 .map(ContentBlock::Image)
120 .map_err(serde::de::Error::custom),
121 "thinking" => serde_json::from_value(value)
122 .map(ContentBlock::Thinking)
123 .map_err(serde::de::Error::custom),
124 "tool_use" => serde_json::from_value(value)
125 .map(ContentBlock::ToolUse)
126 .map_err(serde::de::Error::custom),
127 "tool_result" => serde_json::from_value(value)
128 .map(ContentBlock::ToolResult)
129 .map_err(serde::de::Error::custom),
130 "server_tool_use" => serde_json::from_value(value)
131 .map(ContentBlock::ServerToolUse)
132 .map_err(serde::de::Error::custom),
133 "web_search_tool_result" => serde_json::from_value(value)
134 .map(ContentBlock::WebSearchToolResult)
135 .map_err(serde::de::Error::custom),
136 "code_execution_tool_result" => serde_json::from_value(value)
137 .map(ContentBlock::CodeExecutionToolResult)
138 .map_err(serde::de::Error::custom),
139 "mcp_tool_use" => serde_json::from_value(value)
140 .map(ContentBlock::McpToolUse)
141 .map_err(serde::de::Error::custom),
142 "mcp_tool_result" => serde_json::from_value(value)
143 .map(ContentBlock::McpToolResult)
144 .map_err(serde::de::Error::custom),
145 "container_upload" => serde_json::from_value(value)
146 .map(ContentBlock::ContainerUpload)
147 .map_err(serde::de::Error::custom),
148 "fallback" => serde_json::from_value(value)
149 .map(ContentBlock::Fallback)
150 .map_err(serde::de::Error::custom),
151 _ => Ok(ContentBlock::Unknown(value)),
152 }
153 }
154}
155
156fn serialize_tagged<S: Serializer, T: Serialize>(
158 tag: &str,
159 value: &T,
160 serializer: S,
161) -> Result<S::Ok, S::Error> {
162 let mut map = serde_json::to_value(value).map_err(serde::ser::Error::custom)?;
163 if let Some(obj) = map.as_object_mut() {
164 obj.insert("type".to_string(), Value::String(tag.to_string()));
165 }
166 map.serialize(serializer)
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct TextBlock {
172 pub text: String,
173 #[serde(default, skip_serializing_if = "Vec::is_empty")]
176 pub citations: Vec<Citation>,
177}
178
179#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
189pub struct Citation {
190 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
192 pub citation_type: Option<String>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub url: Option<String>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub title: Option<String>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub cited_text: Option<String>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub document_index: Option<u32>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub document_title: Option<String>,
208 #[serde(flatten)]
210 pub extra: serde_json::Map<String, Value>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct ImageBlock {
216 pub source: ImageSource,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Hash)]
221pub enum ImageSourceType {
222 Base64,
224 Unknown(String),
226}
227
228impl ImageSourceType {
229 pub fn as_str(&self) -> &str {
230 match self {
231 Self::Base64 => "base64",
232 Self::Unknown(s) => s.as_str(),
233 }
234 }
235}
236
237impl fmt::Display for ImageSourceType {
238 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239 f.write_str(self.as_str())
240 }
241}
242
243impl From<&str> for ImageSourceType {
244 fn from(s: &str) -> Self {
245 match s {
246 "base64" => Self::Base64,
247 other => Self::Unknown(other.to_string()),
248 }
249 }
250}
251
252impl Serialize for ImageSourceType {
253 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
254 serializer.serialize_str(self.as_str())
255 }
256}
257
258impl<'de> Deserialize<'de> for ImageSourceType {
259 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
260 let s = String::deserialize(deserializer)?;
261 Ok(Self::from(s.as_str()))
262 }
263}
264
265#[derive(Debug, Clone, PartialEq, Eq, Hash)]
267pub enum MediaType {
268 Jpeg,
270 Png,
272 Gif,
274 Webp,
276 Unknown(String),
278}
279
280impl MediaType {
281 pub fn as_str(&self) -> &str {
282 match self {
283 Self::Jpeg => "image/jpeg",
284 Self::Png => "image/png",
285 Self::Gif => "image/gif",
286 Self::Webp => "image/webp",
287 Self::Unknown(s) => s.as_str(),
288 }
289 }
290}
291
292impl fmt::Display for MediaType {
293 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294 f.write_str(self.as_str())
295 }
296}
297
298impl From<&str> for MediaType {
299 fn from(s: &str) -> Self {
300 match s {
301 "image/jpeg" => Self::Jpeg,
302 "image/png" => Self::Png,
303 "image/gif" => Self::Gif,
304 "image/webp" => Self::Webp,
305 other => Self::Unknown(other.to_string()),
306 }
307 }
308}
309
310impl Serialize for MediaType {
311 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
312 serializer.serialize_str(self.as_str())
313 }
314}
315
316impl<'de> Deserialize<'de> for MediaType {
317 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
318 let s = String::deserialize(deserializer)?;
319 Ok(Self::from(s.as_str()))
320 }
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct ImageSource {
326 #[serde(rename = "type")]
327 pub source_type: ImageSourceType,
328 pub media_type: MediaType,
329 pub data: String,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct ThinkingBlock {
335 pub thinking: String,
336 pub signature: String,
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ToolUseBlock {
342 pub id: String,
343 pub name: String,
344 pub input: Value,
345}
346
347impl ToolUseBlock {
348 pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
370 serde_json::from_value(self.input.clone()).ok()
371 }
372
373 pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
377 serde_json::from_value(self.input.clone())
378 }
379}
380
381#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct ToolResultBlock {
384 pub tool_use_id: String,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub content: Option<ToolResultContent>,
387 #[serde(skip_serializing_if = "Option::is_none")]
388 pub is_error: Option<bool>,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393#[serde(untagged)]
394pub enum ToolResultContent {
395 Text(String),
396 Structured(Vec<Value>),
397}
398
399#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct ServerToolUseBlock {
404 pub id: String,
405 pub name: String,
406 #[serde(default)]
407 pub input: Value,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize)]
412pub struct WebSearchToolResultBlock {
413 pub tool_use_id: String,
414 #[serde(default)]
415 pub content: Value,
416}
417
418#[derive(Debug, Clone, Serialize, Deserialize)]
420pub struct CodeExecutionToolResultBlock {
421 pub tool_use_id: String,
422 #[serde(default)]
423 pub content: Value,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct McpToolUseBlock {
431 pub id: String,
432 pub name: String,
433 #[serde(skip_serializing_if = "Option::is_none")]
434 pub server_name: Option<String>,
435 #[serde(default)]
436 pub input: Value,
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct McpToolResultBlock {
442 pub tool_use_id: String,
443 #[serde(default)]
444 pub content: Value,
445 #[serde(skip_serializing_if = "Option::is_none")]
446 pub is_error: Option<bool>,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct ContainerUploadBlock {
452 #[serde(flatten)]
453 pub data: Value,
454}
455
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462pub struct FallbackBlock {
463 pub from: FallbackModel,
465 pub to: FallbackModel,
467}
468
469#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
471pub struct FallbackModel {
472 pub model: String,
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478 use serde_json::json;
479
480 #[test]
481 fn test_unknown_content_block_deserializes() {
482 let json = json!({
483 "type": "some_future_block_type",
484 "data": "arbitrary"
485 });
486
487 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
488 assert!(block.is_unknown());
489 assert_eq!(block.block_type(), "some_future_block_type");
490 if let ContentBlock::Unknown(v) = &block {
491 assert_eq!(v["data"], "arbitrary");
492 } else {
493 panic!("Expected Unknown variant");
494 }
495 }
496
497 #[test]
498 fn test_unknown_block_roundtrips() {
499 let json = json!({
500 "type": "some_future_type",
501 "tool_use_id": "x",
502 "content": [{"nested": true}]
503 });
504
505 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
506 let reserialized = serde_json::to_value(&block).unwrap();
507 assert_eq!(json, reserialized);
508 }
509
510 #[test]
511 fn test_server_tool_use_deserializes() {
512 let json = json!({
513 "type": "server_tool_use",
514 "id": "srvtu_1",
515 "name": "web_search",
516 "input": {"query": "rust serde"}
517 });
518
519 let block: ContentBlock = serde_json::from_value(json).unwrap();
520 assert!(!block.is_unknown());
521 assert_eq!(block.block_type(), "server_tool_use");
522 if let ContentBlock::ServerToolUse(b) = &block {
523 assert_eq!(b.id, "srvtu_1");
524 assert_eq!(b.name, "web_search");
525 assert_eq!(b.input["query"], "rust serde");
526 } else {
527 panic!("Expected ServerToolUse variant");
528 }
529 }
530
531 #[test]
532 fn test_web_search_tool_result_deserializes() {
533 let json = json!({
534 "type": "web_search_tool_result",
535 "tool_use_id": "srvtu_1",
536 "content": [{"type": "web_search_result", "url": "https://example.com"}]
537 });
538
539 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
540 assert_eq!(block.block_type(), "web_search_tool_result");
541 if let ContentBlock::WebSearchToolResult(b) = &block {
542 assert_eq!(b.tool_use_id, "srvtu_1");
543 } else {
544 panic!("Expected WebSearchToolResult variant");
545 }
546 let reserialized = serde_json::to_value(&block).unwrap();
548 assert_eq!(json, reserialized);
549 }
550
551 #[test]
552 fn test_code_execution_tool_result_deserializes() {
553 let json = json!({
554 "type": "code_execution_tool_result",
555 "tool_use_id": "exec_1",
556 "content": {"stdout": "hello", "exit_code": 0}
557 });
558
559 let block: ContentBlock = serde_json::from_value(json).unwrap();
560 assert_eq!(block.block_type(), "code_execution_tool_result");
561 assert!(matches!(block, ContentBlock::CodeExecutionToolResult(_)));
562 }
563
564 #[test]
565 fn test_mcp_tool_use_deserializes() {
566 let json = json!({
567 "type": "mcp_tool_use",
568 "id": "mcp_tu_1",
569 "name": "custom_tool",
570 "server_name": "my-mcp-server",
571 "input": {"arg": "value"}
572 });
573
574 let block: ContentBlock = serde_json::from_value(json).unwrap();
575 assert_eq!(block.block_type(), "mcp_tool_use");
576 if let ContentBlock::McpToolUse(b) = &block {
577 assert_eq!(b.id, "mcp_tu_1");
578 assert_eq!(b.name, "custom_tool");
579 assert_eq!(b.server_name.as_deref(), Some("my-mcp-server"));
580 } else {
581 panic!("Expected McpToolUse variant");
582 }
583 }
584
585 #[test]
586 fn test_mcp_tool_result_deserializes() {
587 let json = json!({
588 "type": "mcp_tool_result",
589 "tool_use_id": "mcp_tu_1",
590 "content": "tool output text",
591 "is_error": false
592 });
593
594 let block: ContentBlock = serde_json::from_value(json).unwrap();
595 assert_eq!(block.block_type(), "mcp_tool_result");
596 if let ContentBlock::McpToolResult(b) = &block {
597 assert_eq!(b.tool_use_id, "mcp_tu_1");
598 assert_eq!(b.is_error, Some(false));
599 } else {
600 panic!("Expected McpToolResult variant");
601 }
602 }
603
604 #[test]
605 fn test_container_upload_deserializes() {
606 let json = json!({
607 "type": "container_upload",
608 "file_name": "output.csv",
609 "url": "https://storage.example.com/file"
610 });
611
612 let block: ContentBlock = serde_json::from_value(json).unwrap();
613 assert_eq!(block.block_type(), "container_upload");
614 assert!(matches!(block, ContentBlock::ContainerUpload(_)));
615 }
616
617 #[test]
618 fn test_fallback_block_deserializes() {
619 let json = json!({
621 "from": { "model": "claude-fable-5" },
622 "to": { "model": "claude-opus-4-8" },
623 "type": "fallback"
624 });
625
626 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
627 assert!(!block.is_unknown());
628 assert_eq!(block.block_type(), "fallback");
629 if let ContentBlock::Fallback(b) = &block {
630 assert_eq!(b.from.model, "claude-fable-5");
631 assert_eq!(b.to.model, "claude-opus-4-8");
632 } else {
633 panic!("Expected Fallback variant");
634 }
635 let reserialized = serde_json::to_value(&block).unwrap();
637 assert_eq!(json, reserialized);
638 }
639
640 #[test]
641 fn test_known_blocks_still_work() {
642 let text_json = json!({"type": "text", "text": "hello"});
643 let block: ContentBlock = serde_json::from_value(text_json).unwrap();
644 assert!(!block.is_unknown());
645 assert_eq!(block.block_type(), "text");
646 assert!(matches!(block, ContentBlock::Text(TextBlock { text, .. }) if text == "hello"));
647
648 let tool_json =
649 json!({"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}});
650 let block: ContentBlock = serde_json::from_value(tool_json).unwrap();
651 assert_eq!(block.block_type(), "tool_use");
652 assert!(matches!(block, ContentBlock::ToolUse(_)));
653 }
654
655 #[test]
656 fn test_known_blocks_roundtrip() {
657 let text_json = json!({"type": "text", "text": "hello world"});
658 let block: ContentBlock = serde_json::from_value(text_json.clone()).unwrap();
659 let reserialized = serde_json::to_value(&block).unwrap();
660 assert_eq!(text_json, reserialized);
661 }
662
663 #[test]
664 fn test_assistant_message_with_server_tool_use() {
665 let json = r#"{
666 "type": "assistant",
667 "message": {
668 "id": "msg_1",
669 "role": "assistant",
670 "model": "claude-3",
671 "content": [
672 {"type": "text", "text": "Let me search for that."},
673 {"type": "server_tool_use", "id": "srvtu_1", "name": "web_search", "input": {"query": "test"}},
674 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}}
675 ]
676 },
677 "session_id": "abc"
678 }"#;
679
680 let output: crate::io::ClaudeOutput = serde_json::from_str(json).unwrap();
681 assert!(output.is_assistant_message());
682 let assistant = output.as_assistant().unwrap();
683 assert_eq!(assistant.message.content.len(), 3);
684 assert!(matches!(
685 &assistant.message.content[0],
686 ContentBlock::Text(_)
687 ));
688 assert!(matches!(
689 &assistant.message.content[1],
690 ContentBlock::ServerToolUse(_)
691 ));
692 assert!(matches!(
693 &assistant.message.content[2],
694 ContentBlock::ToolUse(_)
695 ));
696
697 assert_eq!(
699 output.text_content(),
700 Some("Let me search for that.".to_string())
701 );
702 assert_eq!(output.tool_uses().count(), 1);
704 }
705
706 #[test]
707 fn test_text_block_with_citations() {
708 let json = json!({
709 "type": "text",
710 "text": "According to the documentation...",
711 "citations": [
712 {"type": "web_search_result_location", "url": "https://example.com", "title": "Example"}
713 ]
714 });
715
716 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
717 if let ContentBlock::Text(t) = &block {
718 assert_eq!(t.text, "According to the documentation...");
719 assert_eq!(t.citations.len(), 1);
720 let cite = &t.citations[0];
721 assert_eq!(
722 cite.citation_type.as_deref(),
723 Some("web_search_result_location")
724 );
725 assert_eq!(cite.url.as_deref(), Some("https://example.com"));
726 assert_eq!(cite.title.as_deref(), Some("Example"));
727 } else {
728 panic!("Expected Text variant");
729 }
730 let reserialized = serde_json::to_value(&block).unwrap();
732 assert_eq!(json, reserialized);
733 }
734
735 #[test]
736 fn test_citation_preserves_unmodeled_location_fields() {
737 let json = json!({
740 "type": "char_location",
741 "cited_text": "the quick brown fox",
742 "document_index": 0,
743 "document_title": "Doc A",
744 "start_char_index": 12,
745 "end_char_index": 31
746 });
747 let cite: Citation = serde_json::from_value(json.clone()).unwrap();
748 assert_eq!(cite.citation_type.as_deref(), Some("char_location"));
749 assert_eq!(cite.cited_text.as_deref(), Some("the quick brown fox"));
750 assert_eq!(cite.document_index, Some(0));
751 assert_eq!(
752 cite.extra.get("start_char_index").and_then(|v| v.as_u64()),
753 Some(12)
754 );
755 assert_eq!(
756 cite.extra.get("end_char_index").and_then(|v| v.as_u64()),
757 Some(31)
758 );
759 assert_eq!(serde_json::to_value(&cite).unwrap(), json);
761 }
762
763 #[test]
764 fn test_text_block_without_citations_defaults_empty() {
765 let json = json!({"type": "text", "text": "no citations"});
766 let block: ContentBlock = serde_json::from_value(json).unwrap();
767 if let ContentBlock::Text(t) = &block {
768 assert!(t.citations.is_empty());
769 } else {
770 panic!("Expected Text variant");
771 }
772 let reserialized = serde_json::to_value(&block).unwrap();
774 assert!(reserialized.get("citations").is_none());
775 }
776
777 #[test]
778 fn test_missing_type_field_errors() {
779 let json = json!({"text": "no type field"});
780 let result = serde_json::from_value::<ContentBlock>(json);
781 assert!(result.is_err());
782 }
783}