use std::collections::HashMap;
use std::time::Duration;
pub struct CachePolicyContext<'a> {
pub model: &'a str,
pub tenant_id: Option<&'a str>,
pub stream: bool,
pub metadata: &'a HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct CacheDecision {
pub use_exact: bool,
pub use_semantic: bool,
pub use_streaming_replay: bool,
pub bypass: bool,
pub ttl_override: Option<Duration>,
pub similarity_threshold: f32,
pub stale_while_revalidate: Option<Duration>,
}
impl Default for CacheDecision {
fn default() -> Self {
Self {
use_exact: true,
use_semantic: false,
use_streaming_replay: false,
bypass: false,
ttl_override: None,
similarity_threshold: 0.95,
stale_while_revalidate: None,
}
}
}
pub trait CachePolicy: Send + Sync + 'static {
fn decide(&self, ctx: &CachePolicyContext<'_>) -> CacheDecision;
}
#[derive(Debug, Clone)]
pub struct StandardCachePolicy {
pub exact_ttl: Duration,
pub semantic_ttl: Option<Duration>,
pub similarity_threshold: f32,
pub bypass_on_no_store: bool,
}
impl Default for StandardCachePolicy {
fn default() -> Self {
Self {
exact_ttl: Duration::from_secs(300),
semantic_ttl: None,
similarity_threshold: 0.95,
bypass_on_no_store: true,
}
}
}
impl CachePolicy for StandardCachePolicy {
fn decide(&self, ctx: &CachePolicyContext<'_>) -> CacheDecision {
let bypass = self.bypass_on_no_store
&& ctx
.metadata
.get("cache")
.is_some_and(|v| v.eq_ignore_ascii_case("no-store"));
CacheDecision {
use_exact: true,
use_semantic: self.semantic_ttl.is_some(),
use_streaming_replay: ctx.stream,
bypass,
ttl_override: if bypass { None } else { Some(self.exact_ttl) },
similarity_threshold: self.similarity_threshold,
stale_while_revalidate: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ctx<'a>(model: &'a str, stream: bool, metadata: &'a HashMap<String, String>) -> CachePolicyContext<'a> {
CachePolicyContext {
model,
tenant_id: None,
stream,
metadata,
}
}
#[test]
fn standard_policy_exact_tier_enabled_by_default() {
let policy = StandardCachePolicy::default();
let meta = HashMap::new();
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!(decision.use_exact);
}
#[test]
fn standard_policy_semantic_tier_disabled_when_no_semantic_ttl() {
let policy = StandardCachePolicy::default();
let meta = HashMap::new();
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!(
!decision.use_semantic,
"semantic tier should be off when semantic_ttl is None"
);
}
#[test]
fn standard_policy_semantic_tier_enabled_when_semantic_ttl_set() {
let policy = StandardCachePolicy {
semantic_ttl: Some(Duration::from_secs(120)),
..Default::default()
};
let meta = HashMap::new();
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!(decision.use_semantic);
}
#[test]
fn standard_policy_bypass_on_no_store_header() {
let policy = StandardCachePolicy::default();
let mut meta = HashMap::new();
meta.insert("cache".into(), "no-store".into());
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!(decision.bypass, "should bypass when cache=no-store is present");
assert!(
decision.ttl_override.is_none(),
"TTL override should be cleared when bypassing"
);
}
#[test]
fn standard_policy_bypass_on_no_store_case_insensitive() {
let policy = StandardCachePolicy::default();
let mut meta = HashMap::new();
meta.insert("cache".into(), "No-Store".into());
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!(decision.bypass, "bypass should be case-insensitive");
}
#[test]
fn standard_policy_no_bypass_when_bypass_on_no_store_is_false() {
let policy = StandardCachePolicy {
bypass_on_no_store: false,
..Default::default()
};
let mut meta = HashMap::new();
meta.insert("cache".into(), "no-store".into());
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!(!decision.bypass, "should not bypass when bypass_on_no_store=false");
}
#[test]
fn standard_policy_ttl_override_populated_when_not_bypassing() {
let policy = StandardCachePolicy {
exact_ttl: Duration::from_secs(42),
..Default::default()
};
let meta = HashMap::new();
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert_eq!(decision.ttl_override, Some(Duration::from_secs(42)));
}
#[test]
fn standard_policy_similarity_threshold_forwarded() {
let policy = StandardCachePolicy {
similarity_threshold: 0.88,
..Default::default()
};
let meta = HashMap::new();
let decision = policy.decide(&ctx("gpt-4", false, &meta));
assert!((decision.similarity_threshold - 0.88).abs() < f32::EPSILON);
}
#[test]
fn standard_policy_streaming_replay_when_stream_is_true() {
let policy = StandardCachePolicy::default();
let meta = HashMap::new();
let decision = policy.decide(&ctx("gpt-4", true, &meta));
assert!(decision.use_streaming_replay);
}
}