feat(models): add prompt caching for the Anthropic provider (promptCaching, cacheTTL) - #2788
Open
teemow wants to merge 2 commits into
Open
feat(models): add prompt caching for the Anthropic provider (promptCaching, cacheTTL)#2788teemow wants to merge 2 commits into
teemow wants to merge 2 commits into
Conversation
Add `promptCaching` (default false) and `cacheTTL` ("5m" | "1h") to
`spec.anthropic` on ModelConfig, mirroring the knobs BedrockConfig got in
kagent-dev#1940, and honor them in both runtimes.
When enabled, every Messages API request carries three `cache_control`
breakpoints: the last tool definition, the last system prompt block and
the last content block of the latest conversation turn. Anthropic
renders tools, then system, then messages and caches the prefix up to
each breakpoint, so an agent loop reads its whole previous history from
the cache and writes only the new turn instead of paying full input
price for the same prefix on every call. "1h" opts into the 1-hour
cache; "5m" (the CRD default) leaves the API default in place.
Go runtime: buildAnthropicParams now owns the request shaping so it can
be unit-tested, markAnthropicCacheBreakpoints sets the markers, and
usage folds cache_read/cache_creation into PromptTokenCount with
cache_read surfaced as CachedContentTokenCount so the UI shows the
effect. Python runtime: KAgentAnthropicLlm wraps the SDK client's
messages resource and marks the same three breakpoints before each
request; google-adk already maps the cache usage fields.
The Claude harness compiler treats the defaulted cacheTTL "5m" like the
Bedrock one and keeps rejecting promptCaching, since Claude Code caches
natively.
Refs kagent-dev#2787, kagent-dev#1866.
Signed-off-by: Timo Derstappen <teemow@gmail.com>
This was referenced Sep 10, 2026
Open
…ests Shallow copies would let a nested in-place mutation of a message or tool block go unnoticed, which is exactly what these tests guard against. Signed-off-by: Timo Derstappen <teemow@gmail.com>
EItanya
reviewed
Sep 10, 2026
EItanya
left a comment
Contributor
There was a problem hiding this comment.
🤖 AI-generated review.
No correctness issues found at f5679c2a. One optional simplification:
- Reuse Python ADK’s existing caching support — _anthropic.py:81. The locked
google-adk 2.8.0already generates these three breakpoints throughLlmRequest.cache_config. Mapping the settings toContextCacheConfigcould remove the custom messages wrapper and marker logic. I verified identical wire requests for both TTLs, with streaming enabled and disabled. Check support at the declared minimum ADK version before switching.
Validation passed: four affected Go packages (adk/pkg/models, adk/pkg/agent, core/internal/translator/adkconfig, and core/internal/translator/claude), all 24 Python Anthropic tests, and an additional SDK comparison probe using mocked HTTP. No live Anthropic or cluster tests run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2787. Picks up the Anthropic half of #1866, whose Bedrock half shipped in #1940.
Problem
A declarative agent re-sends its full history on every model call. With the Anthropic provider nothing is marked cacheable today:
promptCachingexists only onBedrockConfig, and neither runtime sendscache_control, so the whole prefix (system prompt, tool definitions, previous turns) is billed at full input price on every call.Measured on kagent 0.10.1 on an internal installation, through a gateway that logs
gen_ai.usage.*per model call (162 calls over 24 h, three agents,claude-sonnet-4-6via the Anthropic provider):cache_read.input_tokensandcache_creation.input_tokenswere 0 on every call. One incident-investigation session of a Python (kagent-app) agent made 119 model calls whose input grew from 4,423 to 190,809 tokens per call (average 93 k), i.e. roughly 11 M input tokens billed for one session, most of it the same prefix over and over; a Go (golang-adk) agent in the same window went 3.6 k → 93 k over 41 calls. Anthropic's prompt caching turns that repeat into cache reads at a fraction of the input price. Full numbers in #2787.Change
spec.anthropic.promptCaching(bool, defaultfalse) andspec.anthropic.cacheTTL("5m"|"1h", default"5m") onModelConfig, with the same names, defaults and doc shape as the Bedrock fields, wired throughadk.Anthropic(prompt_caching,cache_ttl) to both runtimes. CRDs regenerated (make controller-manifests).cache_controlbreakpoints: the last tool definition, the last system prompt block, and the last content block of the latest conversation turn. Anthropic renders tools → system → messages and caches the prefix up to each breakpoint, so each call of an agent loop reads its whole previous history from the cache and writes only the new turn. That is three of the four breakpoints Anthropic allows; the conversation breakpoint skips thinking blocks, which Anthropic refuses to cache.go/adk/pkg/models/anthropic_adk.go): the request shaping moves intobuildAnthropicParamsso it can be unit-tested,markAnthropicCacheBreakpointssets the markers, and usage now mapscache_read_input_tokenstoCachedContentTokenCountand folds cache read/creation tokens intoPromptTokenCount(the GenAI shape treats cached as a breakdown of prompt, which is also how google-adk maps Anthropic usage on the Python side), so the UI's usage view shows the effect. Streaming and non-streaming.KAgentAnthropicLlm): wraps the SDK client'smessagesresource withPromptCachingMessages, which marks the same three breakpoints before eachcreatecall. That is the one seam google-adk'sAnthropicLlmhands its request through on both the streaming and the non-streaming path, so the request builder is not duplicated. google-adk ≥ 2.x already mapscache_read_input_tokensintocached_content_token_count; a regression guard for that is added.cacheTTL: "5m"like the Bedrock one (without this everyspec.anthropicblock would now fail the "no options beyond baseUrl" check, because the CRD defaults the field whenever the block is present) and keeps rejectingpromptCaching: true, since Claude Code caches natively.providers.anthropic.config.promptCaching/cacheTTLdocumented invalues.yaml; the chart already rendersconfiginto the provider block.Design notes: opt-in like the Bedrock knob. A per-model minimum cacheable prefix applies (1024–4096 tokens; below it the markers are silently ignored) and 5-minute cache writes cost 1.25× normal input, so a prefix has to be read at least once to pay off. One knob enables all three breakpoints: the moving conversation breakpoint is what makes agent loops cheap, and a separate switch for it would be extra API surface for no realistic configuration. This is complementary to compaction (#2728): compaction bounds the growth of the prefix, caching removes the per-call repeat cost.
Testing
Go:
go test ./adk/... ./api/... ./core/internal/translator/...,make -C go lint. Newanthropic_adk_test.goasserts the wire body (as the SDK serializes it) carries exactly three markers at the expected positions for the default,"5m"and"1h"TTLs, none when disabled, the trailing-tool-result and no-tools/no-system cases, the usage fold, and drives both the SSE streaming and the non-streaming path through anhttptestserver. Plus agent wiring and config deserialization, translator wiring, and Claude compiler accept (cacheTTL: "5m") / reject (promptCaching) cases.Python:
pytest packages/kagent-adk/tests/unittests(228 passed;test_grpc_healthfails locally only because port 8081 is taken on my machine). New tests formark_prompt_cache_breakpoints(positions, thinking blocks skipped, no mutation of caller objects), the client wrapping, and end-to-end through google-adk's request builder with the SDK client mocked, including the usage-mapping guard.Against the Anthropic API (
claude-sonnet-4-6, ~2.7 k-token system prompt + 2 tools, three consecutive agent-loop calls alternating non-streaming/streaming, both runtimes, values areprompt_token_count / cached_content_token_countas reported by the runtime):After the first call ~99 % of every prompt is served from the cache and surfaces as cached tokens.
Not run here: a kind-based run of an
AgentTemplateonmain(the kind flow onmainhas Substrate disabled by default). The kind end-to-end run was done on therelease/v0.10.xbackport, feat(models): add prompt caching for the Anthropic provider (backport to release/v0.10.x) #2789, which runs agents as Deployments: Go and Python declarative agents with the built-in Kubernetes tools (~14 k-token prefix), three turns through the controller's A2A endpoint — every model call after the first reads ~99.5 % of its prompt from the cache (e.g. Go 15665 / 14257 cached on the tool-result follow-up, 16337 / 16266 on turn 3), reported in the A2A usage metadata. Table in feat(models): add prompt caching for the Anthropic provider (backport to release/v0.10.x) #2789.Follow-ups, not in this PR: the UI model form exposes neither this nor Bedrock's
promptCaching; a docs note for kagent-dev/websitesupported-providers/anthropic.mdonce this lands.