Skip to main content

behest_core/
cache.rs

1//! Prompt caching control types.
2//!
3//! Provider-neutral abstractions for marking message content and tool
4//! definitions as eligible for prompt caching. Adapters translate these
5//! markers to provider-specific wire formats (e.g. Anthropic's
6//! `cache_control` blocks) or rely on the provider's automatic caching
7//! (e.g. OpenAI's prefix cache).
8//!
9//! # Stability model
10//!
11//! - `CacheControl` is a per-`ContentPart` and per-`ToolSpec` marker.
12//! - A marker on the last content block of a stable region (system
13//!   prompt, tool definitions, conversation tail) instructs the provider
14//!   to create a cache entry covering everything from the start of the
15//!   prompt up to and including that block.
16//! - Markers have no effect on providers that do not support caching;
17//!   they are silently ignored.
18//!
19//! # Example
20//!
21//! ```rust
22//! use behest_core::cache::CacheControl;
23//! use behest_core::message::{ContentPart, Message};
24//!
25//! let ctrl = CacheControl::ephemeral();
26//! let part = ContentPart::text("You are a research assistant.").with_cache_control(ctrl);
27//! let message = Message::system_text("You are a research assistant.")
28//!     .mark_cache_breakpoint();
29//! ```
30
31use serde::{Deserialize, Serialize};
32
33/// Provider-neutral cache control marker.
34///
35/// Attach to a [`crate::message::ContentPart`] or [`crate::tool_types::ToolSpec`]
36/// to request that the provider cache the prefix ending at this marker.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub struct CacheControl {
39    /// Cache strategy. Currently only `Ephemeral` is supported across all
40    /// integrated providers.
41    pub kind: CacheControlKind,
42    /// Time-to-live for the cache entry.
43    pub ttl: CacheTtl,
44}
45
46impl CacheControl {
47    /// Returns an `Ephemeral` cache control with the default 5-minute TTL.
48    #[must_use]
49    pub const fn ephemeral() -> Self {
50        Self {
51            kind: CacheControlKind::Ephemeral,
52            ttl: CacheTtl::FiveMinutes,
53        }
54    }
55
56    /// Returns an `Ephemeral` cache control with the specified TTL.
57    #[must_use]
58    pub const fn with_ttl(mut self, ttl: CacheTtl) -> Self {
59        self.ttl = ttl;
60        self
61    }
62
63    /// Returns the wire-format TTL string (e.g. `"5m"`, `"1h"`).
64    #[must_use]
65    pub const fn ttl_wire(&self) -> &'static str {
66        match self.ttl {
67            CacheTtl::FiveMinutes => "5m",
68            CacheTtl::OneHour => "1h",
69        }
70    }
71}
72
73impl Default for CacheControl {
74    fn default() -> Self {
75        Self::ephemeral()
76    }
77}
78
79/// Cache strategy kinds.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82#[non_exhaustive]
83pub enum CacheControlKind {
84    /// Short-lived cache that the provider may evict.
85    Ephemeral,
86}
87
88/// Time-to-live for a cache entry.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91#[non_exhaustive]
92pub enum CacheTtl {
93    /// 5-minute TTL. Cheapest cache writes (1.25× base input price).
94    #[default]
95    FiveMinutes,
96    /// 1-hour TTL. More expensive cache writes (2× base input price) but
97    /// better hit rate for long-lived sessions.
98    OneHour,
99}
100
101#[cfg(test)]
102#[allow(clippy::unwrap_used)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn cache_control_default_is_ephemeral_five_minutes() {
108        let ctrl = CacheControl::default();
109        assert_eq!(ctrl.kind, CacheControlKind::Ephemeral);
110        assert_eq!(ctrl.ttl, CacheTtl::FiveMinutes);
111    }
112
113    #[test]
114    fn cache_control_with_ttl_overrides() {
115        let ctrl = CacheControl::ephemeral().with_ttl(CacheTtl::OneHour);
116        assert_eq!(ctrl.ttl, CacheTtl::OneHour);
117    }
118
119    #[test]
120    fn cache_control_ttl_wire_strings() {
121        assert_eq!(CacheControl::ephemeral().ttl_wire(), "5m");
122        assert_eq!(
123            CacheControl::ephemeral()
124                .with_ttl(CacheTtl::OneHour)
125                .ttl_wire(),
126            "1h"
127        );
128    }
129}