diff --git a/rig/rig-core/src/providers/anthropic/completion.rs b/rig/rig-core/src/providers/anthropic/completion.rs index 253277d6b..eaa48af7c 100644 --- a/rig/rig-core/src/providers/anthropic/completion.rs +++ b/rig/rig-core/src/providers/anthropic/completion.rs @@ -127,21 +127,78 @@ impl GetTokenUsage for Usage { } } -#[derive(Debug, Deserialize, Serialize)] +/// Tool definition for Anthropic function calling with optional cache control. +/// +/// Tools can be cached to reduce costs when using the same tool definitions +/// across multiple requests. +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct ToolDefinition { pub name: String, pub description: Option, pub input_schema: serde_json::Value, + /// Optional cache control for caching this tool definition + #[serde(skip_serializing_if = "Option::is_none")] + pub cache_control: Option, } -/// Cache control directive for Anthropic prompt caching +impl ToolDefinition { + /// Create a new tool definition + pub fn new(name: impl Into, description: impl Into, input_schema: serde_json::Value) -> Self { + Self { + name: name.into(), + description: Some(description.into()), + input_schema, + cache_control: None, + } + } + + /// Enable ephemeral caching for this tool definition. + /// + /// This allows Anthropic to cache the tool definition for cost savings + /// when using the same tools across multiple requests. + pub fn with_cache_control(mut self) -> Self { + self.cache_control = Some(CacheControl::Ephemeral); + self + } + + /// Alias for `with_cache_control()` - marks this tool for caching + pub fn cached(self) -> Self { + self.with_cache_control() + } +} + +/// Cache control directive for Anthropic prompt caching. +/// +/// Anthropic's prompt caching allows you to cache portions of your prompt +/// (system instructions, tools, and conversation history) for reuse across +/// multiple API calls, reducing costs and latency. +/// +/// # Example +/// ```ignore +/// use rig::providers::anthropic::completion::{Content, CacheControl}; +/// +/// let content = Content::text("Hello").with_cache_control(); +/// ``` +/// +/// See #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum CacheControl { Ephemeral, } -/// System message content block with optional cache control +/// System message content block with optional cache control. +/// +/// System content blocks can be cached for cost savings when using +/// the same system prompt across multiple requests. +/// +/// # Example +/// ```ignore +/// use rig::providers::anthropic::completion::SystemContent; +/// +/// // Create a cached system prompt +/// let system = SystemContent::text("You are a helpful assistant.").cached(); +/// ``` #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum SystemContent { @@ -152,6 +209,33 @@ pub enum SystemContent { }, } +impl SystemContent { + /// Create a new text system content block + pub fn text(text: impl Into) -> Self { + SystemContent::Text { + text: text.into(), + cache_control: None, + } + } + + /// Enable ephemeral caching for this system content block. + /// + /// This allows Anthropic to cache the system prompt for cost savings. + pub fn with_cache_control(self) -> Self { + match self { + SystemContent::Text { text, .. } => SystemContent::Text { + text, + cache_control: Some(CacheControl::Ephemeral), + }, + } + } + + /// Alias for `with_cache_control()` - marks this content for caching + pub fn cached(self) -> Self { + self.with_cache_control() + } +} + impl TryFrom for completion::CompletionResponse { type Error = CompletionError; @@ -246,6 +330,98 @@ impl FromStr for Content { } } +impl Content { + /// Create a new text content block + pub fn text(text: impl Into) -> Self { + Content::Text { + text: text.into(), + cache_control: None, + } + } + + /// Create a new image content block from a base64-encoded image + pub fn image_base64(data: impl Into, media_type: ImageFormat) -> Self { + Content::Image { + source: ImageSource { + data: ImageSourceData::Base64(data.into()), + media_type, + r#type: SourceType::BASE64, + }, + cache_control: None, + } + } + + /// Create a new image content block from a URL + pub fn image_url(url: impl Into, media_type: ImageFormat) -> Self { + Content::Image { + source: ImageSource { + data: ImageSourceData::Url(url.into()), + media_type, + r#type: SourceType::URL, + }, + cache_control: None, + } + } + + /// Create a new document content block + pub fn document(data: impl Into, media_type: DocumentFormat) -> Self { + Content::Document { + source: DocumentSource { + data: data.into(), + media_type, + r#type: SourceType::BASE64, + }, + cache_control: None, + } + } + + /// Enable ephemeral caching for this content block. + /// + /// This allows Anthropic to cache the content for cost savings. + /// Works on Text, Image, Document, and ToolResult content types. + /// + /// # Example + /// ```ignore + /// use rig::providers::anthropic::completion::Content; + /// + /// let cached_content = Content::text("Important context...").with_cache_control(); + /// ``` + pub fn with_cache_control(self) -> Self { + match self { + Content::Text { text, .. } => Content::Text { + text, + cache_control: Some(CacheControl::Ephemeral), + }, + Content::Image { source, .. } => Content::Image { + source, + cache_control: Some(CacheControl::Ephemeral), + }, + Content::Document { source, .. } => Content::Document { + source, + cache_control: Some(CacheControl::Ephemeral), + }, + Content::ToolResult { + tool_use_id, + content, + is_error, + .. + } => Content::ToolResult { + tool_use_id, + content, + is_error, + cache_control: Some(CacheControl::Ephemeral), + }, + // ToolUse and Thinking don't support cache_control + other => other, + } + } + + /// Alias for `with_cache_control()` - marks this content for caching + pub fn cached(self) -> Self { + self.with_cache_control() + } +} + #[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ToolResultContent { @@ -664,6 +840,38 @@ impl TryFrom for message::Message { } } +/// Anthropic completion model with support for prompt caching. +/// +/// # Prompt Caching +/// +/// Anthropic's prompt caching can significantly reduce costs (up to 90%) and latency +/// for multi-turn conversations and repeated prompts. Enable it with [`with_prompt_caching()`](Self::with_prompt_caching). +/// +/// When enabled automatically, cache breakpoints are added to: +/// - The system prompt +/// - The last content block of the last message +/// +/// For finer control, use the builder methods on content types: +/// - [`Content::with_cache_control()`] / [`Content::cached()`] +/// - [`SystemContent::with_cache_control()`] / [`SystemContent::cached()`] +/// - [`ToolDefinition::with_cache_control()`] / [`ToolDefinition::cached()`] +/// +/// # Example +/// +/// ```ignore +/// use rig::providers::anthropic::{Client, CLAUDE_3_5_SONNET}; +/// +/// let client = Client::new("your-api-key"); +/// let model = client.completion_model(CLAUDE_3_5_SONNET) +/// .with_prompt_caching(); // Enable automatic caching +/// +/// // The agent will automatically cache system prompt and conversation history +/// let agent = model.agent() +/// .preamble("You are a helpful assistant...") +/// .build(); +/// ``` +/// +/// See for details. #[derive(Clone)] pub struct CompletionModel { pub(crate) client: Client, @@ -883,6 +1091,7 @@ impl TryFrom> for AnthropicCompletionRequest { name: tool.name, description: Some(tool.description), input_schema: tool.parameters, + cache_control: None, }) .collect::>(); @@ -1464,4 +1673,80 @@ mod tests { } } } + + #[test] + fn test_content_builder_methods() { + // Test Content::text() builder + let content = Content::text("Hello world"); + match content { + Content::Text { text, cache_control } => { + assert_eq!(text, "Hello world"); + assert!(cache_control.is_none()); + } + _ => panic!("Expected text content"), + } + + // Test Content::with_cache_control() / cached() + let cached_content = Content::text("Cached text").cached(); + match cached_content { + Content::Text { text, cache_control } => { + assert_eq!(text, "Cached text"); + assert_eq!(cache_control, Some(CacheControl::Ephemeral)); + } + _ => panic!("Expected text content"), + } + + // Test image content with cache control + let image = Content::image_base64("base64data", ImageFormat::PNG).with_cache_control(); + match image { + Content::Image { cache_control, .. } => { + assert_eq!(cache_control, Some(CacheControl::Ephemeral)); + } + _ => panic!("Expected image content"), + } + } + + #[test] + fn test_system_content_builder_methods() { + // Test SystemContent::text() builder + let system = SystemContent::text("You are helpful"); + match system { + SystemContent::Text { text, cache_control } => { + assert_eq!(text, "You are helpful"); + assert!(cache_control.is_none()); + } + } + + // Test SystemContent::cached() + let cached_system = SystemContent::text("Cached system prompt").cached(); + match cached_system { + SystemContent::Text { text, cache_control } => { + assert_eq!(text, "Cached system prompt"); + assert_eq!(cache_control, Some(CacheControl::Ephemeral)); + } + } + } + + #[test] + fn test_tool_definition_cache_control() { + // Test ToolDefinition builder + let tool = ToolDefinition::new( + "get_weather", + "Get current weather", + json!({"type": "object", "properties": {"location": {"type": "string"}}}), + ); + assert!(tool.cache_control.is_none()); + + // Test ToolDefinition::cached() + let cached_tool = ToolDefinition::new( + "get_weather", + "Get current weather", + json!({"type": "object", "properties": {"location": {"type": "string"}}}), + ).cached(); + assert_eq!(cached_tool.cache_control, Some(CacheControl::Ephemeral)); + + // Test serialization + let json = serde_json::to_string(&cached_tool).unwrap(); + assert!(json.contains(r#""cache_control":{"type":"ephemeral"}"#)); + } } diff --git a/rig/rig-core/src/providers/anthropic/mod.rs b/rig/rig-core/src/providers/anthropic/mod.rs index c13b420b5..7bec68d66 100644 --- a/rig/rig-core/src/providers/anthropic/mod.rs +++ b/rig/rig-core/src/providers/anthropic/mod.rs @@ -4,10 +4,32 @@ //! ``` //! use rig::providers::anthropic; //! -//! let client = anthropic::Anthropic::new("YOUR_API_KEY"); +//! let client = anthropic::Client::new("YOUR_API_KEY"); //! //! let sonnet = client.completion_model(anthropic::CLAUDE_3_5_SONNET); //! ``` +//! +//! # Prompt Caching +//! +//! Anthropic's prompt caching can reduce costs by up to 90% for multi-turn conversations. +//! Enable automatic caching with [`with_prompt_caching()`](completion::CompletionModel::with_prompt_caching): +//! +//! ```ignore +//! let model = client.completion_model(anthropic::CLAUDE_3_5_SONNET) +//! .with_prompt_caching(); +//! ``` +//! +//! For finer control, use the builder methods on content types: +//! +//! ```ignore +//! use rig::providers::anthropic::completion::{Content, SystemContent, CacheControl}; +//! +//! // Cache specific content blocks +//! let cached_text = Content::text("Important context...").cached(); +//! let cached_system = SystemContent::text("You are a helpful assistant.").cached(); +//! ``` +//! +//! See for more details. pub mod client; pub mod completion; @@ -15,3 +37,7 @@ pub mod decoders; pub mod streaming; pub use client::{Client, ClientBuilder}; +pub use completion::{ + CacheControl, Content, SystemContent, ToolDefinition, + CLAUDE_3_5_HAIKU, CLAUDE_3_5_SONNET, CLAUDE_3_7_SONNET, CLAUDE_4_OPUS, CLAUDE_4_SONNET, +}; diff --git a/rig/rig-core/src/providers/anthropic/streaming.rs b/rig/rig-core/src/providers/anthropic/streaming.rs index 3987a1966..d7b30b43c 100644 --- a/rig/rig-core/src/providers/anthropic/streaming.rs +++ b/rig/rig-core/src/providers/anthropic/streaming.rs @@ -214,6 +214,7 @@ where name: tool.name, description: Some(tool.description), input_schema: tool.parameters, + cache_control: None, }) .collect::>(), "tool_choice": ToolChoice::Auto,