Skip to main content

anthropic_async/resources/
messages.rs

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