use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
pub const KEY_PREFIX: &str = "anthropic-cache";
pub enum System<'a> {
Text(&'a str),
Blocks(&'a Value),
None,
}
pub fn find_breakpoints(blocks: &Value) -> Vec<usize> {
let Some(arr) = blocks.as_array() else {
return Vec::new();
};
arr.iter()
.enumerate()
.filter_map(|(i, b)| match b.as_object() {
Some(o) => match o.get("cache_control") {
Some(v) if !v.is_null() => Some(i),
_ => None,
},
None => None,
})
.collect()
}
pub fn scope_blocks(blocks: &Value) -> Vec<Value> {
let Some(arr) = blocks.as_array() else {
return Vec::new();
};
if arr.is_empty() {
return Vec::new();
}
let bps = find_breakpoints(blocks);
let end = match bps.last() {
Some(&last) => last + 1,
None => arr.len(),
};
arr[..end].to_vec()
}
pub fn compute_cache_key(model: &str, system: System<'_>, tools: Option<&Value>) -> String {
let scoped = scope_blocks(&normalize_system(system));
let tools_vec: Vec<Value> = match tools.and_then(|v| v.as_array()) {
Some(arr) => arr.to_vec(),
None => Vec::new(),
};
let mut body = Map::new();
body.insert("model".to_string(), Value::String(model.to_string()));
body.insert("system".to_string(), Value::Array(scoped));
body.insert("tools".to_string(), Value::Array(tools_vec));
let body = Value::Object(body);
let blob = canonical_json(&body);
let mut hasher = Sha256::new();
hasher.update(blob.as_bytes());
let digest = hex_lower(&hasher.finalize());
format!("{KEY_PREFIX}:{model}:sha256:{digest}")
}
pub fn canonical_json(value: &Value) -> String {
let mut out = String::new();
write_canonical(value, &mut out);
out
}
fn normalize_system(system: System<'_>) -> Value {
match system {
System::None => Value::Array(Vec::new()),
System::Text(s) => {
let mut block = Map::new();
block.insert("type".to_string(), Value::String("text".to_string()));
block.insert("text".to_string(), Value::String(s.to_string()));
Value::Array(vec![Value::Object(block)])
}
System::Blocks(v) => match v.as_array() {
Some(arr) => Value::Array(arr.clone()),
None => Value::Array(Vec::new()),
},
}
}
fn write_canonical(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => out.push_str(&n.to_string()),
Value::String(s) => write_json_string(s, out),
Value::Array(arr) => {
out.push('[');
for (i, item) in arr.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_json_string(k, out);
out.push(':');
write_canonical(&map[*k], out);
}
out.push('}');
}
}
}
fn write_json_string(s: &str, out: &mut String) {
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let code = c as u32;
out.push_str(&format!("\\u{code:04x}"));
}
c => out.push(c),
}
}
out.push('"');
}
fn hex_lower(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(bytes.len() * 2);
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}