Skip to main content

anthropic_async/resources/
messages.rs

1use crate::{
2    client::Client,
3    config::Config,
4    error::AnthropicError,
5    types::common::{CacheControl, CacheTtl, validate_mixed_ttl_order},
6    types::content::{
7        ContentBlockParam, MessageContentParam, SystemParam, ToolResultContent,
8        ToolResultContentBlock,
9    },
10    types::messages::{
11        MessageTokensCountRequest, MessageTokensCountResponse, MessagesCreateRequest,
12        MessagesCreateResponse,
13    },
14};
15
16// ============================================================================
17// TTL Validation Helpers
18// ============================================================================
19
20/// Push TTL to the collection if `cache_control` contains a TTL
21fn push_ttl(ttls: &mut Vec<CacheTtl>, cache_control: Option<&CacheControl>) {
22    if let Some(ttl) = cache_control.and_then(|cc| cc.ttl.clone()) {
23        ttls.push(ttl);
24    }
25}
26
27/// Collect TTLs from nested `ToolResultContent` blocks
28fn collect_tool_result_content_ttls(ttls: &mut Vec<CacheTtl>, content: Option<&ToolResultContent>) {
29    let Some(content) = content else { return };
30    if let ToolResultContent::Blocks(blocks) = content {
31        for block in blocks {
32            match block {
33                ToolResultContentBlock::Text { cache_control, .. }
34                | ToolResultContentBlock::Image { cache_control, .. } => {
35                    push_ttl(ttls, cache_control.as_ref());
36                }
37            }
38        }
39    }
40}
41
42/// Collect TTLs from a `ContentBlockParam`, including nested content
43fn collect_block_param_ttls(ttls: &mut Vec<CacheTtl>, block: &ContentBlockParam) {
44    match block {
45        ContentBlockParam::Text { cache_control, .. }
46        | ContentBlockParam::Image { cache_control, .. }
47        | ContentBlockParam::Document { cache_control, .. }
48        | ContentBlockParam::ToolUse { cache_control, .. }
49        | ContentBlockParam::ServerToolUse { cache_control, .. }
50        | ContentBlockParam::SearchResult { cache_control, .. }
51        | ContentBlockParam::WebSearchToolResult { cache_control, .. } => {
52            push_ttl(ttls, cache_control.as_ref());
53        }
54        ContentBlockParam::ToolResult {
55            cache_control,
56            content,
57            ..
58        } => {
59            push_ttl(ttls, cache_control.as_ref());
60            collect_tool_result_content_ttls(ttls, content.as_ref());
61        }
62        ContentBlockParam::Thinking { .. } | ContentBlockParam::RedactedThinking { .. } => {
63            // No cache_control on thinking blocks
64        }
65    }
66}
67
68/// Validate a messages create request
69///
70/// Checks TTL ordering across all cacheable locations (system, tools, messages)
71/// and validates sampling parameters.
72///
73/// # TTL Validation Locations (12 total)
74///
75/// 1. `SystemParam::Blocks` → `TextBlockParam.cache_control.ttl`
76/// 2. `Tool.cache_control.ttl` (tool definitions)
77/// 3. `ContentBlockParam::{Text, Image, Document, ToolUse, ServerToolUse, SearchResult,
78///    WebSearchToolResult}.cache_control.ttl`
79/// 4. `ContentBlockParam::ToolResult.cache_control.ttl`
80/// 5. `ToolResultContentBlock::{Text, Image}.cache_control.ttl` (nested inside `ToolResult`)
81fn validate_messages_create_request(req: &MessagesCreateRequest) -> Result<(), AnthropicError> {
82    // Validate TTL ordering across all cacheable locations
83    // Order: system → tools → messages (canonical traversal)
84    let mut ttls = Vec::new();
85
86    // 1. Scan system blocks
87    if let Some(system) = &req.system
88        && let SystemParam::Blocks(blocks) = system
89    {
90        for tb in blocks {
91            push_ttl(&mut ttls, tb.cache_control.as_ref());
92        }
93    }
94
95    // 2. Scan tool definitions
96    if let Some(tools) = &req.tools {
97        for tool in tools {
98            push_ttl(&mut ttls, tool.cache_control.as_ref());
99        }
100    }
101
102    // 3-12. Scan message content blocks (including nested ToolResult blocks)
103    for message in &req.messages {
104        if let MessageContentParam::Blocks(blocks) = &message.content {
105            for block in blocks {
106                collect_block_param_ttls(&mut ttls, block);
107            }
108        }
109    }
110
111    if !validate_mixed_ttl_order(ttls) {
112        return Err(AnthropicError::Config(
113            "Invalid cache_control TTL ordering: 1h must precede 5m".into(),
114        ));
115    }
116
117    // Validate sampling parameters
118    if let Some(t) = req.temperature
119        && !(0.0..=1.0).contains(&t)
120    {
121        return Err(AnthropicError::Config(format!(
122            "Invalid temperature {t}: must be in [0.0, 1.0]"
123        )));
124    }
125
126    if let Some(p) = req.top_p
127        && (!(0.0..=1.0).contains(&p) || p == 0.0)
128    {
129        return Err(AnthropicError::Config(format!(
130            "Invalid top_p {p}: must be in (0.0, 1.0]"
131        )));
132    }
133
134    if let Some(k) = req.top_k
135        && k < 1
136    {
137        return Err(AnthropicError::Config(format!(
138            "Invalid top_k {k}: must be >= 1"
139        )));
140    }
141
142    if req.max_tokens == 0 {
143        return Err(AnthropicError::Config(
144            "max_tokens must be greater than 0".into(),
145        ));
146    }
147
148    Ok(())
149}
150
151/// API resource for the `/v1/messages` endpoints
152///
153/// Provides methods to create messages and count tokens.
154pub struct Messages<'c, C: Config> {
155    client: &'c Client<C>,
156}
157
158impl<'c, C: Config> Messages<'c, C> {
159    /// Creates a new Messages resource
160    #[must_use]
161    pub const fn new(client: &'c Client<C>) -> Self {
162        Self { client }
163    }
164
165    /// Create a new message
166    ///
167    /// # Errors
168    ///
169    /// Returns an error if:
170    /// - The request fails to send
171    /// - The `cache_control` TTL ordering is invalid (1h must precede 5m)
172    /// - The API returns an error
173    pub async fn create(
174        &self,
175        req: MessagesCreateRequest,
176    ) -> Result<MessagesCreateResponse, AnthropicError> {
177        // Centralized validation
178        validate_messages_create_request(&req)?;
179
180        self.client.post("/v1/messages", req).await
181    }
182
183    /// Count tokens for a message request
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if:
188    /// - The request fails to send
189    /// - The API returns an error
190    pub async fn count_tokens(
191        &self,
192        req: MessageTokensCountRequest,
193    ) -> Result<MessageTokensCountResponse, AnthropicError> {
194        // No TTL validation needed for token counting
195        self.client.post("/v1/messages/count_tokens", req).await
196    }
197
198    /// Create a new message with streaming response
199    ///
200    /// Returns a stream of SSE events that can be processed as they arrive.
201    /// The request will automatically have `stream: true` set.
202    ///
203    /// # Example
204    ///
205    /// ```ignore
206    /// use futures::StreamExt;
207    ///
208    /// let mut stream = client.messages().create_stream(req).await?;
209    /// while let Some(event) = stream.next().await {
210    ///     match event? {
211    ///         Event::ContentBlockDelta { delta, .. } => {
212    ///             if let ContentBlockDeltaData::TextDelta { text } = delta {
213    ///                 print!("{}", text);
214    ///             }
215    ///         }
216    ///         Event::MessageStop => break,
217    ///         _ => {}
218    ///     }
219    /// }
220    /// ```
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if:
225    /// - The request fails to send
226    /// - The API returns an error (non-2xx status)
227    #[cfg(feature = "streaming")]
228    pub async fn create_stream(
229        &self,
230        mut req: MessagesCreateRequest,
231    ) -> Result<crate::streaming::EventStream, AnthropicError> {
232        // Force streaming mode
233        req.stream = Some(true);
234
235        // Centralized validation
236        validate_messages_create_request(&req)?;
237
238        let response = self.client.post_stream("/v1/messages", req).await?;
239        Ok(crate::sse::streaming::event_stream_from_response(response))
240    }
241}
242
243// Add to client
244impl<C: Config> crate::Client<C> {
245    /// Returns the Messages API resource
246    #[must_use]
247    pub const fn messages(&self) -> Messages<'_, C> {
248        Messages::new(self)
249    }
250}
251
252// ============================================================================
253// TTL Validation Tests (12 cacheable locations)
254// ============================================================================
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::types::{
260        content::{
261            DocumentSource, ImageSource, MessageParam, MessageRole, TextBlockParam,
262            ToolResultContent, ToolResultContentBlock,
263        },
264        tools::Tool,
265    };
266
267    /// Create a base request with minimal valid fields
268    fn base_req(messages: Vec<MessageParam>) -> MessagesCreateRequest {
269        MessagesCreateRequest {
270            model: "claude-sonnet-4-20250514".into(),
271            max_tokens: 16,
272            messages,
273            ..Default::default()
274        }
275    }
276
277    /// Create a user message with content blocks
278    fn user_blocks(blocks: Vec<ContentBlockParam>) -> MessageParam {
279        MessageParam {
280            role: MessageRole::User,
281            content: MessageContentParam::Blocks(blocks),
282        }
283    }
284
285    /// Assert that a request with mixed TTLs in wrong order (5m before 1h) errors
286    fn assert_ttl_order_err(req: &MessagesCreateRequest) {
287        let err = validate_messages_create_request(req).unwrap_err();
288        match err {
289            AnthropicError::Config(msg) => {
290                assert!(
291                    msg.contains("TTL ordering"),
292                    "Expected TTL ordering error, got: {msg}"
293                );
294            }
295            _ => panic!("Expected AnthropicError::Config, got {err:?}"),
296        }
297    }
298
299    /// Assert that a request passes validation
300    fn assert_valid(req: &MessagesCreateRequest) {
301        assert!(
302            validate_messages_create_request(req).is_ok(),
303            "Expected valid request"
304        );
305    }
306
307    // -------------------------------------------------------------------------
308    // Location 1: SystemParam::Blocks TTL validation
309    // -------------------------------------------------------------------------
310
311    #[test]
312    fn ttl_system_block_5m_then_1h_fails() {
313        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
314            text: "hi".into(),
315            citations: None,
316            cache_control: None,
317        }])]);
318        req.system = Some(SystemParam::Blocks(vec![
319            TextBlockParam::with_cache_control("first", CacheControl::ephemeral_5m()),
320            TextBlockParam::with_cache_control("second", CacheControl::ephemeral_1h()),
321        ]));
322        assert_ttl_order_err(&req);
323    }
324
325    #[test]
326    fn ttl_system_block_1h_then_5m_passes() {
327        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
328            text: "hi".into(),
329            citations: None,
330            cache_control: None,
331        }])]);
332        req.system = Some(SystemParam::Blocks(vec![
333            TextBlockParam::with_cache_control("first", CacheControl::ephemeral_1h()),
334            TextBlockParam::with_cache_control("second", CacheControl::ephemeral_5m()),
335        ]));
336        assert_valid(&req);
337    }
338
339    // -------------------------------------------------------------------------
340    // Location 2: Tool.cache_control TTL validation
341    // -------------------------------------------------------------------------
342
343    #[test]
344    fn ttl_tool_definition_5m_then_1h_fails() {
345        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
346            text: "hi".into(),
347            citations: None,
348            cache_control: None,
349        }])]);
350        req.tools = Some(vec![
351            Tool {
352                name: "tool1".into(),
353                description: None,
354                input_schema: serde_json::json!({}),
355                cache_control: Some(CacheControl::ephemeral_5m()),
356                strict: None,
357            },
358            Tool {
359                name: "tool2".into(),
360                description: None,
361                input_schema: serde_json::json!({}),
362                cache_control: Some(CacheControl::ephemeral_1h()),
363                strict: None,
364            },
365        ]);
366        assert_ttl_order_err(&req);
367    }
368
369    #[test]
370    fn ttl_tool_definition_1h_then_5m_passes() {
371        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
372            text: "hi".into(),
373            citations: None,
374            cache_control: None,
375        }])]);
376        req.tools = Some(vec![
377            Tool {
378                name: "tool1".into(),
379                description: None,
380                input_schema: serde_json::json!({}),
381                cache_control: Some(CacheControl::ephemeral_1h()),
382                strict: None,
383            },
384            Tool {
385                name: "tool2".into(),
386                description: None,
387                input_schema: serde_json::json!({}),
388                cache_control: Some(CacheControl::ephemeral_5m()),
389                strict: None,
390            },
391        ]);
392        assert_valid(&req);
393    }
394
395    // -------------------------------------------------------------------------
396    // Location 3: ContentBlockParam::Text TTL validation
397    // -------------------------------------------------------------------------
398
399    #[test]
400    fn ttl_text_block_5m_then_1h_fails() {
401        let req = base_req(vec![user_blocks(vec![
402            ContentBlockParam::Text {
403                text: "first".into(),
404                citations: None,
405                cache_control: Some(CacheControl::ephemeral_5m()),
406            },
407            ContentBlockParam::Text {
408                text: "second".into(),
409                citations: None,
410                cache_control: Some(CacheControl::ephemeral_1h()),
411            },
412        ])]);
413        assert_ttl_order_err(&req);
414    }
415
416    #[test]
417    fn ttl_text_block_1h_then_5m_passes() {
418        let req = base_req(vec![user_blocks(vec![
419            ContentBlockParam::Text {
420                text: "first".into(),
421                citations: None,
422                cache_control: Some(CacheControl::ephemeral_1h()),
423            },
424            ContentBlockParam::Text {
425                text: "second".into(),
426                citations: None,
427                cache_control: Some(CacheControl::ephemeral_5m()),
428            },
429        ])]);
430        assert_valid(&req);
431    }
432
433    // -------------------------------------------------------------------------
434    // Location 4: ContentBlockParam::Image TTL validation
435    // -------------------------------------------------------------------------
436
437    #[test]
438    fn ttl_image_block_5m_then_1h_fails() {
439        let req = base_req(vec![user_blocks(vec![
440            ContentBlockParam::Image {
441                source: ImageSource::Base64 {
442                    media_type: "image/png".into(),
443                    data: String::new(),
444                },
445                cache_control: Some(CacheControl::ephemeral_5m()),
446            },
447            ContentBlockParam::Image {
448                source: ImageSource::Base64 {
449                    media_type: "image/png".into(),
450                    data: String::new(),
451                },
452                cache_control: Some(CacheControl::ephemeral_1h()),
453            },
454        ])]);
455        assert_ttl_order_err(&req);
456    }
457
458    // -------------------------------------------------------------------------
459    // Location 5: ContentBlockParam::Document TTL validation
460    // -------------------------------------------------------------------------
461
462    #[test]
463    fn ttl_document_block_5m_then_1h_fails() {
464        let req = base_req(vec![user_blocks(vec![
465            ContentBlockParam::Document {
466                source: DocumentSource::Base64 {
467                    media_type: "application/pdf".into(),
468                    data: String::new(),
469                },
470                cache_control: Some(CacheControl::ephemeral_5m()),
471            },
472            ContentBlockParam::Document {
473                source: DocumentSource::Base64 {
474                    media_type: "application/pdf".into(),
475                    data: String::new(),
476                },
477                cache_control: Some(CacheControl::ephemeral_1h()),
478            },
479        ])]);
480        assert_ttl_order_err(&req);
481    }
482
483    // -------------------------------------------------------------------------
484    // Location 6: ContentBlockParam::ToolUse TTL validation
485    // -------------------------------------------------------------------------
486
487    #[test]
488    fn ttl_tool_use_block_5m_then_1h_fails() {
489        let req = base_req(vec![user_blocks(vec![
490            ContentBlockParam::ToolUse {
491                id: "id1".into(),
492                name: "tool".into(),
493                input: serde_json::json!({}),
494                cache_control: Some(CacheControl::ephemeral_5m()),
495            },
496            ContentBlockParam::ToolUse {
497                id: "id2".into(),
498                name: "tool".into(),
499                input: serde_json::json!({}),
500                cache_control: Some(CacheControl::ephemeral_1h()),
501            },
502        ])]);
503        assert_ttl_order_err(&req);
504    }
505
506    #[test]
507    fn ttl_tool_use_block_1h_then_5m_passes() {
508        let req = base_req(vec![user_blocks(vec![
509            ContentBlockParam::ToolUse {
510                id: "id1".into(),
511                name: "tool".into(),
512                input: serde_json::json!({}),
513                cache_control: Some(CacheControl::ephemeral_1h()),
514            },
515            ContentBlockParam::ToolUse {
516                id: "id2".into(),
517                name: "tool".into(),
518                input: serde_json::json!({}),
519                cache_control: Some(CacheControl::ephemeral_5m()),
520            },
521        ])]);
522        assert_valid(&req);
523    }
524
525    // -------------------------------------------------------------------------
526    // Location 7: ContentBlockParam::ServerToolUse TTL validation
527    // -------------------------------------------------------------------------
528
529    #[test]
530    fn ttl_server_tool_use_block_5m_then_1h_fails() {
531        let req = base_req(vec![user_blocks(vec![
532            ContentBlockParam::ServerToolUse {
533                id: "id1".into(),
534                name: "web_search".into(),
535                input: serde_json::json!({}),
536                cache_control: Some(CacheControl::ephemeral_5m()),
537            },
538            ContentBlockParam::ServerToolUse {
539                id: "id2".into(),
540                name: "web_search".into(),
541                input: serde_json::json!({}),
542                cache_control: Some(CacheControl::ephemeral_1h()),
543            },
544        ])]);
545        assert_ttl_order_err(&req);
546    }
547
548    // -------------------------------------------------------------------------
549    // Location 8: ContentBlockParam::SearchResult TTL validation
550    // -------------------------------------------------------------------------
551
552    #[test]
553    fn ttl_search_result_block_5m_then_1h_fails() {
554        let req = base_req(vec![user_blocks(vec![
555            ContentBlockParam::SearchResult {
556                content: vec![],
557                source: "https://example.com".into(),
558                title: "Result".into(),
559                citations: None,
560                cache_control: Some(CacheControl::ephemeral_5m()),
561            },
562            ContentBlockParam::SearchResult {
563                content: vec![],
564                source: "https://example.com".into(),
565                title: "Result".into(),
566                citations: None,
567                cache_control: Some(CacheControl::ephemeral_1h()),
568            },
569        ])]);
570        assert_ttl_order_err(&req);
571    }
572
573    // -------------------------------------------------------------------------
574    // Location 9: ContentBlockParam::WebSearchToolResult TTL validation
575    // -------------------------------------------------------------------------
576
577    #[test]
578    fn ttl_web_search_tool_result_block_5m_then_1h_fails() {
579        let req = base_req(vec![user_blocks(vec![
580            ContentBlockParam::WebSearchToolResult {
581                tool_use_id: "id1".into(),
582                content: serde_json::json!({}),
583                cache_control: Some(CacheControl::ephemeral_5m()),
584            },
585            ContentBlockParam::WebSearchToolResult {
586                tool_use_id: "id2".into(),
587                content: serde_json::json!({}),
588                cache_control: Some(CacheControl::ephemeral_1h()),
589            },
590        ])]);
591        assert_ttl_order_err(&req);
592    }
593
594    // -------------------------------------------------------------------------
595    // Location 10: ContentBlockParam::ToolResult TTL validation
596    // -------------------------------------------------------------------------
597
598    #[test]
599    fn ttl_tool_result_block_5m_then_1h_fails() {
600        let req = base_req(vec![user_blocks(vec![
601            ContentBlockParam::ToolResult {
602                tool_use_id: "id1".into(),
603                content: None,
604                is_error: None,
605                cache_control: Some(CacheControl::ephemeral_5m()),
606            },
607            ContentBlockParam::ToolResult {
608                tool_use_id: "id2".into(),
609                content: None,
610                is_error: None,
611                cache_control: Some(CacheControl::ephemeral_1h()),
612            },
613        ])]);
614        assert_ttl_order_err(&req);
615    }
616
617    // -------------------------------------------------------------------------
618    // Location 11-12: Nested ToolResultContentBlock::{Text, Image} TTL validation
619    // -------------------------------------------------------------------------
620
621    #[test]
622    fn ttl_nested_tool_result_text_5m_then_1h_fails() {
623        let req = base_req(vec![user_blocks(vec![ContentBlockParam::ToolResult {
624            tool_use_id: "id1".into(),
625            content: Some(ToolResultContent::Blocks(vec![
626                ToolResultContentBlock::Text {
627                    text: "first".into(),
628                    cache_control: Some(CacheControl::ephemeral_5m()),
629                },
630                ToolResultContentBlock::Text {
631                    text: "second".into(),
632                    cache_control: Some(CacheControl::ephemeral_1h()),
633                },
634            ])),
635            is_error: None,
636            cache_control: None,
637        }])]);
638        assert_ttl_order_err(&req);
639    }
640
641    #[test]
642    fn ttl_nested_tool_result_image_5m_then_1h_fails() {
643        let req = base_req(vec![user_blocks(vec![ContentBlockParam::ToolResult {
644            tool_use_id: "id1".into(),
645            content: Some(ToolResultContent::Blocks(vec![
646                ToolResultContentBlock::Image {
647                    source: ImageSource::Base64 {
648                        media_type: "image/png".into(),
649                        data: String::new(),
650                    },
651                    cache_control: Some(CacheControl::ephemeral_5m()),
652                },
653                ToolResultContentBlock::Image {
654                    source: ImageSource::Base64 {
655                        media_type: "image/png".into(),
656                        data: String::new(),
657                    },
658                    cache_control: Some(CacheControl::ephemeral_1h()),
659                },
660            ])),
661            is_error: None,
662            cache_control: None,
663        }])]);
664        assert_ttl_order_err(&req);
665    }
666
667    #[test]
668    fn ttl_nested_tool_result_1h_then_5m_passes() {
669        let req = base_req(vec![user_blocks(vec![ContentBlockParam::ToolResult {
670            tool_use_id: "id1".into(),
671            content: Some(ToolResultContent::Blocks(vec![
672                ToolResultContentBlock::Text {
673                    text: "first".into(),
674                    cache_control: Some(CacheControl::ephemeral_1h()),
675                },
676                ToolResultContentBlock::Text {
677                    text: "second".into(),
678                    cache_control: Some(CacheControl::ephemeral_5m()),
679                },
680            ])),
681            is_error: None,
682            cache_control: None,
683        }])]);
684        assert_valid(&req);
685    }
686
687    // -------------------------------------------------------------------------
688    // Cross-location TTL ordering (canonical traversal: system → tools → messages)
689    // -------------------------------------------------------------------------
690
691    #[test]
692    fn ttl_system_5m_tool_1h_fails() {
693        // System comes before tools in canonical traversal
694        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
695            text: "hi".into(),
696            citations: None,
697            cache_control: None,
698        }])]);
699        req.system = Some(SystemParam::Blocks(vec![
700            TextBlockParam::with_cache_control("sys", CacheControl::ephemeral_5m()),
701        ]));
702        req.tools = Some(vec![Tool {
703            name: "tool".into(),
704            description: None,
705            input_schema: serde_json::json!({}),
706            cache_control: Some(CacheControl::ephemeral_1h()),
707            strict: None,
708        }]);
709        assert_ttl_order_err(&req);
710    }
711
712    #[test]
713    fn ttl_tool_5m_message_1h_fails() {
714        // Tools come before messages in canonical traversal
715        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
716            text: "hi".into(),
717            citations: None,
718            cache_control: Some(CacheControl::ephemeral_1h()),
719        }])]);
720        req.tools = Some(vec![Tool {
721            name: "tool".into(),
722            description: None,
723            input_schema: serde_json::json!({}),
724            cache_control: Some(CacheControl::ephemeral_5m()),
725            strict: None,
726        }]);
727        assert_ttl_order_err(&req);
728    }
729
730    #[test]
731    fn ttl_system_1h_tool_5m_message_none_passes() {
732        // Correct order: 1h → 5m across system and tools
733        let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
734            text: "hi".into(),
735            citations: None,
736            cache_control: None,
737        }])]);
738        req.system = Some(SystemParam::Blocks(vec![
739            TextBlockParam::with_cache_control("sys", CacheControl::ephemeral_1h()),
740        ]));
741        req.tools = Some(vec![Tool {
742            name: "tool".into(),
743            description: None,
744            input_schema: serde_json::json!({}),
745            cache_control: Some(CacheControl::ephemeral_5m()),
746            strict: None,
747        }]);
748        assert_valid(&req);
749    }
750
751    // -------------------------------------------------------------------------
752    // Edge cases: Thinking blocks (no cache_control), String content, no TTLs
753    // -------------------------------------------------------------------------
754
755    #[test]
756    fn ttl_thinking_blocks_ignored() {
757        // Thinking and RedactedThinking have no cache_control
758        let req = base_req(vec![user_blocks(vec![
759            ContentBlockParam::Thinking {
760                thinking: "thinking...".into(),
761                signature: "sig".into(),
762            },
763            ContentBlockParam::RedactedThinking {
764                data: "redacted".into(),
765            },
766            ContentBlockParam::Text {
767                text: "hi".into(),
768                citations: None,
769                cache_control: Some(CacheControl::ephemeral_1h()),
770            },
771        ])]);
772        assert_valid(&req);
773    }
774
775    #[test]
776    fn ttl_string_content_ignored() {
777        // String content (not Blocks) is skipped
778        let req = base_req(vec![MessageParam {
779            role: MessageRole::User,
780            content: MessageContentParam::String("hi".into()),
781        }]);
782        assert_valid(&req);
783    }
784
785    #[test]
786    fn ttl_no_cache_control_passes() {
787        // Request with no TTLs should pass
788        let req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
789            text: "hi".into(),
790            citations: None,
791            cache_control: None,
792        }])]);
793        assert_valid(&req);
794    }
795}