claude_agent_sdk/types/
messages.rs

1//! Message types for Claude Agent SDK
2
3use serde::{Deserialize, Serialize};
4
5/// Supported image MIME types for Claude API
6const SUPPORTED_IMAGE_MIME_TYPES: &[&str] = &["image/jpeg", "image/png", "image/gif", "image/webp"];
7
8/// Maximum base64 data size (15MB results in ~20MB decoded, within Claude's limits)
9const MAX_BASE64_SIZE: usize = 15_728_640;
10
11/// Error types for assistant messages
12#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "snake_case")]
14pub enum AssistantMessageError {
15    /// Authentication failed
16    AuthenticationFailed,
17    /// Billing error
18    BillingError,
19    /// Rate limit exceeded
20    RateLimit,
21    /// Invalid request
22    InvalidRequest,
23    /// Server error
24    ServerError,
25    /// Unknown error
26    Unknown,
27}
28
29/// Main message enum containing all message types from CLI
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "type", rename_all = "lowercase")]
32pub enum Message {
33    /// Assistant message
34    #[serde(rename = "assistant")]
35    Assistant(AssistantMessage),
36    /// System message
37    #[serde(rename = "system")]
38    System(SystemMessage),
39    /// Result message
40    #[serde(rename = "result")]
41    Result(ResultMessage),
42    /// Stream event
43    #[serde(rename = "stream_event")]
44    StreamEvent(StreamEvent),
45    /// User message (rarely used in stream output)
46    #[serde(rename = "user")]
47    User(UserMessage),
48    /// Control cancel request (ignore this - it's internal control protocol)
49    #[serde(rename = "control_cancel_request")]
50    ControlCancelRequest(serde_json::Value),
51}
52
53/// User message
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct UserMessage {
56    /// Message text
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub text: Option<String>,
59    /// Message content blocks
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub content: Option<Vec<ContentBlock>>,
62    /// UUID for file checkpointing (used with enable_file_checkpointing and rewind_files)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub uuid: Option<String>,
65    /// Parent tool use ID (if this is a tool result)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub parent_tool_use_id: Option<String>,
68    /// Additional fields
69    #[serde(flatten)]
70    pub extra: serde_json::Value,
71}
72
73/// Message content can be text or blocks
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(untagged)]
76pub enum MessageContent {
77    /// Simple text content
78    Text { text: String },
79    /// Structured content blocks
80    Blocks { content: Vec<ContentBlock> },
81}
82
83impl From<String> for MessageContent {
84    fn from(text: String) -> Self {
85        MessageContent::Text { text }
86    }
87}
88
89impl From<&str> for MessageContent {
90    fn from(text: &str) -> Self {
91        MessageContent::Text {
92            text: text.to_string(),
93        }
94    }
95}
96
97impl From<Vec<ContentBlock>> for MessageContent {
98    fn from(blocks: Vec<ContentBlock>) -> Self {
99        MessageContent::Blocks { content: blocks }
100    }
101}
102
103/// Assistant message
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct AssistantMessage {
106    /// The actual message content (wrapped)
107    pub message: AssistantMessageInner,
108    /// Parent tool use ID (if applicable)
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub parent_tool_use_id: Option<String>,
111    /// Session ID
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub session_id: Option<String>,
114    /// UUID
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub uuid: Option<String>,
117}
118
119/// Inner assistant message content
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct AssistantMessageInner {
122    /// Message content blocks
123    #[serde(default)]
124    pub content: Vec<ContentBlock>,
125    /// Model used
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub model: Option<String>,
128    /// Message ID
129    #[serde(skip_serializing_if = "Option::is_none")]
130    pub id: Option<String>,
131    /// Stop reason
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub stop_reason: Option<String>,
134    /// Usage statistics
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub usage: Option<serde_json::Value>,
137    /// Error type (if any)
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub error: Option<AssistantMessageError>,
140}
141
142/// System message
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct SystemMessage {
145    /// Message subtype
146    pub subtype: String,
147    /// Current working directory
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub cwd: Option<String>,
150    /// Session ID
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub session_id: Option<String>,
153    /// Available tools
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub tools: Option<Vec<String>>,
156    /// MCP servers
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub mcp_servers: Option<Vec<serde_json::Value>>,
159    /// Model being used
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub model: Option<String>,
162    /// Permission mode
163    #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
164    pub permission_mode: Option<String>,
165    /// UUID
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub uuid: Option<String>,
168    /// Additional data
169    #[serde(flatten)]
170    pub data: serde_json::Value,
171}
172
173/// Result message indicating query completion
174#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct ResultMessage {
176    /// Result subtype
177    pub subtype: String,
178    /// Duration in milliseconds
179    pub duration_ms: u64,
180    /// API duration in milliseconds
181    pub duration_api_ms: u64,
182    /// Whether this is an error result
183    pub is_error: bool,
184    /// Number of turns in conversation
185    pub num_turns: u32,
186    /// Session ID
187    pub session_id: String,
188    /// Total cost in USD
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub total_cost_usd: Option<f64>,
191    /// Usage statistics
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub usage: Option<serde_json::Value>,
194    /// Result text (if any)
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub result: Option<String>,
197    /// Structured output (when output_format is specified)
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub structured_output: Option<serde_json::Value>,
200}
201
202/// Stream event message
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct StreamEvent {
205    /// Event UUID
206    pub uuid: String,
207    /// Session ID
208    pub session_id: String,
209    /// Event data
210    pub event: serde_json::Value,
211    /// Parent tool use ID (if applicable)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub parent_tool_use_id: Option<String>,
214}
215
216/// Content block types
217#[derive(Debug, Clone, Serialize, Deserialize)]
218#[serde(tag = "type", rename_all = "snake_case")]
219pub enum ContentBlock {
220    /// Text block
221    Text(TextBlock),
222    /// Thinking block (extended thinking)
223    Thinking(ThinkingBlock),
224    /// Tool use block
225    ToolUse(ToolUseBlock),
226    /// Tool result block
227    ToolResult(ToolResultBlock),
228    /// Image block
229    Image(ImageBlock),
230}
231
232/// Text content block
233#[derive(Debug, Clone, Serialize, Deserialize)]
234pub struct TextBlock {
235    /// Text content
236    pub text: String,
237}
238
239/// Thinking block (extended thinking)
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ThinkingBlock {
242    /// Thinking content
243    pub thinking: String,
244    /// Signature
245    pub signature: String,
246}
247
248/// Tool use block
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct ToolUseBlock {
251    /// Tool use ID
252    pub id: String,
253    /// Tool name
254    pub name: String,
255    /// Tool input parameters
256    pub input: serde_json::Value,
257}
258
259/// Tool result block
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct ToolResultBlock {
262    /// Tool use ID this result corresponds to
263    pub tool_use_id: String,
264    /// Result content
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub content: Option<ToolResultContent>,
267    /// Whether this is an error
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub is_error: Option<bool>,
270}
271
272/// Tool result content
273#[derive(Debug, Clone, Serialize, Deserialize)]
274#[serde(untagged)]
275pub enum ToolResultContent {
276    /// Text result
277    Text(String),
278    /// Structured blocks
279    Blocks(Vec<serde_json::Value>),
280}
281
282/// Image source for user prompts
283///
284/// Represents the source of image data that can be included in user messages.
285/// Claude supports both base64-encoded images and URL references.
286///
287/// # Supported Formats
288///
289/// - JPEG (`image/jpeg`)
290/// - PNG (`image/png`)
291/// - GIF (`image/gif`)
292/// - WebP (`image/webp`)
293#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
294#[serde(tag = "type", rename_all = "snake_case")]
295pub enum ImageSource {
296    /// Base64-encoded image data
297    Base64 {
298        /// MIME type (e.g., "image/png", "image/jpeg", "image/gif", "image/webp")
299        media_type: String,
300        /// Base64-encoded image data (without data URI prefix)
301        data: String,
302    },
303    /// URL reference to an image
304    Url {
305        /// Publicly accessible image URL
306        url: String,
307    },
308}
309
310/// Image block for user prompts
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312pub struct ImageBlock {
313    /// Image source (base64 or URL)
314    pub source: ImageSource,
315}
316
317/// Content block for user prompts (input)
318///
319/// Represents content that can be included in user messages.
320#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
321#[serde(tag = "type", rename_all = "snake_case")]
322pub enum UserContentBlock {
323    /// Text content
324    Text {
325        /// Text content string
326        text: String,
327    },
328    /// Image content
329    Image {
330        /// Image source (base64 or URL)
331        source: ImageSource,
332    },
333}
334
335impl UserContentBlock {
336    /// Create a text content block
337    pub fn text(text: impl Into<String>) -> Self {
338        UserContentBlock::Text { text: text.into() }
339    }
340
341    /// Create an image content block from base64 data
342    ///
343    /// # Arguments
344    ///
345    /// * `media_type` - MIME type of the image (e.g., "image/png", "image/jpeg")
346    /// * `data` - Base64-encoded image data (without data URI prefix)
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if:
351    /// - The MIME type is not supported (valid types: image/jpeg, image/png, image/gif, image/webp)
352    /// - The base64 data exceeds the maximum size limit (15MB)
353    ///
354    /// # Example
355    ///
356    /// ```no_run
357    /// # use claude_agent_sdk::UserContentBlock;
358    /// let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=")?;
359    /// # Ok::<(), claude_agent_sdk::ClaudeError>(())
360    /// ```
361    pub fn image_base64(
362        media_type: impl Into<String>,
363        data: impl Into<String>,
364    ) -> crate::errors::Result<Self> {
365        let media_type_str = media_type.into();
366        let data_str = data.into();
367
368        // Validate MIME type
369        if !SUPPORTED_IMAGE_MIME_TYPES.contains(&media_type_str.as_str()) {
370            return Err(crate::errors::ImageValidationError::new(format!(
371                "Unsupported media type '{}'. Supported types: {:?}",
372                media_type_str, SUPPORTED_IMAGE_MIME_TYPES
373            ))
374            .into());
375        }
376
377        // Validate base64 size
378        if data_str.len() > MAX_BASE64_SIZE {
379            return Err(crate::errors::ImageValidationError::new(format!(
380                "Base64 data exceeds maximum size of {} bytes (got {} bytes)",
381                MAX_BASE64_SIZE,
382                data_str.len()
383            ))
384            .into());
385        }
386
387        Ok(UserContentBlock::Image {
388            source: ImageSource::Base64 {
389                media_type: media_type_str,
390                data: data_str,
391            },
392        })
393    }
394
395    /// Create an image content block from URL
396    pub fn image_url(url: impl Into<String>) -> Self {
397        UserContentBlock::Image {
398            source: ImageSource::Url { url: url.into() },
399        }
400    }
401
402    /// Validate a collection of content blocks
403    ///
404    /// Ensures the content is non-empty. This is used internally by query functions
405    /// to provide consistent validation.
406    ///
407    /// # Errors
408    ///
409    /// Returns an error if the content blocks slice is empty.
410    pub fn validate_content(blocks: &[UserContentBlock]) -> crate::Result<()> {
411        if blocks.is_empty() {
412            return Err(crate::errors::ClaudeError::InvalidConfig(
413                "Content must include at least one block (text or image)".to_string(),
414            ));
415        }
416        Ok(())
417    }
418}
419
420impl From<String> for UserContentBlock {
421    fn from(text: String) -> Self {
422        UserContentBlock::Text { text }
423    }
424}
425
426impl From<&str> for UserContentBlock {
427    fn from(text: &str) -> Self {
428        UserContentBlock::Text {
429            text: text.to_string(),
430        }
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437    use serde_json::json;
438
439    #[test]
440    fn test_content_block_text_serialization() {
441        let block = ContentBlock::Text(TextBlock {
442            text: "Hello".to_string(),
443        });
444
445        let json = serde_json::to_value(&block).unwrap();
446        assert_eq!(json["type"], "text");
447        assert_eq!(json["text"], "Hello");
448    }
449
450    #[test]
451    fn test_content_block_tool_use_serialization() {
452        let block = ContentBlock::ToolUse(ToolUseBlock {
453            id: "tool_123".to_string(),
454            name: "Bash".to_string(),
455            input: json!({"command": "echo hello"}),
456        });
457
458        let json = serde_json::to_value(&block).unwrap();
459        assert_eq!(json["type"], "tool_use");
460        assert_eq!(json["id"], "tool_123");
461        assert_eq!(json["name"], "Bash");
462        assert_eq!(json["input"]["command"], "echo hello");
463    }
464
465    #[test]
466    fn test_message_assistant_deserialization() {
467        let json_str = r#"{
468            "type": "assistant",
469            "message": {
470                "content": [{"type": "text", "text": "Hello"}],
471                "model": "claude-sonnet-4"
472            },
473            "session_id": "test-session"
474        }"#;
475
476        let msg: Message = serde_json::from_str(json_str).unwrap();
477        match msg {
478            Message::Assistant(assistant) => {
479                assert_eq!(assistant.session_id, Some("test-session".to_string()));
480                assert_eq!(assistant.message.model, Some("claude-sonnet-4".to_string()));
481            },
482            _ => panic!("Expected Assistant variant"),
483        }
484    }
485
486    #[test]
487    fn test_message_result_deserialization() {
488        let json_str = r#"{
489            "type": "result",
490            "subtype": "query_complete",
491            "duration_ms": 1500,
492            "duration_api_ms": 1200,
493            "is_error": false,
494            "num_turns": 3,
495            "session_id": "test-session",
496            "total_cost_usd": 0.0042
497        }"#;
498
499        let msg: Message = serde_json::from_str(json_str).unwrap();
500        match msg {
501            Message::Result(result) => {
502                assert_eq!(result.subtype, "query_complete");
503                assert_eq!(result.duration_ms, 1500);
504                assert_eq!(result.num_turns, 3);
505                assert_eq!(result.total_cost_usd, Some(0.0042));
506            },
507            _ => panic!("Expected Result variant"),
508        }
509    }
510
511    #[test]
512    fn test_message_system_deserialization() {
513        let json_str = r#"{
514            "type": "system",
515            "subtype": "session_start",
516            "cwd": "/home/user",
517            "session_id": "test-session",
518            "tools": ["Bash", "Read", "Write"]
519        }"#;
520
521        let msg: Message = serde_json::from_str(json_str).unwrap();
522        match msg {
523            Message::System(system) => {
524                assert_eq!(system.subtype, "session_start");
525                assert_eq!(system.cwd, Some("/home/user".to_string()));
526                assert_eq!(system.tools.as_ref().unwrap().len(), 3);
527            },
528            _ => panic!("Expected System variant"),
529        }
530    }
531
532    #[test]
533    fn test_tool_result_content_text() {
534        let content = ToolResultContent::Text("Command output".to_string());
535        let json = serde_json::to_value(&content).unwrap();
536        assert_eq!(json, "Command output");
537    }
538
539    #[test]
540    fn test_tool_result_content_blocks() {
541        let content = ToolResultContent::Blocks(vec![json!({"type": "text", "text": "Result"})]);
542        let json = serde_json::to_value(&content).unwrap();
543        assert!(json.is_array());
544        assert_eq!(json[0]["type"], "text");
545    }
546
547    #[test]
548    fn test_image_source_base64_serialization() {
549        let source = ImageSource::Base64 {
550            media_type: "image/png".to_string(),
551            data: "iVBORw0KGgo=".to_string(),
552        };
553
554        let json = serde_json::to_value(&source).unwrap();
555        assert_eq!(json["type"], "base64");
556        assert_eq!(json["media_type"], "image/png");
557        assert_eq!(json["data"], "iVBORw0KGgo=");
558    }
559
560    #[test]
561    fn test_image_source_url_serialization() {
562        let source = ImageSource::Url {
563            url: "https://example.com/image.png".to_string(),
564        };
565
566        let json = serde_json::to_value(&source).unwrap();
567        assert_eq!(json["type"], "url");
568        assert_eq!(json["url"], "https://example.com/image.png");
569    }
570
571    #[test]
572    fn test_image_source_base64_deserialization() {
573        let json_str = r#"{
574            "type": "base64",
575            "media_type": "image/jpeg",
576            "data": "base64data=="
577        }"#;
578
579        let source: ImageSource = serde_json::from_str(json_str).unwrap();
580        match source {
581            ImageSource::Base64 { media_type, data } => {
582                assert_eq!(media_type, "image/jpeg");
583                assert_eq!(data, "base64data==");
584            },
585            _ => panic!("Expected Base64 variant"),
586        }
587    }
588
589    #[test]
590    fn test_image_source_url_deserialization() {
591        let json_str = r#"{
592            "type": "url",
593            "url": "https://example.com/test.gif"
594        }"#;
595
596        let source: ImageSource = serde_json::from_str(json_str).unwrap();
597        match source {
598            ImageSource::Url { url } => {
599                assert_eq!(url, "https://example.com/test.gif");
600            },
601            _ => panic!("Expected Url variant"),
602        }
603    }
604
605    #[test]
606    fn test_user_content_block_text_serialization() {
607        let block = UserContentBlock::text("Hello world");
608
609        let json = serde_json::to_value(&block).unwrap();
610        assert_eq!(json["type"], "text");
611        assert_eq!(json["text"], "Hello world");
612    }
613
614    #[test]
615    fn test_user_content_block_image_base64_serialization() {
616        let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=").unwrap();
617
618        let json = serde_json::to_value(&block).unwrap();
619        assert_eq!(json["type"], "image");
620        assert_eq!(json["source"]["type"], "base64");
621        assert_eq!(json["source"]["media_type"], "image/png");
622        assert_eq!(json["source"]["data"], "iVBORw0KGgo=");
623    }
624
625    #[test]
626    fn test_user_content_block_image_url_serialization() {
627        let block = UserContentBlock::image_url("https://example.com/image.webp");
628
629        let json = serde_json::to_value(&block).unwrap();
630        assert_eq!(json["type"], "image");
631        assert_eq!(json["source"]["type"], "url");
632        assert_eq!(json["source"]["url"], "https://example.com/image.webp");
633    }
634
635    #[test]
636    fn test_user_content_block_from_string() {
637        let block: UserContentBlock = "Test message".into();
638
639        match block {
640            UserContentBlock::Text { text } => {
641                assert_eq!(text, "Test message");
642            },
643            _ => panic!("Expected Text variant"),
644        }
645    }
646
647    #[test]
648    fn test_user_content_block_from_owned_string() {
649        let block: UserContentBlock = String::from("Owned message").into();
650
651        match block {
652            UserContentBlock::Text { text } => {
653                assert_eq!(text, "Owned message");
654            },
655            _ => panic!("Expected Text variant"),
656        }
657    }
658
659    #[test]
660    fn test_image_block_serialization() {
661        let block = ImageBlock {
662            source: ImageSource::Base64 {
663                media_type: "image/gif".to_string(),
664                data: "R0lGODlh".to_string(),
665            },
666        };
667
668        let json = serde_json::to_value(&block).unwrap();
669        assert_eq!(json["source"]["type"], "base64");
670        assert_eq!(json["source"]["media_type"], "image/gif");
671        assert_eq!(json["source"]["data"], "R0lGODlh");
672    }
673
674    #[test]
675    fn test_content_block_image_serialization() {
676        let block = ContentBlock::Image(ImageBlock {
677            source: ImageSource::Url {
678                url: "https://example.com/photo.jpg".to_string(),
679            },
680        });
681
682        let json = serde_json::to_value(&block).unwrap();
683        assert_eq!(json["type"], "image");
684        assert_eq!(json["source"]["type"], "url");
685        assert_eq!(json["source"]["url"], "https://example.com/photo.jpg");
686    }
687
688    #[test]
689    fn test_content_block_image_deserialization() {
690        let json_str = r#"{
691            "type": "image",
692            "source": {
693                "type": "base64",
694                "media_type": "image/webp",
695                "data": "UklGR"
696            }
697        }"#;
698
699        let block: ContentBlock = serde_json::from_str(json_str).unwrap();
700        match block {
701            ContentBlock::Image(image) => match image.source {
702                ImageSource::Base64 { media_type, data } => {
703                    assert_eq!(media_type, "image/webp");
704                    assert_eq!(data, "UklGR");
705                },
706                _ => panic!("Expected Base64 source"),
707            },
708            _ => panic!("Expected Image variant"),
709        }
710    }
711
712    #[test]
713    fn test_user_content_block_deserialization() {
714        let json_str = r#"{
715            "type": "text",
716            "text": "Describe this image"
717        }"#;
718
719        let block: UserContentBlock = serde_json::from_str(json_str).unwrap();
720        match block {
721            UserContentBlock::Text { text } => {
722                assert_eq!(text, "Describe this image");
723            },
724            _ => panic!("Expected Text variant"),
725        }
726    }
727
728    #[test]
729    fn test_user_content_block_image_deserialization() {
730        let json_str = r#"{
731            "type": "image",
732            "source": {
733                "type": "url",
734                "url": "https://example.com/diagram.png"
735            }
736        }"#;
737
738        let block: UserContentBlock = serde_json::from_str(json_str).unwrap();
739        match block {
740            UserContentBlock::Image { source } => match source {
741                ImageSource::Url { url } => {
742                    assert_eq!(url, "https://example.com/diagram.png");
743                },
744                _ => panic!("Expected Url source"),
745            },
746            _ => panic!("Expected Image variant"),
747        }
748    }
749
750    #[test]
751    fn test_image_base64_valid() {
752        let block = UserContentBlock::image_base64("image/png", "iVBORw0KGgo=");
753        assert!(block.is_ok());
754    }
755
756    #[test]
757    fn test_image_base64_invalid_mime_type() {
758        let block = UserContentBlock::image_base64("image/bmp", "data");
759        assert!(block.is_err());
760        let err = block.unwrap_err().to_string();
761        assert!(err.contains("Unsupported media type"));
762        assert!(err.contains("image/bmp"));
763    }
764
765    #[test]
766    fn test_image_base64_exceeds_size_limit() {
767        let large_data = "a".repeat(MAX_BASE64_SIZE + 1);
768        let block = UserContentBlock::image_base64("image/png", large_data);
769        assert!(block.is_err());
770        let err = block.unwrap_err().to_string();
771        assert!(err.contains("exceeds maximum size"));
772    }
773}