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