Skip to content

fix(llm-client): enable Anthropic prompt caching by default - #233

Open
nachiketb-nvidia wants to merge 1 commit into
mainfrom
feat/default-prompt-caching
Open

fix(llm-client): enable Anthropic prompt caching by default#233
nachiketb-nvidia wants to merge 1 commit into
mainfrom
feat/default-prompt-caching

Conversation

@nachiketb-nvidia

@nachiketb-nvidia nachiketb-nvidia commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Enable Anthropic prompt caching by default in TranslatingLlmClient.

Why

Native Anthropic requests require an explicit cache breakpoint to use provider prompt caching.

How

For Backend::Anthropic, the client marks the final message content block with {"cache_control": {"type": "ephemeral"}} after request translation. Other backend formats are unchanged.

What to review

  • caching is limited to Anthropic backends
  • existing explicit cache control on the final block is preserved
  • protocol and translation types remain unchanged

Validation

  • cargo test -p switchyard-llm-client anthropic_prompt_caching_marks_final_message
  • cargo clippy -p switchyard-llm-client --all-targets -- -D warnings

@nachiketb-nvidia
nachiketb-nvidia requested a review from a team as a code owner July 31, 2026 19:35
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The change adds request-level caching control to LlmRequest and applies Anthropic prompt-caching rules to encoded request bodies. Tests cover default values, explicit disabling, marker injection, and marker removal.

Changes

Prompt caching

Layer / File(s) Summary
Request caching contract
crates/protocol/src/llm.rs
LlmRequest adds a public caching field. Manual defaults enable caching. Deserialization preserves explicit values.
Anthropic cache policy
crates/libsy-llm-client/src/client.rs
Anthropic processing removes cache markers when caching is disabled and preserves or injects ephemeral markers when caching is enabled. Tests cover injection and removal.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

A rabbit hops through cached text,
And marks the final block with care.
If caching sleeps, the marks depart,
While defaults keep the feature there.
“Thump-thump!” says Bun, “the paths are clear!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: enabling Anthropic prompt caching by default.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/libsy-llm-client/src/client.rs (1)

936-952: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Broaden test coverage for the new caching branches.

The new test only covers a single plain-string message content block being marked and then unmarked. It does not exercise: preserving a caller-supplied cache_control when caching is already enabled (the has_anthropic_cache_control early return), the system-prompt fallback, the last-tool fallback, or removal from a tools array with input_schema. Given this logic gates a cost-sensitive feature enabled by default, add cases for these branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/src/client.rs` around lines 936 - 952, Expand the
test `anthropic_prompt_caching_is_injected_and_can_be_disabled` with cases
covering preservation of caller-supplied `cache_control` via the
`has_anthropic_cache_control` early return, system-prompt fallback, last-tool
fallback, and removal of `cache_control` from tools containing `input_schema`.
Assert each branch’s expected JSON behavior while retaining the existing
plain-string injection and disablement coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/libsy-llm-client/src/client.rs`:
- Around line 718-779: Add concise comments to the private helpers
mark_last_text_cacheable, has_anthropic_cache_control, remove_cache_controls,
and is_anthropic_cacheable explaining their traversal purpose, the meaning of
Anthropic-cacheable content, the reverse traversal used to select the last text
block, and why input_schema is excluded from recursion. Keep the implementation
unchanged and align the wording with the nearby apply_anthropic_prompt_caching
comment.
- Around line 685-740: Update apply_anthropic_prompt_caching and
mark_last_text_cacheable to identify and mark the last block accepted by
is_anthropic_cacheable, including trailing image, document, tool_use, and
tool_result blocks rather than only text blocks. Add tests covering trailing
tool_use and tool_result blocks, and ensure the implementation does not create
redundant manual cache breakpoints when supported automatic top-level
cache_control is used.

---

Nitpick comments:
In `@crates/libsy-llm-client/src/client.rs`:
- Around line 936-952: Expand the test
`anthropic_prompt_caching_is_injected_and_can_be_disabled` with cases covering
preservation of caller-supplied `cache_control` via the
`has_anthropic_cache_control` early return, system-prompt fallback, last-tool
fallback, and removal of `cache_control` from tools containing `input_schema`.
Assert each branch’s expected JSON behavior while retaining the existing
plain-string injection and disablement coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 58c8b986-f59b-421b-918f-54e783240d64

📥 Commits

Reviewing files that changed from the base of the PR and between d676965 and 21f83ad.

📒 Files selected for processing (2)
  • crates/libsy-llm-client/src/client.rs
  • crates/protocol/src/llm.rs

Comment thread crates/libsy-llm-client/src/client.rs Outdated
Comment on lines +685 to +740
// Applies Switchyard's prompt-caching policy to an Anthropic request body.
fn apply_anthropic_prompt_caching(body: &mut Value, enabled: bool) {
if !enabled {
remove_cache_controls(body);
return;
}
if has_anthropic_cache_control(body) {
return;
}

if let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) {
for message in messages.iter_mut().rev() {
if message
.get_mut("content")
.is_some_and(mark_last_text_cacheable)
{
return;
}
}
}
if body.get_mut("system").is_some_and(mark_last_text_cacheable) {
return;
}
if let Some(tool) = body
.get_mut("tools")
.and_then(Value::as_array_mut)
.and_then(|tools| tools.last_mut())
.and_then(Value::as_object_mut)
{
tool.insert("cache_control".to_string(), json!({"type": "ephemeral"}));
}
}

fn mark_last_text_cacheable(content: &mut Value) -> bool {
match content {
Value::String(text) => {
*content = json!([{
"type": "text",
"text": std::mem::take(text),
"cache_control": {"type": "ephemeral"}
}]);
true
}
Value::Array(blocks) => blocks.iter_mut().rev().any(|block| {
let Some(object) = block.as_object_mut() else {
return false;
};
if object.get("type").and_then(Value::as_str) != Some("text") {
return false;
}
object.insert("cache_control".to_string(), json!({"type": "ephemeral"}));
true
}),
_ => false,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Anthropic's Messages API support a top-level automatic cache_control field, and is it compatible with existing per-block cache_control breakpoints in the same request?

💡 Result:

Yes, Anthropic's Messages API supports a top-level cache_control field, which enables automatic prompt caching [1][2]. When you include a cache_control field at the top level of your request, the system automatically applies a cache breakpoint to the last cacheable block and updates it as the conversation grows [1][3]. This method is designed for simplicity, particularly in multi-turn conversations [1][2]. Furthermore, automatic caching is compatible with explicit, per-block cache_control breakpoints within the same request [4][3]. When both are used together, the automatic cache breakpoint consumes one of the available four breakpoint slots per request [3]. This allows developers to combine the convenience of automatic, top-level caching with the fine-grained control provided by manual block-level breakpoints [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- client.rs symbols ---'
rg -n -C 8 'apply_anthropic_prompt_caching|mark_last_text_cacheable|is_anthropic_cacheable|remove_cache_controls|has_anthropic_cache_control' crates/libsy-llm-client/src/client.rs
printf '%s\n' '--- Anthropic API/version references ---'
rg -n -i -C 3 'anthropic|cache_control|prompt.?cach|messages' crates/libsy-llm-client Cargo.toml Cargo.lock 2>/dev/null | head -n 300
printf '%s\n' '--- package files ---'
git ls-files '*Cargo.toml' '*Cargo.lock'

Repository: NVIDIA-NeMo/Switchyard

Length of output: 27863


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- helper implementation and tests ---'
sed -n '685,780p' crates/libsy-llm-client/src/client.rs
sed -n '920,1035p' crates/libsy-llm-client/src/client.rs
printf '%s\n' '--- Anthropic wire-format definitions and conversions ---'
rg -n -C 5 'AnthropicMessages|tool_use|tool_result|document|cache_control|content' crates/libsy-llm-client/src
printf '%s\n' '--- request configuration and extra_body documentation ---'
rg -n -C 5 'struct HttpBackendConfig|extra_body|anthropic-version|caching' crates/libsy-llm-client/src crates/libsy-llm-client/README.md

Repository: NVIDIA-NeMo/Switchyard

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import json

def is_cacheable(obj):
    return "input_schema" in obj or obj.get("type") in {
        "text", "image", "document", "tool_use", "tool_result"
    }

def mark_last_text_cacheable(content):
    if isinstance(content, str):
        return [{"type": "text", "text": content,
                 "cache_control": {"type": "ephemeral"}}]
    if isinstance(content, list):
        for block in reversed(content):
            if not isinstance(block, dict) or block.get("type") != "text":
                continue
            block["cache_control"] = {"type": "ephemeral"}
            return True
    return False

cases = [
    ["text", "tool_use"],
    ["text", "tool_result"],
    ["image", "tool_result"],
    ["tool_result"],
]
for types in cases:
    body = {"messages": [{"role": "assistant", "content":
        [{"type": t} for t in types]}]}
    mark_last_text_cacheable(body["messages"][0]["content"])
    markers = [
        (i, b.get("type"))
        for i, b in enumerate(body["messages"][0]["content"])
        if isinstance(b, dict) and "cache_control" in b
    ]
    print(json.dumps({"input": types, "marked": markers}))
PY

Repository: NVIDIA-NeMo/Switchyard

Length of output: 368


🌐 Web query:

site:platform.claude.com/docs prompt caching top-level cache_control automatic cache_control Messages API anthropic-version 2023-06-01 tool_use tool_result

💡 Result:

Prompt caching in the Anthropic Messages API allows you to cache large prompt prefixes—including system instructions, tool definitions, and long conversation histories—to reduce latency and costs [1]. You can implement this using either top-level automatic caching or explicit cache breakpoints [1]. Top-Level vs. Explicit Cache Control - Top-Level Cache Control: By adding a single cache_control field at the top level of your request body, the API automatically applies a cache breakpoint to the last cacheable block in your request [1][2]. This is the simplest method and is well-suited for growing message histories [1]. - Explicit Cache Breakpoints: For more precise control, you can place a cache_control object directly on individual content blocks (such as specific text blocks or tool definitions) [1][2]. This allows you to cache different sections that change at different frequencies [1]. The cache_control object requires a type (currently always "ephemeral") and an optional ttl (time-to-live), which can be set to "5m" (5 minutes) or "1h" (1 hour) [3][2]. Tool Use and Cache Interaction - Automatic Breakpoints with Server Tools: If your request uses a server tool (e.g., web search, code execution), the API automatically places a cache breakpoint on the tool result before the next iteration of the agentic loop [4]. This automatic breakpoint uses a default 5-minute TTL, regardless of the TTLs you manually configure for other blocks [4]. - Tool Results: You can enable caching for tool results by attaching a cache_control object to the tool_result block [5]. When handling tool calls, ensure your tool_result blocks are placed first in the user message content array [6]. - Tool Definitions: Prompt caching references the full prompt prefix, which includes the tools, system instructions, and messages in that specific order [1]. Changing the tools array (such as adding, removing, or reordering tools) will invalidate the cache for that prefix [7]. For optimal performance, always include the anthropic-version header (e.g., 2023-06-01 or later) to ensure access to these features [3]. You can monitor the effectiveness of your caching strategy by checking the cache_creation and cache_read_input_tokens fields in the response usage object [8][9][7].

Citations:


Mark the last Anthropic cacheable block, not only text blocks.

is_anthropic_cacheable includes image, document, tool_use, and tool_result, but mark_last_text_cacheable checks only text. A trailing tool result therefore causes an earlier text block to be marked, or no message block to be marked. Reuse is_anthropic_cacheable and add tests for trailing tool_use and tool_result blocks.

Anthropic also supports top-level automatic cache_control for the Messages API, including use with explicit breakpoints. Consider using it instead of this traversal if all targeted providers support it; otherwise, prevent redundant manual breakpoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/src/client.rs` around lines 685 - 740, Update
apply_anthropic_prompt_caching and mark_last_text_cacheable to identify and mark
the last block accepted by is_anthropic_cacheable, including trailing image,
document, tool_use, and tool_result blocks rather than only text blocks. Add
tests covering trailing tool_use and tool_result blocks, and ensure the
implementation does not create redundant manual cache breakpoints when supported
automatic top-level cache_control is used.

Comment thread crates/libsy-llm-client/src/client.rs Outdated
Comment on lines +718 to +779
fn mark_last_text_cacheable(content: &mut Value) -> bool {
match content {
Value::String(text) => {
*content = json!([{
"type": "text",
"text": std::mem::take(text),
"cache_control": {"type": "ephemeral"}
}]);
true
}
Value::Array(blocks) => blocks.iter_mut().rev().any(|block| {
let Some(object) = block.as_object_mut() else {
return false;
};
if object.get("type").and_then(Value::as_str) != Some("text") {
return false;
}
object.insert("cache_control".to_string(), json!({"type": "ephemeral"}));
true
}),
_ => false,
}
}

fn has_anthropic_cache_control(value: &Value) -> bool {
match value {
Value::Object(object) => {
(is_anthropic_cacheable(object) && object.contains_key("cache_control"))
|| object
.iter()
.filter(|(key, _)| key.as_str() != "input_schema")
.any(|(_, value)| has_anthropic_cache_control(value))
}
Value::Array(values) => values.iter().any(has_anthropic_cache_control),
_ => false,
}
}

fn remove_cache_controls(body: &mut Value) {
match body {
Value::Object(object) => {
if is_anthropic_cacheable(object) {
object.remove("cache_control");
}
for (key, value) in object {
if key != "input_schema" {
remove_cache_controls(value);
}
}
}
Value::Array(values) => values.iter_mut().for_each(remove_cache_controls),
_ => {}
}
}

fn is_anthropic_cacheable(object: &Map<String, Value>) -> bool {
object.contains_key("input_schema")
|| matches!(
object.get("type").and_then(Value::as_str),
Some("text" | "image" | "document" | "tool_use" | "tool_result")
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add comments for these private traversal helpers.

mark_last_text_cacheable, has_anthropic_cache_control, remove_cache_controls, and is_anthropic_cacheable implement non-trivial recursive JSON traversal with an input_schema exclusion rule, but none carry a comment explaining intent (why input_schema is excluded, what "cacheable" means, why traversal order matters). apply_anthropic_prompt_caching has a one-line comment; these four do not.

As per coding guidelines, "Add concise Rust documentation comments (///) for public items and comments for module intent, non-obvious private helpers, ... and complex validation, routing, configuration, async, lifecycle, or concurrency logic."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libsy-llm-client/src/client.rs` around lines 718 - 779, Add concise
comments to the private helpers mark_last_text_cacheable,
has_anthropic_cache_control, remove_cache_controls, and is_anthropic_cacheable
explaining their traversal purpose, the meaning of Anthropic-cacheable content,
the reverse traversal used to select the last text block, and why input_schema
is excluded from recursion. Keep the implementation unchanged and align the
wording with the nearby apply_anthropic_prompt_caching comment.

Source: Coding guidelines

@nachiketb-nvidia
nachiketb-nvidia force-pushed the feat/default-prompt-caching branch 2 times, most recently from d7636bd to 04ad90e Compare July 31, 2026 19:52
@nachiketb-nvidia nachiketb-nvidia changed the title feat(llm): enable Anthropic prompt caching by default fix(translation): enable Anthropic prompt caching by default Jul 31, 2026
Signed-off-by: nachiketb <nachiketb@nvidia.com>
@nachiketb-nvidia
nachiketb-nvidia force-pushed the feat/default-prompt-caching branch from 04ad90e to 6ddd961 Compare July 31, 2026 20:08
@nachiketb-nvidia nachiketb-nvidia changed the title fix(translation): enable Anthropic prompt caching by default fix(llm-client): enable Anthropic prompt caching by default Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant