feat(models): add prompt caching for the Anthropic provider (backport to release/v0.10.x) - #2789
Conversation
Backport of kagent-dev#2788 to release/v0.10.x. 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. Unlike main, this branch pins google-adk 1.x, which maps only input_tokens for Anthropic, so the wrapper also records the raw usage per request and generate_content_async folds the cache breakdown into the response the same way google-adk 2.x does. Refs kagent-dev#2787, kagent-dev#1866. Signed-off-by: Timo Derstappen <teemow@gmail.com>
There was a problem hiding this comment.
🟡 Changes recommended
Two newly added tests intended to assert “no caller object mutation” currently use shallow copies and can miss nested in-place mutations, weakening the regression coverage for this change.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR backports Anthropic prompt caching support to release/v0.10.x, adding per-ModelConfig knobs and wiring them through the controller translator into both the Go and Python runtimes so Anthropic Messages requests include cache_control breakpoints and cache usage is reflected in token accounting.
Changes:
- Add
spec.anthropic.promptCaching(defaultfalse) andspec.anthropic.cacheTTL("5m"|"1h", default"5m") to the v1alpha2ModelConfigAPI + regenerate CRDs. - Implement request shaping for Anthropic prompt caching in both runtimes (mark 3 breakpoints) and fold Anthropic cache usage into the GenAI usage shape.
- Add/extend unit tests for translator wiring, runtime request shaping, and usage folding; document Helm values.
File summaries
| File | Description |
|---|---|
| python/packages/kagent-adk/tests/unittests/models/test_anthropic.py | Adds unit tests covering Anthropic prompt caching request shaping and usage folding (plus translator/config wiring). |
| python/packages/kagent-adk/src/kagent/adk/types.py | Extends the Python Anthropic model config with prompt_caching/cache_ttl and forwards them into LLM construction. |
| python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py | Implements prompt-caching request shaping via an AsyncMessages wrapper + folds cache usage into response usage metadata. |
| helm/kagent/values.yaml | Documents Anthropic provider config values for prompt caching. |
| helm/kagent-crds/templates/kagent.dev_modelconfigs.yaml | Helm-rendered CRD schema update for the new Anthropic fields. |
| go/core/internal/controller/translator/agent/adk_api_translator.go | Wires promptCaching/cacheTTL from ModelConfig into the internal ADK model config. |
| go/core/internal/controller/translator/agent/adk_api_translator_test.go | Adds a translator test ensuring Anthropic prompt caching fields are passed through. |
| go/api/v1alpha2/modelconfig_types.go | Adds the new Anthropic CRD fields and docs to the Go API types. |
| go/api/config/crd/bases/kagent.dev_modelconfigs.yaml | Regenerated base CRD YAML reflecting the new schema fields. |
| go/api/adk/types.go | Extends the internal adk.Anthropic config with prompt_caching/cache_ttl JSON fields. |
| go/adk/pkg/models/anthropic.go | Threads the prompt caching knobs into the Go Anthropic runtime config struct. |
| go/adk/pkg/models/anthropic_adk.go | Implements breakpoint marking and cache-usage folding for Anthropic in the Go runtime (streaming + non-streaming). |
| go/adk/pkg/models/anthropic_adk_test.go | New Go unit tests asserting wire-level markers and usage folding, including streaming via httptest. |
| go/adk/pkg/agent/agent.go | Passes new Anthropic prompt caching config through agent LLM creation. |
| go/adk/pkg/agent/agent_test.go | Extends deserialization and creation tests to validate Anthropic prompt caching fields flow into the runtime. |
Review details
Suppressed comments (1)
python/packages/kagent-adk/tests/unittests/models/test_anthropic.py:251
test_does_not_mutate_caller_objectssnapshotslast_tool/last_messagewith shallowdict(...)copies;last_messagestill shares nestedcontentlists/dicts with the original, so in-place mutations to nested structures wouldn’t be caught. Use deep copies for these snapshots to make the test robust.
tools, messages = kwargs["tools"], kwargs["messages"]
last_tool, last_message = dict(tools[-1]), dict(messages[-1])
mark_prompt_cache_breakpoints(kwargs, self.CC)
assert tools[-1] == last_tool
assert messages[-1] == last_message
- Files reviewed: 15/15 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| before = {k: list(v) if isinstance(v, list) else v for k, v in kwargs.items()} | ||
| mark_prompt_cache_breakpoints(kwargs, self.CC) | ||
| assert kwargs == before |
…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>
Backport of #2788 to
release/v0.10.x. Refs #2787 (the feature request, with measurements) and #1866 (the earlier request 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
Same shape as #2788, on the
v1alpha2API: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;adk_api_translator.gowires them intoadk.Anthropic(prompt_caching,cache_ttl). 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. 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): request shaping extracted intobuildAnthropicParams,markAnthropicCacheBreakpointssets the markers, and usage mapscache_read_input_tokenstoCachedContentTokenCountand folds cache read/creation tokens intoPromptTokenCount, 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 — the one seam google-adk'sAnthropicLlmhands its request through on both paths.main: this branch pins google-adk 1.x, which maps onlyinput_tokensfor Anthropic and drops the cache breakdown, whereas google-adk 2.x onmainalready folds it. Here the wrapper also records the raw response usage per request (aContextVar, so concurrent requests on one model stay apart; for streaming it readsmessage_start) andgenerate_content_asyncfolds it into google-adk's usage metadata the same way google-adk 2.x does (fold_cache_usage). WithpromptCachingoff nothing changes.providers.anthropic.config.promptCaching/cacheTTLdocumented invalues.yaml; the chart already rendersconfiginto the provider block.The Claude-harness compiler change from #2788 does not apply here (no harness compilers on 0.10.x).
Testing
Go:
go vet+go test ./adk/... ./api/... ./core/internal/controller/translator/...(newanthropic_adk_test.go— wire body carries exactly three markers at the expected positions for default/5m/1h, none when disabled, trailing-tool-result and no-tools/no-system cases, usage fold, and the SSE streaming + non-streaming paths through anhttptestserver; agent wiring and config deserialization;Test_AdkApiTranslator_AnthropicPromptCaching). golangci-lint: the fullmake -C go lintcrashes locally inside staticcheck'sbuildiron this branch with my toolchain (unrelated to the change); the remaining linters report 0 issues on the changed packages, CI runs the full set.Python:
pytest packages/kagent-adk/tests/unittests(443 passed) — new tests formark_prompt_cache_breakpoints, the client wrapping,fold_cache_usage, and end-to-end through google-adk's request builder with the SDK client mocked, for both the non-streaming and the streaming path.Against the Anthropic API with the same runtime code on
main(see feat(models): add prompt caching for the Anthropic provider (promptCaching, cacheTTL) #2788): after the first call ~99 % of every prompt is served from the cache and reported as cached tokens, in both runtimes.kind end-to-end on this branch (controller,
golang-adkandappimages built from this commit; chart installed withproviders.default=anthropic,providers.anthropic.config.promptCaching=true, modelclaude-sonnet-4-6; the renderedModelConfigwas accepted withpromptCaching: true/cacheTTL: 5m). Two declarative agents,runtime: goandruntime: python, with the built-inkagent-tool-serverKubernetes tools (~14 k tokens of tool definitions + system prompt), driven through the controller's A2A endpoint (/api/a2a/kagent/<agent>,message/sendwith onecontextId) for three turns. Values areprompt_token_count / cached_content_token_countfrom the usage metadata the runtime attaches to the A2A response, i.e. what the UI shows:Every model call after the first, including the tool-result follow-up inside the same turn, reads ~99.5 % of its prompt from the cache.