Repository navigation
feat(models): add prompt caching for the Anthropic provider (promptCaching, cacheTTL) - #2788
Conversation
EItanya
left a comment
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.
…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.
…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.
) — kagent-dev#2788 carried as a cherry-pick
|
Looked at it. Kept the wrapper for now: ADK's Anthropic breakpoints are not in 2.6.2 (our 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>
f5679c2 to
aab0eaa
Compare
) — kagent-dev#2788 carried as a cherry-pick
) — kagent-dev#2788 carried as a cherry-pick
|
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
|
@EItanya can you re-run the failed job? It's not a test failure but |
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.
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.
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.