Skip to main content

cookie_agent_models/authoring/
cache.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
4pub enum CacheTtl {
5    #[serde(rename = "1h")]
6    OneHour,
7    #[serde(rename = "5m")]
8    FiveMinutes,
9    #[serde(rename = "off")]
10    Off,
11}
12
13#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
14pub enum RollingCacheTtl {
15    #[serde(rename = "5m")]
16    FiveMinutes,
17    #[serde(rename = "off")]
18    Off,
19}
20
21#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
22#[serde(deny_unknown_fields)]
23pub struct AnthropicCacheConfig {
24    pub system: CacheTtl,
25    pub tools: CacheTtl,
26    pub rolling: RollingCacheTtl,
27    #[serde(skip)]
28    explicitly_requests_one_hour: bool,
29}
30
31impl Default for AnthropicCacheConfig {
32    fn default() -> Self {
33        Self {
34            system: CacheTtl::OneHour,
35            tools: CacheTtl::OneHour,
36            rolling: RollingCacheTtl::FiveMinutes,
37            explicitly_requests_one_hour: false,
38        }
39    }
40}
41
42impl<'de> Deserialize<'de> for AnthropicCacheConfig {
43    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
44    where
45        D: serde::Deserializer<'de>,
46    {
47        #[derive(Deserialize)]
48        #[serde(deny_unknown_fields)]
49        struct Raw {
50            system: Option<CacheTtl>,
51            tools: Option<CacheTtl>,
52            rolling: Option<RollingCacheTtl>,
53        }
54
55        let raw = Raw::deserialize(deserializer)?;
56        Ok(Self {
57            explicitly_requests_one_hour: raw.system == Some(CacheTtl::OneHour)
58                || raw.tools == Some(CacheTtl::OneHour),
59            system: raw.system.unwrap_or(CacheTtl::OneHour),
60            tools: raw.tools.unwrap_or(CacheTtl::OneHour),
61            rolling: raw.rolling.unwrap_or(RollingCacheTtl::FiveMinutes),
62        })
63    }
64}
65
66impl AnthropicCacheConfig {
67    pub fn validate(&self) -> Result<(), &'static str> {
68        validate_ttl_order([
69            ttl(self.tools),
70            ttl(self.system),
71            match self.rolling {
72                RollingCacheTtl::FiveMinutes => Some(CacheTtl::FiveMinutes),
73                RollingCacheTtl::Off => None,
74            },
75        ])
76    }
77
78    #[must_use]
79    pub const fn explicitly_requests_one_hour(&self) -> bool {
80        self.explicitly_requests_one_hour
81    }
82}
83
84#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
85#[serde(deny_unknown_fields)]
86pub struct BedrockCacheConfig {
87    #[serde(default = "five_minutes")]
88    pub system: CacheTtl,
89    #[serde(default = "five_minutes")]
90    pub tools: CacheTtl,
91    #[serde(default = "rolling_five_minutes")]
92    pub rolling: RollingCacheTtl,
93}
94
95impl Default for BedrockCacheConfig {
96    fn default() -> Self {
97        Self {
98            system: five_minutes(),
99            tools: five_minutes(),
100            rolling: rolling_five_minutes(),
101        }
102    }
103}
104
105impl BedrockCacheConfig {
106    pub fn validate(&self) -> Result<(), &'static str> {
107        validate_ttl_order([
108            ttl(self.tools),
109            ttl(self.system),
110            match self.rolling {
111                RollingCacheTtl::FiveMinutes => Some(CacheTtl::FiveMinutes),
112                RollingCacheTtl::Off => None,
113            },
114        ])
115    }
116}
117
118#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
119#[serde(rename_all = "snake_case")]
120pub enum OpenAiPromptCacheRetention {
121    InMemory,
122    #[serde(rename = "24h")]
123    TwentyFourHours,
124}
125
126#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
127#[serde(rename_all = "snake_case")]
128pub enum OpenAiCacheMode {
129    #[default]
130    #[serde(rename = "auto")]
131    Auto,
132    #[serde(rename = "explicit")]
133    Explicit,
134}
135
136#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
137pub enum OpenAiPromptCacheTtl {
138    #[default]
139    #[serde(rename = "30m")]
140    ThirtyMinutes,
141}
142
143#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
144#[serde(deny_unknown_fields)]
145pub struct OpenAiCacheConfig {
146    pub prompt_cache_retention: Option<OpenAiPromptCacheRetention>,
147    pub mode: Option<OpenAiCacheMode>,
148    pub ttl: Option<OpenAiPromptCacheTtl>,
149    #[serde(default)]
150    pub system: bool,
151    #[serde(default)]
152    pub rolling: bool,
153}
154
155impl OpenAiCacheConfig {
156    #[must_use]
157    pub const fn gpt_5_6_controls_enabled(&self) -> bool {
158        self.mode.is_some() || self.ttl.is_some() || self.system || self.rolling
159    }
160
161    #[must_use]
162    pub fn effective_mode(&self) -> OpenAiCacheMode {
163        self.mode.unwrap_or_default()
164    }
165
166    #[must_use]
167    pub fn effective_ttl(&self) -> OpenAiPromptCacheTtl {
168        self.ttl.unwrap_or_default()
169    }
170}
171
172#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
173#[serde(deny_unknown_fields)]
174pub struct OpenAiCompatibleCacheConfig {
175    pub prompt_cache_key: Option<String>,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
179#[serde(transparent)]
180pub struct ProviderCacheConfig(serde_json::Value);
181
182impl<'de> Deserialize<'de> for ProviderCacheConfig {
183    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
184    where
185        D: serde::Deserializer<'de>,
186    {
187        #[derive(Deserialize)]
188        #[serde(deny_unknown_fields)]
189        struct KnownFields {
190            system: Option<serde_json::Value>,
191            tools: Option<serde_json::Value>,
192            rolling: Option<serde_json::Value>,
193            prompt_cache_retention: Option<serde_json::Value>,
194            mode: Option<serde_json::Value>,
195            ttl: Option<serde_json::Value>,
196            prompt_cache_key: Option<serde_json::Value>,
197        }
198
199        let value = serde_json::Value::deserialize(deserializer)?;
200        let fields = KnownFields::deserialize(value.clone()).map_err(serde::de::Error::custom)?;
201        let _ = (
202            fields.system,
203            fields.tools,
204            fields.rolling,
205            fields.prompt_cache_retention,
206            fields.mode,
207            fields.ttl,
208            fields.prompt_cache_key,
209        );
210        Ok(Self(value))
211    }
212}
213
214impl ProviderCacheConfig {
215    pub fn validate_supported_shape(&self) -> Result<(), String> {
216        let object = self
217            .0
218            .as_object()
219            .expect("provider cache deserialization requires an object");
220        if object.is_empty() {
221            return Ok(());
222        }
223        if object.contains_key("prompt_cache_key") {
224            return self.openai_compatible().map(|_| ());
225        }
226        if object.contains_key("mode")
227            || object.contains_key("ttl")
228            || object.contains_key("prompt_cache_retention")
229            || object
230                .get("system")
231                .is_some_and(serde_json::Value::is_boolean)
232            || object
233                .get("rolling")
234                .is_some_and(serde_json::Value::is_boolean)
235        {
236            return self.openai().map(|_| ());
237        }
238        self.anthropic().map(|_| ())
239    }
240
241    pub fn anthropic(&self) -> Result<AnthropicCacheConfig, String> {
242        self.validate_field::<CacheTtl>("system")?;
243        self.validate_field::<CacheTtl>("tools")?;
244        self.validate_field::<RollingCacheTtl>("rolling")?;
245        decode_and_validate(&self.0, AnthropicCacheConfig::validate)
246    }
247
248    pub fn bedrock(&self) -> Result<BedrockCacheConfig, String> {
249        self.validate_field::<CacheTtl>("system")?;
250        self.validate_field::<CacheTtl>("tools")?;
251        self.validate_field::<RollingCacheTtl>("rolling")?;
252        decode_and_validate(&self.0, BedrockCacheConfig::validate)
253    }
254
255    pub fn openai(&self) -> Result<OpenAiCacheConfig, String> {
256        self.validate_field::<OpenAiPromptCacheRetention>("prompt_cache_retention")?;
257        self.validate_field::<OpenAiCacheMode>("mode")?;
258        self.validate_field::<OpenAiPromptCacheTtl>("ttl")?;
259        self.validate_field::<bool>("system")?;
260        self.validate_field::<bool>("rolling")?;
261        serde_json::from_value(self.0.clone()).map_err(|error| error.to_string())
262    }
263
264    pub fn openai_compatible(&self) -> Result<OpenAiCompatibleCacheConfig, String> {
265        self.validate_field::<String>("prompt_cache_key")?;
266        let config: OpenAiCompatibleCacheConfig =
267            serde_json::from_value(self.0.clone()).map_err(|error| error.to_string())?;
268        if let Some(key) = &config.prompt_cache_key {
269            let expanded = key.replace("${session_id}", "00000000-0000-0000-0000-000000000000");
270            if expanded.contains("${") || expanded.chars().count() > 64 {
271                return Err(
272                    "prompt_cache_key supports only ${session_id} and must expand to at most 64 characters"
273                        .into(),
274                );
275            }
276        }
277        Ok(config)
278    }
279
280    fn validate_field<T>(&self, field: &str) -> Result<(), String>
281    where
282        T: serde::de::DeserializeOwned,
283    {
284        let Some(value) = self.0.get(field) else {
285            return Ok(());
286        };
287        serde_json::from_value::<T>(value.clone())
288            .map(|_| ())
289            .map_err(|error| format!("field `{field}`: {error}"))
290    }
291}
292
293fn decode_and_validate<T>(
294    value: &serde_json::Value,
295    validate: impl FnOnce(&T) -> Result<(), &'static str>,
296) -> Result<T, String>
297where
298    T: serde::de::DeserializeOwned,
299{
300    let config: T = serde_json::from_value(value.clone()).map_err(|error| error.to_string())?;
301    validate(&config).map_err(str::to_owned)?;
302    Ok(config)
303}
304
305const fn ttl(value: CacheTtl) -> Option<CacheTtl> {
306    match value {
307        CacheTtl::OneHour | CacheTtl::FiveMinutes => Some(value),
308        CacheTtl::Off => None,
309    }
310}
311
312fn validate_ttl_order(
313    values: impl IntoIterator<Item = Option<CacheTtl>>,
314) -> Result<(), &'static str> {
315    let mut saw_five_minutes = false;
316    for value in values.into_iter().flatten() {
317        match value {
318            CacheTtl::FiveMinutes => saw_five_minutes = true,
319            CacheTtl::OneHour if saw_five_minutes => {
320                return Err("one-hour cache points must precede five-minute cache points");
321            }
322            CacheTtl::OneHour | CacheTtl::Off => {}
323        }
324    }
325    Ok(())
326}
327
328const fn five_minutes() -> CacheTtl {
329    CacheTtl::FiveMinutes
330}
331
332const fn rolling_five_minutes() -> RollingCacheTtl {
333    RollingCacheTtl::FiveMinutes
334}