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
16fn 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
27fn 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
42fn 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 }
65 }
66}
67
68fn validate_messages_create_request(req: &MessagesCreateRequest) -> Result<(), AnthropicError> {
82 let mut ttls = Vec::new();
85
86 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 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 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 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
151pub struct Messages<'c, C: Config> {
155 client: &'c Client<C>,
156}
157
158impl<'c, C: Config> Messages<'c, C> {
159 #[must_use]
161 pub const fn new(client: &'c Client<C>) -> Self {
162 Self { client }
163 }
164
165 pub async fn create(
174 &self,
175 req: MessagesCreateRequest,
176 ) -> Result<MessagesCreateResponse, AnthropicError> {
177 validate_messages_create_request(&req)?;
179
180 self.client.post("/v1/messages", req).await
181 }
182
183 pub async fn count_tokens(
191 &self,
192 req: MessageTokensCountRequest,
193 ) -> Result<MessageTokensCountResponse, AnthropicError> {
194 self.client.post("/v1/messages/count_tokens", req).await
196 }
197
198 #[cfg(feature = "streaming")]
228 pub async fn create_stream(
229 &self,
230 mut req: MessagesCreateRequest,
231 ) -> Result<crate::streaming::EventStream, AnthropicError> {
232 req.stream = Some(true);
234
235 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
243impl<C: Config> crate::Client<C> {
245 #[must_use]
247 pub const fn messages(&self) -> Messages<'_, C> {
248 Messages::new(self)
249 }
250}
251
252#[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 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 fn user_blocks(blocks: Vec<ContentBlockParam>) -> MessageParam {
279 MessageParam {
280 role: MessageRole::User,
281 content: MessageContentParam::Blocks(blocks),
282 }
283 }
284
285 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 fn assert_valid(req: &MessagesCreateRequest) {
301 assert!(
302 validate_messages_create_request(req).is_ok(),
303 "Expected valid request"
304 );
305 }
306
307 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[test]
692 fn ttl_system_5m_tool_1h_fails() {
693 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 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 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 #[test]
756 fn ttl_thinking_blocks_ignored() {
757 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 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 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}