use crate::model::{AnthropicMessagesCompat, Model, StreamingProtocolCompat};
use crate::provider::CacheRetention;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CacheControlEphemeral {
#[serde(rename = "type")]
pub kind: EphemeralType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ttl: Option<EphemeralTtl>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EphemeralType;
impl Serialize for EphemeralType {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("ephemeral")
}
}
impl<'de> Deserialize<'de> for EphemeralType {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
if s != "ephemeral" {
return Err(serde::de::Error::custom(format!(
"expected \"ephemeral\", got {s:?}"
)));
}
Ok(Self)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EphemeralTtl;
impl Serialize for EphemeralTtl {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str("1h")
}
}
impl<'de> Deserialize<'de> for EphemeralTtl {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
if s != "1h" {
return Err(serde::de::Error::custom(format!(
"expected \"1h\", got {s:?}"
)));
}
Ok(Self)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CacheControlOption {
pub retention: CacheRetention,
pub cache_control: Option<CacheControlEphemeral>,
}
impl CacheControlOption {
pub fn has_block(&self) -> bool {
self.cache_control.is_some()
}
}
pub fn resolve_cache_retention(cache_retention: CacheRetention) -> CacheRetention {
cache_retention
}
pub fn get_cache_control(model: &Model, cache_retention: CacheRetention) -> CacheControlOption {
let retention = resolve_cache_retention(cache_retention);
if matches!(retention, CacheRetention::None) {
return CacheControlOption {
retention,
cache_control: None,
};
}
let supports_long = model
.compat
.as_ref()
.and_then(|c| match c {
StreamingProtocolCompat::AnthropicMessages(a) => Some(a),
_ => None,
})
.map(|c: &AnthropicMessagesCompat| c.long_cache_retention())
.unwrap_or(true);
let ttl = if matches!(retention, CacheRetention::Long) && supports_long {
Some(EphemeralTtl)
} else {
None
};
CacheControlOption {
retention,
cache_control: Some(CacheControlEphemeral {
kind: EphemeralType,
ttl,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{AnthropicMessagesCompat, StreamingProtocolCompat};
use crate::provider::CacheRetention;
use crate::types::{Api, InputModality, ModelCost};
fn model_with_compat(compat: AnthropicMessagesCompat) -> Model {
let mut m = Model::new(
"claude-opus-4-7",
"Claude Opus 4.7",
Api::AnthropicMessages,
"anthropic",
"https://api.anthropic.com",
);
m.input = vec![InputModality::Text, InputModality::Image];
m.context_window = 200_000;
m.max_tokens = 32_000;
m.cost = ModelCost::default();
m.compat = Some(StreamingProtocolCompat::AnthropicMessages(compat));
m
}
#[test]
fn none_retention_produces_no_block() {
let model = model_with_compat(AnthropicMessagesCompat::default());
let opt = get_cache_control(&model, CacheRetention::None);
assert_eq!(opt.retention, CacheRetention::None);
assert!(opt.cache_control.is_none());
assert!(!opt.has_block());
}
#[test]
fn short_retention_produces_plain_ephemeral() {
let model = model_with_compat(AnthropicMessagesCompat::default());
let opt = get_cache_control(&model, CacheRetention::Short);
let cc = opt.cache_control.expect("short produces a block");
assert_eq!(opt.retention, CacheRetention::Short);
assert!(cc.ttl.is_none());
let v = serde_json::to_value(&cc).unwrap();
assert_eq!(v["type"], serde_json::json!("ephemeral"));
assert!(v.get("ttl").is_none());
}
#[test]
fn long_retention_with_support_attaches_1h_ttl() {
let mut compat = AnthropicMessagesCompat::default();
compat.supports_long_cache_retention = Some(true);
let model = model_with_compat(compat);
let opt = get_cache_control(&model, CacheRetention::Long);
let cc = opt.cache_control.expect("long produces a block");
assert!(cc.ttl.is_some());
let v = serde_json::to_value(&cc).unwrap();
assert_eq!(v["type"], serde_json::json!("ephemeral"));
assert_eq!(v["ttl"], serde_json::json!("1h"));
}
#[test]
fn long_retention_without_support_falls_back_to_plain_ephemeral() {
let mut compat = AnthropicMessagesCompat::default();
compat.supports_long_cache_retention = Some(false);
let model = model_with_compat(compat);
let opt = get_cache_control(&model, CacheRetention::Long);
let cc = opt.cache_control.expect("block still emitted");
assert!(cc.ttl.is_none());
assert_eq!(opt.retention, CacheRetention::Long);
}
#[test]
fn no_compat_configured_treats_long_as_supported() {
let mut m = Model::new(
"claude-opus-4-7",
"Claude Opus 4.7",
Api::AnthropicMessages,
"anthropic",
"https://api.anthropic.com",
);
m.compat = None;
let opt = get_cache_control(&m, CacheRetention::Long);
assert!(opt.cache_control.unwrap().ttl.is_some());
}
}