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
17fn 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
28fn 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
43fn 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 }
66 }
67}
68
69fn validate_messages_create_request(req: &MessagesCreateRequest) -> Result<(), AnthropicError> {
83 let mut ttls = Vec::new();
86
87 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 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 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 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
152pub struct Messages<'c, C: Config> {
156 client: &'c Client<C>,
157}
158
159impl<'c, C: Config> Messages<'c, C> {
160 #[must_use]
162 pub const fn new(client: &'c Client<C>) -> Self {
163 Self { client }
164 }
165
166 pub async fn create(
175 &self,
176 req: MessagesCreateRequest,
177 ) -> Result<MessagesCreateResponse, AnthropicError> {
178 validate_messages_create_request(&req)?;
180
181 self.client.post("/v1/messages", req).await
182 }
183
184 pub async fn count_tokens(
192 &self,
193 req: MessageTokensCountRequest,
194 ) -> Result<MessageTokensCountResponse, AnthropicError> {
195 self.client.post("/v1/messages/count_tokens", req).await
197 }
198
199 #[cfg(feature = "streaming")]
229 pub async fn create_stream(
230 &self,
231 mut req: MessagesCreateRequest,
232 ) -> Result<crate::streaming::EventStream, AnthropicError> {
233 req.stream = Some(true);
235
236 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
244impl<C: Config> crate::Client<C> {
246 #[must_use]
248 pub const fn messages(&self) -> Messages<'_, C> {
249 Messages::new(self)
250 }
251}
252
253#[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 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 fn user_blocks(blocks: Vec<ContentBlockParam>) -> MessageParam {
281 MessageParam {
282 role: MessageRole::User,
283 content: MessageContentParam::Blocks(blocks),
284 }
285 }
286
287 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 fn assert_valid(req: &MessagesCreateRequest) {
303 assert!(
304 validate_messages_create_request(req).is_ok(),
305 "Expected valid request"
306 );
307 }
308
309 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
694 fn ttl_system_5m_tool_1h_fails() {
695 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 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 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 #[test]
758 fn ttl_thinking_blocks_ignored() {
759 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 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 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}