use crate::client::Client;
use crate::config::Config;
use crate::error::AnthropicError;
use crate::types::common::CacheControl;
use crate::types::common::CacheTtl;
use crate::types::common::validate_mixed_ttl_order;
use crate::types::content::ContentBlockParam;
use crate::types::content::MessageContentParam;
use crate::types::content::SystemParam;
use crate::types::content::ToolResultContent;
use crate::types::content::ToolResultContentBlock;
use crate::types::messages::MessageTokensCountRequest;
use crate::types::messages::MessageTokensCountResponse;
use crate::types::messages::MessagesCreateRequest;
use crate::types::messages::MessagesCreateResponse;
fn push_ttl(ttls: &mut Vec<CacheTtl>, cache_control: Option<&CacheControl>) {
if let Some(ttl) = cache_control.and_then(|cc| cc.ttl.clone()) {
ttls.push(ttl);
}
}
fn collect_tool_result_content_ttls(ttls: &mut Vec<CacheTtl>, content: Option<&ToolResultContent>) {
let Some(content) = content else { return };
if let ToolResultContent::Blocks(blocks) = content {
for block in blocks {
match block {
ToolResultContentBlock::Text { cache_control, .. }
| ToolResultContentBlock::Image { cache_control, .. } => {
push_ttl(ttls, cache_control.as_ref());
}
}
}
}
}
fn collect_block_param_ttls(ttls: &mut Vec<CacheTtl>, block: &ContentBlockParam) {
match block {
ContentBlockParam::Text { cache_control, .. }
| ContentBlockParam::Image { cache_control, .. }
| ContentBlockParam::Document { cache_control, .. }
| ContentBlockParam::ToolUse { cache_control, .. }
| ContentBlockParam::ServerToolUse { cache_control, .. }
| ContentBlockParam::SearchResult { cache_control, .. }
| ContentBlockParam::WebSearchToolResult { cache_control, .. } => {
push_ttl(ttls, cache_control.as_ref());
}
ContentBlockParam::ToolResult {
cache_control,
content,
..
} => {
push_ttl(ttls, cache_control.as_ref());
collect_tool_result_content_ttls(ttls, content.as_ref());
}
ContentBlockParam::Thinking { .. } | ContentBlockParam::RedactedThinking { .. } => {
}
}
}
fn validate_messages_create_request(req: &MessagesCreateRequest) -> Result<(), AnthropicError> {
let mut ttls = Vec::new();
if let Some(system) = &req.system
&& let SystemParam::Blocks(blocks) = system
{
for tb in blocks {
push_ttl(&mut ttls, tb.cache_control.as_ref());
}
}
if let Some(tools) = &req.tools {
for tool in tools {
push_ttl(&mut ttls, tool.cache_control.as_ref());
}
}
for message in &req.messages {
if let MessageContentParam::Blocks(blocks) = &message.content {
for block in blocks {
collect_block_param_ttls(&mut ttls, block);
}
}
}
if !validate_mixed_ttl_order(ttls) {
return Err(AnthropicError::Config(
"Invalid cache_control TTL ordering: 1h must precede 5m".into(),
));
}
if let Some(t) = req.temperature
&& !(0.0..=1.0).contains(&t)
{
return Err(AnthropicError::Config(format!(
"Invalid temperature {t}: must be in [0.0, 1.0]"
)));
}
if let Some(p) = req.top_p
&& (!(0.0..=1.0).contains(&p) || p == 0.0)
{
return Err(AnthropicError::Config(format!(
"Invalid top_p {p}: must be in (0.0, 1.0]"
)));
}
if let Some(k) = req.top_k
&& k < 1
{
return Err(AnthropicError::Config(format!(
"Invalid top_k {k}: must be >= 1"
)));
}
if req.max_tokens == 0 {
return Err(AnthropicError::Config(
"max_tokens must be greater than 0".into(),
));
}
Ok(())
}
pub struct Messages<'c, C: Config> {
client: &'c Client<C>,
}
impl<'c, C: Config> Messages<'c, C> {
#[must_use]
pub const fn new(client: &'c Client<C>) -> Self {
Self { client }
}
pub async fn create(
&self,
req: MessagesCreateRequest,
) -> Result<MessagesCreateResponse, AnthropicError> {
validate_messages_create_request(&req)?;
self.client.post("/v1/messages", req).await
}
pub async fn count_tokens(
&self,
req: MessageTokensCountRequest,
) -> Result<MessageTokensCountResponse, AnthropicError> {
self.client.post("/v1/messages/count_tokens", req).await
}
#[cfg(feature = "streaming")]
pub async fn create_stream(
&self,
mut req: MessagesCreateRequest,
) -> Result<crate::streaming::EventStream, AnthropicError> {
req.stream = Some(true);
validate_messages_create_request(&req)?;
let response = self.client.post_stream("/v1/messages", req).await?;
Ok(crate::sse::streaming::event_stream_from_response(response))
}
}
impl<C: Config> crate::Client<C> {
#[must_use]
pub const fn messages(&self) -> Messages<'_, C> {
Messages::new(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::content::DocumentSource;
use crate::types::content::ImageSource;
use crate::types::content::MessageParam;
use crate::types::content::MessageRole;
use crate::types::content::TextBlockParam;
use crate::types::content::ToolResultContent;
use crate::types::content::ToolResultContentBlock;
use crate::types::tools::Tool;
fn base_req(messages: Vec<MessageParam>) -> MessagesCreateRequest {
MessagesCreateRequest {
model: "claude-sonnet-4-20250514".into(),
max_tokens: 16,
messages,
..Default::default()
}
}
fn user_blocks(blocks: Vec<ContentBlockParam>) -> MessageParam {
MessageParam {
role: MessageRole::User,
content: MessageContentParam::Blocks(blocks),
}
}
fn assert_ttl_order_err(req: &MessagesCreateRequest) {
let err = validate_messages_create_request(req).unwrap_err();
match err {
AnthropicError::Config(msg) => {
assert!(
msg.contains("TTL ordering"),
"Expected TTL ordering error, got: {msg}"
);
}
_ => panic!("Expected AnthropicError::Config, got {err:?}"),
}
}
fn assert_valid(req: &MessagesCreateRequest) {
assert!(
validate_messages_create_request(req).is_ok(),
"Expected valid request"
);
}
#[test]
fn ttl_system_block_5m_then_1h_fails() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
req.system = Some(SystemParam::Blocks(vec![
TextBlockParam::with_cache_control("first", CacheControl::ephemeral_5m()),
TextBlockParam::with_cache_control("second", CacheControl::ephemeral_1h()),
]));
assert_ttl_order_err(&req);
}
#[test]
fn ttl_system_block_1h_then_5m_passes() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
req.system = Some(SystemParam::Blocks(vec![
TextBlockParam::with_cache_control("first", CacheControl::ephemeral_1h()),
TextBlockParam::with_cache_control("second", CacheControl::ephemeral_5m()),
]));
assert_valid(&req);
}
#[test]
fn ttl_tool_definition_5m_then_1h_fails() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
req.tools = Some(vec![
Tool {
name: "tool1".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
strict: None,
},
Tool {
name: "tool2".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
strict: None,
},
]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_tool_definition_1h_then_5m_passes() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
req.tools = Some(vec![
Tool {
name: "tool1".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
strict: None,
},
Tool {
name: "tool2".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
strict: None,
},
]);
assert_valid(&req);
}
#[test]
fn ttl_text_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::Text {
text: "first".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::Text {
text: "second".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_text_block_1h_then_5m_passes() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::Text {
text: "first".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_1h()),
},
ContentBlockParam::Text {
text: "second".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_5m()),
},
])]);
assert_valid(&req);
}
#[test]
fn ttl_image_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::Image {
source: ImageSource::Base64 {
media_type: "image/png".into(),
data: String::new(),
},
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::Image {
source: ImageSource::Base64 {
media_type: "image/png".into(),
data: String::new(),
},
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_document_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::Document {
source: DocumentSource::Base64 {
media_type: "application/pdf".into(),
data: String::new(),
},
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::Document {
source: DocumentSource::Base64 {
media_type: "application/pdf".into(),
data: String::new(),
},
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_tool_use_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::ToolUse {
id: "id1".into(),
name: "tool".into(),
input: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::ToolUse {
id: "id2".into(),
name: "tool".into(),
input: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_tool_use_block_1h_then_5m_passes() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::ToolUse {
id: "id1".into(),
name: "tool".into(),
input: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
},
ContentBlockParam::ToolUse {
id: "id2".into(),
name: "tool".into(),
input: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
},
])]);
assert_valid(&req);
}
#[test]
fn ttl_server_tool_use_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::ServerToolUse {
id: "id1".into(),
name: "web_search".into(),
input: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::ServerToolUse {
id: "id2".into(),
name: "web_search".into(),
input: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_search_result_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::SearchResult {
content: vec![],
source: "https://example.com".into(),
title: "Result".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::SearchResult {
content: vec![],
source: "https://example.com".into(),
title: "Result".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_web_search_tool_result_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::WebSearchToolResult {
tool_use_id: "id1".into(),
content: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::WebSearchToolResult {
tool_use_id: "id2".into(),
content: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_tool_result_block_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::ToolResult {
tool_use_id: "id1".into(),
content: None,
is_error: None,
cache_control: Some(CacheControl::ephemeral_5m()),
},
ContentBlockParam::ToolResult {
tool_use_id: "id2".into(),
content: None,
is_error: None,
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_nested_tool_result_text_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![ContentBlockParam::ToolResult {
tool_use_id: "id1".into(),
content: Some(ToolResultContent::Blocks(vec![
ToolResultContentBlock::Text {
text: "first".into(),
cache_control: Some(CacheControl::ephemeral_5m()),
},
ToolResultContentBlock::Text {
text: "second".into(),
cache_control: Some(CacheControl::ephemeral_1h()),
},
])),
is_error: None,
cache_control: None,
}])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_nested_tool_result_image_5m_then_1h_fails() {
let req = base_req(vec![user_blocks(vec![ContentBlockParam::ToolResult {
tool_use_id: "id1".into(),
content: Some(ToolResultContent::Blocks(vec![
ToolResultContentBlock::Image {
source: ImageSource::Base64 {
media_type: "image/png".into(),
data: String::new(),
},
cache_control: Some(CacheControl::ephemeral_5m()),
},
ToolResultContentBlock::Image {
source: ImageSource::Base64 {
media_type: "image/png".into(),
data: String::new(),
},
cache_control: Some(CacheControl::ephemeral_1h()),
},
])),
is_error: None,
cache_control: None,
}])]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_nested_tool_result_1h_then_5m_passes() {
let req = base_req(vec![user_blocks(vec![ContentBlockParam::ToolResult {
tool_use_id: "id1".into(),
content: Some(ToolResultContent::Blocks(vec![
ToolResultContentBlock::Text {
text: "first".into(),
cache_control: Some(CacheControl::ephemeral_1h()),
},
ToolResultContentBlock::Text {
text: "second".into(),
cache_control: Some(CacheControl::ephemeral_5m()),
},
])),
is_error: None,
cache_control: None,
}])]);
assert_valid(&req);
}
#[test]
fn ttl_system_5m_tool_1h_fails() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
req.system = Some(SystemParam::Blocks(vec![
TextBlockParam::with_cache_control("sys", CacheControl::ephemeral_5m()),
]));
req.tools = Some(vec![Tool {
name: "tool".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_1h()),
strict: None,
}]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_tool_5m_message_1h_fails() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_1h()),
}])]);
req.tools = Some(vec![Tool {
name: "tool".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
strict: None,
}]);
assert_ttl_order_err(&req);
}
#[test]
fn ttl_system_1h_tool_5m_message_none_passes() {
let mut req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
req.system = Some(SystemParam::Blocks(vec![
TextBlockParam::with_cache_control("sys", CacheControl::ephemeral_1h()),
]));
req.tools = Some(vec![Tool {
name: "tool".into(),
description: None,
input_schema: serde_json::json!({}),
cache_control: Some(CacheControl::ephemeral_5m()),
strict: None,
}]);
assert_valid(&req);
}
#[test]
fn ttl_thinking_blocks_ignored() {
let req = base_req(vec![user_blocks(vec![
ContentBlockParam::Thinking {
thinking: "thinking...".into(),
signature: "sig".into(),
},
ContentBlockParam::RedactedThinking {
data: "redacted".into(),
},
ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: Some(CacheControl::ephemeral_1h()),
},
])]);
assert_valid(&req);
}
#[test]
fn ttl_string_content_ignored() {
let req = base_req(vec![MessageParam {
role: MessageRole::User,
content: MessageContentParam::String("hi".into()),
}]);
assert_valid(&req);
}
#[test]
fn ttl_no_cache_control_passes() {
let req = base_req(vec![user_blocks(vec![ContentBlockParam::Text {
text: "hi".into(),
citations: None,
cache_control: None,
}])]);
assert_valid(&req);
}
}