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<Value>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ImageBlock {
174 pub source: ImageSource,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Hash)]
179pub enum ImageSourceType {
180 Base64,
182 Unknown(String),
184}
185
186impl ImageSourceType {
187 pub fn as_str(&self) -> &str {
188 match self {
189 Self::Base64 => "base64",
190 Self::Unknown(s) => s.as_str(),
191 }
192 }
193}
194
195impl fmt::Display for ImageSourceType {
196 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197 f.write_str(self.as_str())
198 }
199}
200
201impl From<&str> for ImageSourceType {
202 fn from(s: &str) -> Self {
203 match s {
204 "base64" => Self::Base64,
205 other => Self::Unknown(other.to_string()),
206 }
207 }
208}
209
210impl Serialize for ImageSourceType {
211 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
212 serializer.serialize_str(self.as_str())
213 }
214}
215
216impl<'de> Deserialize<'de> for ImageSourceType {
217 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
218 let s = String::deserialize(deserializer)?;
219 Ok(Self::from(s.as_str()))
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Hash)]
225pub enum MediaType {
226 Jpeg,
228 Png,
230 Gif,
232 Webp,
234 Unknown(String),
236}
237
238impl MediaType {
239 pub fn as_str(&self) -> &str {
240 match self {
241 Self::Jpeg => "image/jpeg",
242 Self::Png => "image/png",
243 Self::Gif => "image/gif",
244 Self::Webp => "image/webp",
245 Self::Unknown(s) => s.as_str(),
246 }
247 }
248}
249
250impl fmt::Display for MediaType {
251 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252 f.write_str(self.as_str())
253 }
254}
255
256impl From<&str> for MediaType {
257 fn from(s: &str) -> Self {
258 match s {
259 "image/jpeg" => Self::Jpeg,
260 "image/png" => Self::Png,
261 "image/gif" => Self::Gif,
262 "image/webp" => Self::Webp,
263 other => Self::Unknown(other.to_string()),
264 }
265 }
266}
267
268impl Serialize for MediaType {
269 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
270 serializer.serialize_str(self.as_str())
271 }
272}
273
274impl<'de> Deserialize<'de> for MediaType {
275 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
276 let s = String::deserialize(deserializer)?;
277 Ok(Self::from(s.as_str()))
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct ImageSource {
284 #[serde(rename = "type")]
285 pub source_type: ImageSourceType,
286 pub media_type: MediaType,
287 pub data: String,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize)]
292pub struct ThinkingBlock {
293 pub thinking: String,
294 pub signature: String,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct ToolUseBlock {
300 pub id: String,
301 pub name: String,
302 pub input: Value,
303}
304
305impl ToolUseBlock {
306 pub fn typed_input(&self) -> Option<crate::tool_inputs::ToolInput> {
328 serde_json::from_value(self.input.clone()).ok()
329 }
330
331 pub fn try_typed_input(&self) -> Result<crate::tool_inputs::ToolInput, serde_json::Error> {
335 serde_json::from_value(self.input.clone())
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct ToolResultBlock {
342 pub tool_use_id: String,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub content: Option<ToolResultContent>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub is_error: Option<bool>,
347}
348
349#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(untagged)]
352pub enum ToolResultContent {
353 Text(String),
354 Structured(Vec<Value>),
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ServerToolUseBlock {
362 pub id: String,
363 pub name: String,
364 #[serde(default)]
365 pub input: Value,
366}
367
368#[derive(Debug, Clone, Serialize, Deserialize)]
370pub struct WebSearchToolResultBlock {
371 pub tool_use_id: String,
372 #[serde(default)]
373 pub content: Value,
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize)]
378pub struct CodeExecutionToolResultBlock {
379 pub tool_use_id: String,
380 #[serde(default)]
381 pub content: Value,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct McpToolUseBlock {
389 pub id: String,
390 pub name: String,
391 #[serde(skip_serializing_if = "Option::is_none")]
392 pub server_name: Option<String>,
393 #[serde(default)]
394 pub input: Value,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399pub struct McpToolResultBlock {
400 pub tool_use_id: String,
401 #[serde(default)]
402 pub content: Value,
403 #[serde(skip_serializing_if = "Option::is_none")]
404 pub is_error: Option<bool>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ContainerUploadBlock {
410 #[serde(flatten)]
411 pub data: Value,
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use serde_json::json;
418
419 #[test]
420 fn test_unknown_content_block_deserializes() {
421 let json = json!({
422 "type": "some_future_block_type",
423 "data": "arbitrary"
424 });
425
426 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
427 assert!(block.is_unknown());
428 assert_eq!(block.block_type(), "some_future_block_type");
429 if let ContentBlock::Unknown(v) = &block {
430 assert_eq!(v["data"], "arbitrary");
431 } else {
432 panic!("Expected Unknown variant");
433 }
434 }
435
436 #[test]
437 fn test_unknown_block_roundtrips() {
438 let json = json!({
439 "type": "some_future_type",
440 "tool_use_id": "x",
441 "content": [{"nested": true}]
442 });
443
444 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
445 let reserialized = serde_json::to_value(&block).unwrap();
446 assert_eq!(json, reserialized);
447 }
448
449 #[test]
450 fn test_server_tool_use_deserializes() {
451 let json = json!({
452 "type": "server_tool_use",
453 "id": "srvtu_1",
454 "name": "web_search",
455 "input": {"query": "rust serde"}
456 });
457
458 let block: ContentBlock = serde_json::from_value(json).unwrap();
459 assert!(!block.is_unknown());
460 assert_eq!(block.block_type(), "server_tool_use");
461 if let ContentBlock::ServerToolUse(b) = &block {
462 assert_eq!(b.id, "srvtu_1");
463 assert_eq!(b.name, "web_search");
464 assert_eq!(b.input["query"], "rust serde");
465 } else {
466 panic!("Expected ServerToolUse variant");
467 }
468 }
469
470 #[test]
471 fn test_web_search_tool_result_deserializes() {
472 let json = json!({
473 "type": "web_search_tool_result",
474 "tool_use_id": "srvtu_1",
475 "content": [{"type": "web_search_result", "url": "https://example.com"}]
476 });
477
478 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
479 assert_eq!(block.block_type(), "web_search_tool_result");
480 if let ContentBlock::WebSearchToolResult(b) = &block {
481 assert_eq!(b.tool_use_id, "srvtu_1");
482 } else {
483 panic!("Expected WebSearchToolResult variant");
484 }
485 let reserialized = serde_json::to_value(&block).unwrap();
487 assert_eq!(json, reserialized);
488 }
489
490 #[test]
491 fn test_code_execution_tool_result_deserializes() {
492 let json = json!({
493 "type": "code_execution_tool_result",
494 "tool_use_id": "exec_1",
495 "content": {"stdout": "hello", "exit_code": 0}
496 });
497
498 let block: ContentBlock = serde_json::from_value(json).unwrap();
499 assert_eq!(block.block_type(), "code_execution_tool_result");
500 assert!(matches!(block, ContentBlock::CodeExecutionToolResult(_)));
501 }
502
503 #[test]
504 fn test_mcp_tool_use_deserializes() {
505 let json = json!({
506 "type": "mcp_tool_use",
507 "id": "mcp_tu_1",
508 "name": "custom_tool",
509 "server_name": "my-mcp-server",
510 "input": {"arg": "value"}
511 });
512
513 let block: ContentBlock = serde_json::from_value(json).unwrap();
514 assert_eq!(block.block_type(), "mcp_tool_use");
515 if let ContentBlock::McpToolUse(b) = &block {
516 assert_eq!(b.id, "mcp_tu_1");
517 assert_eq!(b.name, "custom_tool");
518 assert_eq!(b.server_name.as_deref(), Some("my-mcp-server"));
519 } else {
520 panic!("Expected McpToolUse variant");
521 }
522 }
523
524 #[test]
525 fn test_mcp_tool_result_deserializes() {
526 let json = json!({
527 "type": "mcp_tool_result",
528 "tool_use_id": "mcp_tu_1",
529 "content": "tool output text",
530 "is_error": false
531 });
532
533 let block: ContentBlock = serde_json::from_value(json).unwrap();
534 assert_eq!(block.block_type(), "mcp_tool_result");
535 if let ContentBlock::McpToolResult(b) = &block {
536 assert_eq!(b.tool_use_id, "mcp_tu_1");
537 assert_eq!(b.is_error, Some(false));
538 } else {
539 panic!("Expected McpToolResult variant");
540 }
541 }
542
543 #[test]
544 fn test_container_upload_deserializes() {
545 let json = json!({
546 "type": "container_upload",
547 "file_name": "output.csv",
548 "url": "https://storage.example.com/file"
549 });
550
551 let block: ContentBlock = serde_json::from_value(json).unwrap();
552 assert_eq!(block.block_type(), "container_upload");
553 assert!(matches!(block, ContentBlock::ContainerUpload(_)));
554 }
555
556 #[test]
557 fn test_known_blocks_still_work() {
558 let text_json = json!({"type": "text", "text": "hello"});
559 let block: ContentBlock = serde_json::from_value(text_json).unwrap();
560 assert!(!block.is_unknown());
561 assert_eq!(block.block_type(), "text");
562 assert!(matches!(block, ContentBlock::Text(TextBlock { text, .. }) if text == "hello"));
563
564 let tool_json =
565 json!({"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}});
566 let block: ContentBlock = serde_json::from_value(tool_json).unwrap();
567 assert_eq!(block.block_type(), "tool_use");
568 assert!(matches!(block, ContentBlock::ToolUse(_)));
569 }
570
571 #[test]
572 fn test_known_blocks_roundtrip() {
573 let text_json = json!({"type": "text", "text": "hello world"});
574 let block: ContentBlock = serde_json::from_value(text_json.clone()).unwrap();
575 let reserialized = serde_json::to_value(&block).unwrap();
576 assert_eq!(text_json, reserialized);
577 }
578
579 #[test]
580 fn test_assistant_message_with_server_tool_use() {
581 let json = r#"{
582 "type": "assistant",
583 "message": {
584 "id": "msg_1",
585 "role": "assistant",
586 "model": "claude-3",
587 "content": [
588 {"type": "text", "text": "Let me search for that."},
589 {"type": "server_tool_use", "id": "srvtu_1", "name": "web_search", "input": {"query": "test"}},
590 {"type": "tool_use", "id": "tu_1", "name": "Bash", "input": {"command": "ls"}}
591 ]
592 },
593 "session_id": "abc"
594 }"#;
595
596 let output: crate::io::ClaudeOutput = serde_json::from_str(json).unwrap();
597 assert!(output.is_assistant_message());
598 let assistant = output.as_assistant().unwrap();
599 assert_eq!(assistant.message.content.len(), 3);
600 assert!(matches!(
601 &assistant.message.content[0],
602 ContentBlock::Text(_)
603 ));
604 assert!(matches!(
605 &assistant.message.content[1],
606 ContentBlock::ServerToolUse(_)
607 ));
608 assert!(matches!(
609 &assistant.message.content[2],
610 ContentBlock::ToolUse(_)
611 ));
612
613 assert_eq!(
615 output.text_content(),
616 Some("Let me search for that.".to_string())
617 );
618 assert_eq!(output.tool_uses().count(), 1);
620 }
621
622 #[test]
623 fn test_text_block_with_citations() {
624 let json = json!({
625 "type": "text",
626 "text": "According to the documentation...",
627 "citations": [
628 {"type": "web_search_result_location", "url": "https://example.com", "title": "Example"}
629 ]
630 });
631
632 let block: ContentBlock = serde_json::from_value(json.clone()).unwrap();
633 if let ContentBlock::Text(t) = &block {
634 assert_eq!(t.text, "According to the documentation...");
635 assert_eq!(t.citations.len(), 1);
636 assert_eq!(t.citations[0]["url"], "https://example.com");
637 } else {
638 panic!("Expected Text variant");
639 }
640 let reserialized = serde_json::to_value(&block).unwrap();
642 assert_eq!(json, reserialized);
643 }
644
645 #[test]
646 fn test_text_block_without_citations_defaults_empty() {
647 let json = json!({"type": "text", "text": "no citations"});
648 let block: ContentBlock = serde_json::from_value(json).unwrap();
649 if let ContentBlock::Text(t) = &block {
650 assert!(t.citations.is_empty());
651 } else {
652 panic!("Expected Text variant");
653 }
654 let reserialized = serde_json::to_value(&block).unwrap();
656 assert!(reserialized.get("citations").is_none());
657 }
658
659 #[test]
660 fn test_missing_type_field_errors() {
661 let json = json!({"text": "no type field"});
662 let result = serde_json::from_value::<ContentBlock>(json);
663 assert!(result.is_err());
664 }
665}