use crate::llm::api::LlmRequestPayload;
use crate::llm::capabilities::{CacheBreakpointStyle, Capabilities};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub(crate) enum PromptCacheBreakpoint {
Placed { style: BreakpointPlacement },
NotRequested,
Unsupported,
Automatic,
DeferredToExistingMarker,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum BreakpointPlacement {
TopLevel,
LastBlock,
}
impl PromptCacheBreakpoint {
pub(crate) fn for_request(request: &LlmRequestPayload) -> Self {
let caps = crate::llm::managed_supply::capabilities_for(&request.provider, &request.model);
Self::resolve(request, &caps)
}
pub(crate) fn resolve(request: &LlmRequestPayload, caps: &Capabilities) -> Self {
if !request.cache {
return Self::NotRequested;
}
if !caps.prompt_caching {
return Self::Unsupported;
}
let style = match caps.cache_breakpoint_style {
CacheBreakpointStyle::None => return Self::Automatic,
CacheBreakpointStyle::TopLevel => BreakpointPlacement::TopLevel,
CacheBreakpointStyle::LastBlock => BreakpointPlacement::LastBlock,
};
if request_carries_cache_marker(request) {
return Self::DeferredToExistingMarker;
}
Self::Placed { style }
}
}
pub(crate) fn apply_prompt_cache_breakpoint(
body: &mut serde_json::Value,
breakpoint: PromptCacheBreakpoint,
marker: serde_json::Value,
) {
match breakpoint {
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::TopLevel,
} => body["cache_control"] = marker,
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::LastBlock,
} => {
insert_last_message_cache_control(body, &marker);
}
PromptCacheBreakpoint::NotRequested
| PromptCacheBreakpoint::Unsupported
| PromptCacheBreakpoint::Automatic
| PromptCacheBreakpoint::DeferredToExistingMarker => {}
}
}
fn request_carries_cache_marker(request: &LlmRequestPayload) -> bool {
request.messages.iter().any(message_carries_cache_marker)
|| request
.native_tools
.iter()
.flatten()
.any(|tool| tool.get("cache_control").is_some())
}
fn message_carries_cache_marker(message: &serde_json::Value) -> bool {
match message.get("content") {
Some(serde_json::Value::Array(blocks)) => blocks.iter().any(block_carries_cache_marker),
Some(block @ serde_json::Value::Object(_)) => block_carries_cache_marker(block),
_ => false,
}
}
fn block_carries_cache_marker(block: &serde_json::Value) -> bool {
(block.get("cache_control").is_some() && !is_whitespace_text_block(block))
|| block
.get("content")
.and_then(serde_json::Value::as_array)
.is_some_and(|nested| nested.iter().any(block_carries_cache_marker))
}
fn is_whitespace_text_block(block: &serde_json::Value) -> bool {
block.get("type").and_then(serde_json::Value::as_str) == Some("text")
&& block
.get("text")
.and_then(serde_json::Value::as_str)
.is_some_and(|text| text.trim().is_empty())
}
fn insert_last_message_cache_control(
body: &mut serde_json::Value,
marker: &serde_json::Value,
) -> bool {
let Some(messages) = body
.get_mut("messages")
.and_then(serde_json::Value::as_array_mut)
else {
return false;
};
messages
.iter_mut()
.rev()
.any(|message| insert_message_cache_control(message, marker))
}
fn insert_message_cache_control(
message: &mut serde_json::Value,
marker: &serde_json::Value,
) -> bool {
let Some(content) = message
.as_object_mut()
.and_then(|object| object.get_mut("content"))
else {
return false;
};
match content {
serde_json::Value::String(text) => {
if text.is_empty() {
return false;
}
let text = text.clone();
*content = serde_json::json!([{
"type": "text",
"text": text,
"cache_control": marker,
}]);
true
}
serde_json::Value::Array(blocks) => blocks.iter_mut().rev().any(|block| {
let Some(object) = block.as_object_mut() else {
return false;
};
object
.entry("cache_control".to_string())
.or_insert_with(|| marker.clone());
true
}),
serde_json::Value::Object(object) => {
object
.entry("cache_control".to_string())
.or_insert_with(|| marker.clone());
true
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn caps(style: CacheBreakpointStyle) -> Capabilities {
Capabilities {
prompt_caching: true,
cache_breakpoint_style: style,
..Capabilities::default()
}
}
fn request(messages: Vec<serde_json::Value>) -> LlmRequestPayload {
let mut opts = crate::llm::api::options::base_opts("anthropic");
opts.messages = messages;
opts.native_tools = None;
opts.cache = true;
LlmRequestPayload::from(&opts)
}
fn ordinary() -> LlmRequestPayload {
request(vec![
serde_json::json!({"role": "user", "content": "hello"}),
])
}
fn body() -> serde_json::Value {
serde_json::json!({
"model": "m",
"messages": [{"role": "user", "content": "hello"}],
})
}
fn marker() -> serde_json::Value {
serde_json::json!({"type": "ephemeral"})
}
fn body_contains_cache_control(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Object(object) => {
object.contains_key("cache_control")
|| object.values().any(body_contains_cache_control)
}
serde_json::Value::Array(values) => values.iter().any(body_contains_cache_control),
_ => false,
}
}
fn count_cache_control(value: &serde_json::Value) -> usize {
match value {
serde_json::Value::Object(object) => {
usize::from(object.contains_key("cache_control"))
+ object.values().map(count_cache_control).sum::<usize>()
}
serde_json::Value::Array(values) => values.iter().map(count_cache_control).sum(),
_ => 0,
}
}
fn resolve_and_apply(
style: CacheBreakpointStyle,
) -> (PromptCacheBreakpoint, serde_json::Value) {
let breakpoint = PromptCacheBreakpoint::resolve(&ordinary(), &caps(style));
let mut value = body();
apply_prompt_cache_breakpoint(&mut value, breakpoint, marker());
(breakpoint, value)
}
#[test]
fn none_style_emits_no_marker() {
let (breakpoint, value) = resolve_and_apply(CacheBreakpointStyle::None);
assert_eq!(breakpoint, PromptCacheBreakpoint::Automatic);
assert_eq!(value, body(), "automatic-cache routes take no marker");
}
#[test]
fn top_level_style_still_emits_the_marker() {
let (breakpoint, value) = resolve_and_apply(CacheBreakpointStyle::TopLevel);
assert_eq!(
breakpoint,
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::TopLevel
}
);
assert_eq!(value["cache_control"], marker());
}
#[test]
fn last_block_style_still_marks_the_final_message() {
let (breakpoint, value) = resolve_and_apply(CacheBreakpointStyle::LastBlock);
assert_eq!(
breakpoint,
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::LastBlock
}
);
assert_eq!(
value["messages"][0]["content"][0]["cache_control"],
marker()
);
assert!(value.get("cache_control").is_none());
}
#[test]
fn caller_marker_defers_and_ordinary_request_places() {
let caps = caps(CacheBreakpointStyle::LastBlock);
let marked = request(vec![
serde_json::json!({"role": "user", "content": [
{"type": "text", "text": "stable prefix", "cache_control": marker()},
]}),
serde_json::json!({"role": "assistant", "content": "ok"}),
serde_json::json!({"role": "user", "content": "hello"}),
]);
assert_eq!(
PromptCacheBreakpoint::resolve(&marked, &caps),
PromptCacheBreakpoint::DeferredToExistingMarker
);
assert_eq!(
PromptCacheBreakpoint::resolve(&ordinary(), &caps),
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::LastBlock
}
);
}
#[test]
fn caller_markers_in_every_provider_position_defer() {
let caps = caps(CacheBreakpointStyle::LastBlock);
let tool_result_block = request(vec![serde_json::json!({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": "t1", "content": [
{"type": "text", "text": "out", "cache_control": marker()},
]}],
})]);
let mut tool_definition = ordinary();
tool_definition.native_tools = Some(vec![serde_json::json!({
"name": "lookup",
"input_schema": {"type": "object"},
"cache_control": marker(),
})]);
for (label, request) in [
("tool_result block", tool_result_block),
("tool definition", tool_definition),
] {
assert_eq!(
PromptCacheBreakpoint::resolve(&request, &caps),
PromptCacheBreakpoint::DeferredToExistingMarker,
"{label}"
);
}
}
#[test]
fn cache_control_named_user_data_does_not_defer() {
let mut request = request(vec![
serde_json::json!({"role": "assistant", "content": [
{"type": "tool_use", "id": "t1", "name": "set", "input": {"cache_control": "on"}},
]}),
serde_json::json!({"role": "user", "content": "hello"}),
]);
request.native_tools = Some(vec![serde_json::json!({
"name": "set",
"input_schema": {
"type": "object",
"properties": {"cache_control": {"type": "string"}},
},
})]);
assert_eq!(
PromptCacheBreakpoint::resolve(&request, &caps(CacheBreakpointStyle::LastBlock)),
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::LastBlock
}
);
}
fn marker_corpus() -> Vec<(
&'static str,
Vec<serde_json::Value>,
Option<serde_json::Value>,
bool,
)> {
let use_and_result = |result: serde_json::Value| {
vec![
serde_json::json!({"role": "user", "content": "run it"}),
serde_json::json!({"role": "assistant", "content": [
{"type": "tool_use", "id": "toolu_1", "name": "run", "input": {}},
]}),
serde_json::json!({"role": "user", "content": [result]}),
]
};
vec![
(
"text block",
vec![serde_json::json!({"role": "user", "content": [
{"type": "text", "text": "stable prefix", "cache_control": marker()},
{"type": "text", "text": "hello"},
]})],
None,
true,
),
(
"tool_result block",
use_and_result(serde_json::json!({
"type": "tool_result", "tool_use_id": "toolu_1", "content": "out",
"cache_control": marker(),
})),
None,
true,
),
(
"block nested in a tool_result",
use_and_result(serde_json::json!({
"type": "tool_result", "tool_use_id": "toolu_1", "content": [
{"type": "text", "text": "out", "cache_control": marker()},
],
})),
None,
true,
),
(
"tool definition",
vec![serde_json::json!({"role": "user", "content": "hello"})],
Some(serde_json::json!({
"type": "function",
"function": {"name": "lookup", "parameters": {"type": "object"}},
"cache_control": marker(),
})),
true,
),
(
"message level",
vec![serde_json::json!({
"role": "user", "content": "hello", "cache_control": marker(),
})],
None,
false,
),
(
"whitespace-only text block",
vec![serde_json::json!({"role": "user", "content": [
{"type": "text", "text": " ", "cache_control": marker()},
{"type": "text", "text": "hello"},
]})],
None,
false,
),
]
}
fn corpus_request(
provider: &str,
model: &str,
messages: Vec<serde_json::Value>,
tool: Option<serde_json::Value>,
cache: bool,
) -> LlmRequestPayload {
let mut opts = crate::llm::api::options::base_opts(provider);
opts.model = model.to_string();
opts.messages = messages;
opts.native_tools = tool.map(|tool| vec![tool]);
opts.cache = cache;
LlmRequestPayload::from(&opts)
}
#[test]
fn deferral_counts_only_markers_in_provider_read_positions() {
let caps = caps(CacheBreakpointStyle::LastBlock);
for (label, messages, tool, counted) in marker_corpus() {
let request = corpus_request(
"anthropic",
"claude-opus-4-5-20251101",
messages,
tool,
true,
);
let expected = if counted {
PromptCacheBreakpoint::DeferredToExistingMarker
} else {
PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::LastBlock,
}
};
assert_eq!(
PromptCacheBreakpoint::resolve(&request, &caps),
expected,
"{label}"
);
}
}
#[test]
fn every_counted_marker_survives_both_marker_placing_adapters() {
for (label, messages, tool, counted) in marker_corpus() {
if !counted {
continue;
}
let anthropic = crate::llm::providers::anthropic::AnthropicProvider::build_request_body(
&corpus_request(
"anthropic",
"claude-opus-4-5-20251101",
messages.clone(),
tool.clone(),
false,
),
);
let openai_compat =
crate::llm::providers::openai_compat::OpenAiCompatibleProvider::build_request_body(
&corpus_request(
"openrouter",
"anthropic/claude-fable-5-1",
messages,
tool,
false,
),
);
for (adapter, built) in [("anthropic", anthropic), ("openai_compat", openai_compat)] {
assert!(
body_contains_cache_control(&built),
"{label}: the {adapter} adapter dropped a marker the deferral rule counts\n{built:#}"
);
}
}
}
#[test]
fn cache_off_is_not_requested() {
let mut request = ordinary();
request.cache = false;
assert_eq!(
PromptCacheBreakpoint::resolve(&request, &caps(CacheBreakpointStyle::LastBlock)),
PromptCacheBreakpoint::NotRequested
);
}
#[test]
fn prompt_caching_off_suppresses_every_style() {
for style in [
CacheBreakpointStyle::TopLevel,
CacheBreakpointStyle::LastBlock,
CacheBreakpointStyle::None,
] {
let off = Capabilities {
prompt_caching: false,
cache_breakpoint_style: style,
..Capabilities::default()
};
let breakpoint = PromptCacheBreakpoint::resolve(&ordinary(), &off);
assert_eq!(breakpoint, PromptCacheBreakpoint::Unsupported, "{style:?}");
let mut value = body();
apply_prompt_cache_breakpoint(&mut value, breakpoint, marker());
assert_eq!(
value,
body(),
"{style:?} must stay inert while caching is off"
);
}
}
#[test]
fn outcome_serializes_as_tagged_record() {
assert_eq!(
serde_json::to_value(PromptCacheBreakpoint::Placed {
style: BreakpointPlacement::LastBlock
})
.unwrap(),
serde_json::json!({"outcome": "placed", "style": "last_block"})
);
assert_eq!(
serde_json::to_value(PromptCacheBreakpoint::DeferredToExistingMarker).unwrap(),
serde_json::json!({"outcome": "deferred_to_existing_marker"})
);
}
#[test]
fn openai_route_with_cache_on_sends_no_cache_control() {
for model in ["gpt-6-astra", "gpt-5.6-luna", "gpt-4o-mini"] {
let mut opts = crate::llm::api::options::base_opts("openai");
opts.model = model.to_string();
opts.cache = true;
let payload = LlmRequestPayload::from(&opts);
assert_eq!(
PromptCacheBreakpoint::for_request(&payload),
PromptCacheBreakpoint::Automatic,
"{model}"
);
let built =
crate::llm::providers::openai_compat::OpenAiCompatibleProvider::build_request_body(
&payload,
);
assert!(
!body_contains_cache_control(&built),
"{model} must not carry cache_control anywhere in the request"
);
}
}
#[test]
fn anthropic_route_with_cache_on_does_send_cache_control() {
let mut opts = crate::llm::api::options::base_opts("anthropic");
opts.model = "claude-opus-4-5-20251101".to_string();
opts.cache = true;
let payload = LlmRequestPayload::from(&opts);
let built =
crate::llm::providers::anthropic::AnthropicProvider::build_request_body(&payload);
assert!(
body_contains_cache_control(&built),
"an Anthropic route with caching on must still carry a cache_control marker"
);
}
#[test]
fn anthropic_builder_obeys_a_deferral() {
let mut opts = crate::llm::api::options::base_opts("anthropic");
opts.model = "claude-opus-4-5-20251101".to_string();
opts.cache = true;
opts.native_tools = None;
opts.messages = vec![
serde_json::json!({"role": "user", "content": [
{"type": "text", "text": "stable prefix", "cache_control": marker()},
]}),
serde_json::json!({"role": "assistant", "content": "ok"}),
serde_json::json!({"role": "user", "content": "hello"}),
];
let payload = LlmRequestPayload::from(&opts);
assert_eq!(
PromptCacheBreakpoint::for_request(&payload),
PromptCacheBreakpoint::DeferredToExistingMarker
);
let built =
crate::llm::providers::anthropic::AnthropicProvider::build_request_body(&payload);
assert_eq!(count_cache_control(&built), 1, "{built:#}");
assert!(!body_contains_cache_control(
built["messages"].as_array().unwrap().last().unwrap()
));
}
}