Skip to content

feat(anthropic): prompt caching via inline cache_control markers#2629

Open
hb-cam wants to merge 1 commit into
vectorize-io:mainfrom
hb-cam:anthropic-prompt-caching
Open

feat(anthropic): prompt caching via inline cache_control markers#2629
hb-cam wants to merge 1 commit into
vectorize-io:mainfrom
hb-cam:anthropic-prompt-caching

Conversation

@hb-cam

@hb-cam hb-cam commented Jul 8, 2026

Copy link
Copy Markdown

What

Adds Anthropic prompt caching to AnthropicLLM via inline cache_control markers — the "inline-marker provider" strategy that LLMInterface.get_or_create_cached_prefix's docstring already describes for Anthropic. No engine changes, no new config.

Today the provider sends no cache_control at all, so every call pays full input price on content the engine resends verbatim:

  • fact extraction reuses the same system prompt across every chunk;
  • the reflect agent loop resends the entire growing conversation on each of its iterations (up to HINDSIGHT_API_REFLECT_MAX_ITERATIONS), re-paying the full prefix each time.

Anthropic cache reads bill at ~10% of the base input price, so on reflect-heavy workloads (mental-model refresh) this is a large input-cost reduction with no latency penalty.

How

Two breakpoints (of the four Anthropic allows):

  1. System prompt, both entry points — rendered as a block list with cache_control: {type: "ephemeral"}. Caching is a prefix match, so this caches tools + system together. Schema text-injection (the non-strict response_format path) happens before marking and lands inside the cached block.
  2. Final message content block, call_with_tools() only — each agent-loop request's end-marker becomes the next iteration's cache read point. String content converts to a text block (skipped for empty strings, which the API rejects); block-list content (tool results) gets the marker on its last block.

Marking is safe unconditionally: below the model's minimum cacheable prefix the marker is silently ignored — no write premium, no error. cache_read_input_tokens already flows through _usage_from_anthropic_response into metrics/tracing, so cache effectiveness is observable immediately via cached_tokens.

Testing

  • 5 new tests in tests/test_anthropic_prompt_caching.py (written first, watched fail): marker placement on both entry points, schema-injection landing inside the cached block, tool-result block-list handling, and the no-system passthrough.
  • One existing assertion updated for the representation change: test_non_strict_keeps_text_injection_fallback checked a substring on system as a plain string; the schema-in-prompt behavior itself is unchanged and still covered.
  • test_anthropic_structured_tool_use.py, test_llm_extra_body.py, and test_mental_model_delta.py pass; ruff and ty clean.

🤖 Generated with Claude Code

The Anthropic provider sent no cache_control at all, so every call paid full
input price on content the engine resends verbatim: fact extraction reuses
the same system prompt across every chunk, and the reflect agent loop resends
the entire growing conversation on each of its (up to
HINDSIGHT_API_REFLECT_MAX_ITERATIONS) iterations. Anthropic cache reads bill
at ~10% of base input price.

Implement the "inline-marker provider" strategy that
LLMInterface.get_or_create_cached_prefix already documents for Anthropic —
no engine changes, no new config:

- call() and call_with_tools() render the system prompt as a block list with
  a cache_control breakpoint (a prefix match, so tools + system cache
  together); schema text-injection happens before marking and lands inside
  the cached block.
- call_with_tools() additionally marks the final message content block, so
  each agent-loop request's end-marker becomes the next iteration's cache
  read point. 2 of the 4 allowed breakpoints used.

Marking is safe unconditionally: below the model's minimum cacheable prefix
the marker is silently ignored (no write premium), and cache_read_input_
tokens already flows through _usage_from_anthropic_response into metrics.

One existing assertion updated for the representation change
(test_non_strict_keeps_text_injection_fallback checked a substring on system
as a string; the schema-in-prompt behavior itself is unchanged and still
covered). 5 new tests pin the marker placement on both entry points.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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