use serde_json::{json, Map, Value};
const LOOKBACK: usize = 20;
pub(super) fn apply(body: &mut Map<String, Value>) {
let head = if has_blocks(body, "system") {
"system"
} else {
"tools"
};
if let Some(block) = last_of(body, head) {
mark(block);
}
let msgs = body.get("messages").and_then(Value::as_array);
let targets = conversation_marks(msgs.map_or(&[] as &[_], Vec::as_slice));
for (m, b) in targets {
mark(&mut body["messages"][m]["content"][b]);
}
}
fn conversation_marks(msgs: &[Value]) -> Vec<(usize, usize)> {
let ongoing = msgs
.split_last()
.is_some_and(|(_, before)| before.iter().any(|m| m["role"] == "assistant"));
if !ongoing {
return Vec::new();
}
let Some(last) = msgs.iter().rposition(|m| m["role"] != "assistant") else {
return Vec::new();
};
let eligible: Vec<(usize, usize)> = msgs[..=last]
.iter()
.enumerate()
.flat_map(|(m, msg)| {
msg["content"]
.as_array()
.into_iter()
.flatten()
.enumerate()
.filter(|(_, block)| cacheable(block))
.map(move |(b, _)| (m, b))
})
.collect();
let Some((&rolling, before)) = eligible.split_last() else {
return Vec::new();
};
let mut out = Vec::new();
if before.len() >= LOOKBACK {
out.push(before[before.len() - LOOKBACK]);
}
out.push(rolling);
out
}
fn cacheable(block: &Value) -> bool {
block["type"] != "thinking" && block["type"] != "redacted_thinking"
}
fn has_blocks(body: &Map<String, Value>, key: &str) -> bool {
body.get(key)
.and_then(Value::as_array)
.is_some_and(|a| !a.is_empty())
}
fn last_of<'a>(body: &'a mut Map<String, Value>, key: &str) -> Option<&'a mut Value> {
body.get_mut(key)?.as_array_mut()?.last_mut()
}
fn mark(block: &mut Value) {
block["cache_control"] = json!({"type": "ephemeral"});
}