Skip to content

feat(models): add prompt caching for the Anthropic provider (promptCaching, cacheTTL) - #2788

Merged
EItanya merged 3 commits into
kagent-dev:mainfrom
teemow:feat/anthropic-prompt-caching
Oct 5, 2026
Merged

EItanya merged 3 commits into
kagent-dev:mainfrom
teemow:feat/anthropic-prompt-caching

Conversation

@teemow

@teemow teemow commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

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: promptCaching exists only on BedrockConfig, and neither runtime sends cache_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-6 via the Anthropic provider): cache_read.input_tokens and cache_creation.input_tokens were 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, default false) and spec.anthropic.cacheTTL ("5m" | "1h", default "5m") on ModelConfig, with the same names, defaults and doc shape as the Bedrock fields, wired through adk.Anthropic (prompt_caching, cache_ttl) to both runtimes. CRDs regenerated (make controller-manifests).
  • 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 → 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 runtime (go/adk/pkg/models/anthropic_adk.go): the request shaping moves into buildAnthropicParams so it can be unit-tested, markAnthropicCacheBreakpoints sets the markers, and usage now maps cache_read_input_tokens to CachedContentTokenCount and folds cache read/creation tokens into PromptTokenCount (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.
  • Python runtime (KAgentAnthropicLlm): wraps the SDK client's messages resource with PromptCachingMessages, which marks the same three breakpoints before each create call. That is the one seam google-adk's AnthropicLlm hands its request through on both the streaming and the non-streaming path, so the request builder is not duplicated. google-adk ≥ 2.x already maps cache_read_input_tokens into cached_content_token_count; a regression guard for that is added.
  • Claude harness compiler: treats the CRD-defaulted cacheTTL: "5m" like the Bedrock one (without this every spec.anthropic block would now fail the "no options beyond baseUrl" check, because the CRD defaults the field whenever the block is present) and keeps rejecting promptCaching: true, since Claude Code caches natively.
  • Helm: providers.anthropic.config.promptCaching / cacheTTL documented in values.yaml; the chart already renders config into 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. New anthropic_adk_test.go asserts 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 an httptest server. 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_health fails locally only because port 8081 is taken on my machine). New tests for mark_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 are prompt_token_count / cached_content_token_count as reported by the runtime):

    runtime promptCaching call 1 call 2 call 3
    Go off 2739 / 0 2751 / 0 2763 / 0
    Go on, 5m 2739 / 0 (cache write) 2751 / 2736 2763 / 2748
    Python off 2740 / 0 2756 / 0 2772 / 0
    Python on, 5m 2740 / 0 (cache write) 2756 / 2737 2772 / 2753

    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 AgentTemplate on main (the kind flow on main has Substrate disabled by default). The kind end-to-end run was done on the release/v0.10.x backport, 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/website supported-providers/anthropic.md once this lands.

@EItanya EItanya left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 AI-generated review.

No correctness issues found at f5679c2a. One optional simplification:

  1. Reuse Python ADK’s existing caching support — _anthropic.py:81. The locked google-adk 2.8.0 already generates these three breakpoints through LlmRequest.cache_config. Mapping the settings to ContextCacheConfig could 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.

teemow added a commit to giantswarm/kagent-upstream that referenced this pull request Sep 15, 2026
teemow added a commit to giantswarm/agent-platform that referenced this pull request Sep 15, 2026
…figs; the kagent line moves to v0.11.0-gs.15 (giantswarm/giantswarm#37788)

The kagent Go ADK sent every Anthropic request without cache_control, so
each model call of an agent loop re-processed its whole prefix at full
input price. kagent 0.11.0-gs.15 carries kagent-dev/kagent#2788
(spec.anthropic.promptCaching / cacheTTL on the ModelConfig CRD, three
cache_control breakpoints in the Go ADK). The meta chart sets
kagent.providers.anthropic.config.promptCaching: true and cacheTTL: 5m
for the default ModelConfig; the connectivity chart's Anthropic
kagent.modelConfigs[] entries inherit both (an entry's own keys win,
Bedrock takes its own keys, other providers refuse them, cacheTTL is held
to the CRD's enum). components.kagent / kagent-crds move to
>=0.11.0-gs.15 <0.11.1-0, the floor because an older CRD would prune the
fields. make verify-prompt-caching, the CRD validation shape, BOM and
verify-components follow.
teemow added a commit to giantswarm/agent-platform that referenced this pull request Sep 15, 2026
…figs; the kagent line moves to v0.11.0-gs.15 (giantswarm/giantswarm#37788) (#464)

The kagent Go ADK sent every Anthropic request without cache_control, so
each model call of an agent loop re-processed its whole prefix at full
input price. kagent 0.11.0-gs.15 carries kagent-dev/kagent#2788
(spec.anthropic.promptCaching / cacheTTL on the ModelConfig CRD, three
cache_control breakpoints in the Go ADK). The meta chart sets
kagent.providers.anthropic.config.promptCaching: true and cacheTTL: 5m
for the default ModelConfig; the connectivity chart's Anthropic
kagent.modelConfigs[] entries inherit both (an entry's own keys win,
Bedrock takes its own keys, other providers refuse them, cacheTTL is held
to the CRD's enum). components.kagent / kagent-crds move to
>=0.11.0-gs.15 <0.11.1-0, the floor because an older CRD would prune the
fields. make verify-prompt-caching, the CRD validation shape, BOM and
verify-components follow.
teemow added a commit to giantswarm/kagent-upstream that referenced this pull request Sep 15, 2026
@teemow

teemow commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Looked at it. Kept the wrapper for now: ADK's Anthropic breakpoints are not in 2.6.2 (our floor), ContextCacheConfig is app-wide and experimental, and it only caches from the second turn. Happy to switch once we raise the floor.

Add `promptCaching` (default false) and `cacheTTL` ("5m" | "1h") to
`spec.anthropic` on ModelConfig, mirroring the knobs BedrockConfig got in

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>
…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>
@teemow
teemow force-pushed the feat/anthropic-prompt-caching branch from f5679c2 to aab0eaa Compare September 20, 2026 08:17
QuentinBisson pushed a commit to giantswarm/kagent-upstream that referenced this pull request Sep 22, 2026
QuentinBisson pushed a commit to giantswarm/kagent-upstream that referenced this pull request Sep 22, 2026
QuentinBisson pushed a commit to giantswarm/kagent-upstream that referenced this pull request Sep 24, 2026
QuentinBisson pushed a commit to giantswarm/kagent-upstream that referenced this pull request Sep 24, 2026
@fiunchinho

Copy link
Copy Markdown

hey @EItanya , is there any pending work to be done before this PR can be merged?

* upstream/main: (79 commits)
  fix(translator): accept Mistral ModelConfigs in v1alpha3 (kagent-dev#3008)
  fix: allow disabling kagent model streaming (kagent-dev#3028)
  feat(helm): make the controller ServiceAccount optional (kagent-dev#3022)
  chore: bump retired gemini 2.5 default models to 3.5 (kagent-dev#3027)
  fix(api): defer unsupported configuration fields (kagent-dev#3007)
  docs: update README for the v1 architecture (kagent-dev#2996)
  fix: keep Go ADK SQLite connections out of VM snapshots (kagent-dev#3004)
  fix(ui): report harness delete failures (kagent-dev#2964)
  fix: repair image scans and use uv-managed Python (kagent-dev#2995)
  fix(e2e): wait for controller health and parallelize independent tests (kagent-dev#2993)
  fix(adk): pass MCP embedded resource text to OpenAI-compatible models (kagent-dev#2992)
  fix(controller): simplify agent preparation and retry handling (kagent-dev#2994)
  chore(deps): bump filelock from 3.29.4 to 4.0.3 in /python (kagent-dev#2982)
  chore(deps): bump the python-minor-patch group in /python with 6 updates (kagent-dev#2977)
  chore(deps): bump the python-google-ai group in /python with 2 updates (kagent-dev#2975)
  chore(deps): update wheel requirement from >=0.47.0 to >=0.48.0 in /python (kagent-dev#2981)
  chore(deps): update anthropic[vertex] requirement from >=0.120.0 to >=1.8.0 in /python (kagent-dev#2985)
  chore(deps): bump mcp from 1.29.0 to 2.2.0 in /python (kagent-dev#2984)
  fix(adk): keep Ollama tool call argument order stable across requests (kagent-dev#2988)
  chore(deps): update google-adk[a2a] requirement from <3,>=2.6.2 to >=2.9.2,<3 in /python (kagent-dev#2983)
  ...

Signed-off-by: Timo Derstappen <teemow@gmail.com>

# Conflicts:
#	go/adk/pkg/models/anthropic_adk.go
#	go/adk/pkg/models/anthropic_adk_test.go
#	go/core/internal/translator/adkconfig/model_test.go
#	python/packages/kagent-adk/src/kagent/adk/models/_anthropic.py
@fiunchinho

Copy link
Copy Markdown

@EItanya can you re-run the failed job? It's not a test failure but The self-hosted runner lost communication with the server..

@EItanya
EItanya enabled auto-merge October 5, 2026 11:09
@EItanya
EItanya added this pull request to the merge queue Oct 5, 2026
Merged via the queue into kagent-dev:main with commit 23c805b Oct 5, 2026
55 of 56 checks passed
QuentinBisson added a commit to giantswarm/kagent-upstream that referenced this pull request Oct 8, 2026
The pin row names upstream v1.0.0-alpha9 (main@6c4cfb2b); the carried
table loses the prompt-caching row upstream merged as kagent-dev#2788;
the manual re-pin record is current. The ledger's docs(fork) commits since
the last re-pin are folded into this one.
QuentinBisson added a commit to giantswarm/kagent-upstream that referenced this pull request Oct 8, 2026
The pin row names upstream v1.0.0-alpha9 (main@6c4cfb2b); the carried
table loses the prompt-caching row upstream merged as kagent-dev#2788;
the manual re-pin record is current. The ledger's docs(fork) commits since
the last re-pin are folded into this one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Prompt caching for the Anthropic provider (cache_control on system prompt and tools), like Bedrock's promptCaching

3 participants