use crate::ApiError;
use crate::Client;
use crate::ClientError;
use crate::Beta;
use crate::messages::chunk_stream::ChunkStream;
use crate::messages::{
MessageChunk, MessagesError, MessagesRequestBody, MessagesResponseBody,
StreamError, StreamOption, CacheTtl,
};
use futures_core::Stream;
fn has_one_hour_ttl(request_body: &MessagesRequestBody) -> bool {
for message in &request_body.messages {
match &message.content {
crate::messages::Content::SingleText(_) => {
},
crate::messages::Content::MultipleBlocks(blocks) => {
for content_block in blocks {
if let Some(cache_control) = &content_block.cache_control() {
if let Some(ttl) = &cache_control.ttl {
if *ttl == CacheTtl::OneHour {
return true;
}
}
}
}
},
}
}
if let Some(system_prompt) = &request_body.system {
match system_prompt {
crate::messages::SystemPrompt::Simple(_) => {
},
crate::messages::SystemPrompt::Advanced(blocks) => {
for content_block in blocks {
if let Some(cache_control) = &content_block.cache_control() {
if let Some(ttl) = &cache_control.ttl {
if *ttl == CacheTtl::OneHour {
return true;
}
}
}
}
},
}
}
false
}
pub(crate) async fn create_a_message(
client: &Client,
request_body: MessagesRequestBody,
) -> Result<MessagesResponseBody, MessagesError> {
if let Some(stream) = &request_body.stream {
if *stream != StreamOption::ReturnOnce {
return Err(MessagesError::StreamOptionMismatch);
}
}
let mut request_builder = client.post("https://api.anthropic.com/v1/messages");
if has_one_hour_ttl(&request_body) {
request_builder = request_builder.header("anthropic-beta", Beta::ExtendedCacheTtl2025_04_11.to_string());
}
let response = request_builder
.json(&request_body)
.send()
.await
.map_err(ClientError::HttpRequestError)?;
let status_code = response.status();
let response_text = response
.text()
.await
.map_err(ClientError::ReadResponseTextFailed)?;
if status_code.is_success() {
serde_json::from_str(&response_text).map_err(|error| {
{
ClientError::ResponseDeserializationFailed {
error,
text: response_text,
}
}
.into()
})
}
else {
let error_response =
serde_json::from_str(&response_text).map_err(|error| {
ClientError::ErrorResponseDeserializationFailed {
error,
text: response_text,
}
})?;
Err(ApiError::new(status_code, error_response).into())
}
}
pub(crate) async fn create_a_message_stream(
client: &Client,
request_body: MessagesRequestBody,
) -> Result<impl Stream<Item = Result<MessageChunk, StreamError>>, MessagesError>
{
if let None = &request_body.stream {
return Err(MessagesError::StreamOptionMismatch);
}
if let Some(stream) = &request_body.stream {
if *stream != StreamOption::ReturnStream {
return Err(MessagesError::StreamOptionMismatch);
}
}
let mut request_builder = client.post("https://api.anthropic.com/v1/messages");
if has_one_hour_ttl(&request_body) {
request_builder = request_builder.header("anthropic-beta", Beta::ExtendedCacheTtl2025_04_11.to_string());
}
let response = request_builder
.json(&request_body)
.send()
.await
.map_err(ClientError::HttpRequestError)?;
let status_code = response.status();
if status_code.is_success() {
let byte_stream = response.bytes_stream();
let chunk_stream = ChunkStream::new(byte_stream);
Ok(chunk_stream)
}
else {
let response_text = response
.text()
.await
.map_err(ClientError::ReadResponseTextFailed)?;
let error_response =
serde_json::from_str(&response_text).map_err(|error| {
ClientError::ErrorResponseDeserializationFailed {
error,
text: response_text,
}
})?;
Err(ApiError::new(status_code, error_response).into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::messages::{
CacheControl, CacheControlType, ClaudeModel, ContentBlock, MaxTokens, Message,
MessagesRequestBody, Role, SystemPrompt, TextContentBlock,
};
#[test]
fn test_has_one_hour_ttl() {
let request_body = MessagesRequestBody {
model: ClaudeModel::Claude3Sonnet20240229,
max_tokens: MaxTokens::new(1024, ClaudeModel::Claude3Sonnet20240229).unwrap(),
messages: vec![Message::user("Hello")],
..Default::default()
};
assert!(!has_one_hour_ttl(&request_body));
let message = Message {
role: Role::User,
content: crate::messages::Content::MultipleBlocks(vec![
ContentBlock::Text(TextContentBlock::new_with_cache_control(
"Hello",
CacheControl {
_type: CacheControlType::Ephemeral,
ttl: Some(CacheTtl::OneHour),
},
)),
]),
};
let request_body = MessagesRequestBody {
model: ClaudeModel::Claude3Sonnet20240229,
max_tokens: MaxTokens::new(1024, ClaudeModel::Claude3Sonnet20240229).unwrap(),
messages: vec![message],
..Default::default()
};
assert!(has_one_hour_ttl(&request_body));
let system_prompt = SystemPrompt::from_text_blocks_with_cache_control(vec![
("You are a helpful assistant.", None),
(
"Cached information",
Some(CacheControl {
_type: CacheControlType::Ephemeral,
ttl: Some(CacheTtl::OneHour),
}),
),
]);
let request_body = MessagesRequestBody {
model: ClaudeModel::Claude3Sonnet20240229,
max_tokens: MaxTokens::new(1024, ClaudeModel::Claude3Sonnet20240229).unwrap(),
messages: vec![Message::user("Hello")],
system: Some(system_prompt),
..Default::default()
};
assert!(has_one_hour_ttl(&request_body));
}
}