cookie_agent_models/authoring/
cache.rs1use 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}