Skip to content

feat: Gemini context caching (ContextCacheConfig), at parity with adk-python, Java and Kotlin #1937

Description

@karthik-0306

Required Information

Is your feature request related to a specific problem?

Every turn of an ADK Go agent resends the full system instruction, every tool declaration and the whole conversation history to Gemini. For agents with long instructions, large toolsets (MCP, skills, OpenAPI) or long sessions, most of each request is a prefix the model has already processed. That prefix is paid for in full input tokens and latency on every call.

Gemini supports explicit context caching (CachedContent), and ADK exposes it as ContextCacheConfig in adk-python (v1.15.0), adk-java (v0.1.0) and adk-kotlin (v0.7.0) (docs). ADK Go has no equivalent: CachedContent is only read back today as CachedContentTokenCount for telemetry and agent analytics, and nothing in the flow creates or reuses a cache.

Proposed Solution

Port the existing cross-ADK design rather than invent a Go dialect. The behavior follows adk-python's GeminiContextCacheManager and ContextCacheRequestProcessor:

  1. Opt-in config on the runner. A ContextCache field on runner.Config, next to Compaction. Nil (the default) disables caching, so existing callers are unaffected. Fields and defaults match the other ADKs:
    • CacheIntervals (default 10, range 1–100): maximum invocations that reuse one cache before it is refreshed.
    • TTL (default 30m): lifetime of a created cache.
    • MinTokens (default 0): minimum prompt token count of the previous request before a cache is created.
    • CreateHTTPOptions (optional): for example a timeout on Caches.Create. When creation fails or times out, the request proceeds uncached.
  2. Request processor in internal/llminternal. This runs before contents are sent. It walks the session's events newest first for the current agent. It finds the latest cache metadata, incrementing InvocationsUsed when that metadata comes from an earlier invocation, and the previous prompt token count. Both go onto the request.
  3. Gemini cache manager in model/gemini. It uses the client's existing Caches API (Create / Delete):
    • It fingerprints the cacheable prefix: model, backend scope, system instruction, canonicalized tools, tool config and the first N contents. N is everything before the trailing run of user contents.
    • It reuses a cache while it has not expired, InvocationsUsed <= CacheIntervals, and the fingerprint still matches.
    • On invalidation it deletes the old cache. If the prefix is unchanged it creates a new one. Otherwise it returns fingerprint-only metadata so the next turn can try again.
    • It never creates a cache on a session's first request, below MinTokens, or below the model's minimum cache size (2048 tokens for Gemini 2.5, 4096 for Gemini 3).
    • It applies a cache by setting Config.CachedContent and dropping the system instruction, tools, tool config and cached contents from the request. The final content is always kept.
  4. CacheMetadata on model.LLMResponse. Because session.Event embeds LLMResponse, the metadata travels with the event, which is how the next turn finds it. The type has two states, active (CacheName, ExpireTime, InvocationsUsed, CreatedAt) and fingerprint-only (Fingerprint, ContentsCount), plus an ExpireSoon() helper. Token counts stay in UsageMetadata, as in the other ADKs.

Models other than Gemini ignore the config, so openaimodel is unchanged. This is independent of #1866, which adds Anthropic's own cache_control breakpoints inside the Anthropic model package. That PR does not manage Gemini CachedContent resources.

Impact on your work

This brings ADK Go to parity with three other ADKs on a feature that directly cuts cost and latency for production agents. It is also a prerequisite for a later StaticInstruction on llmagent, the stable-prefix instruction adk-python pairs with caching.


Recommended Information

Alternatives Considered

  • Setting GenerateContentConfig.CachedContent by hand. This works for one static cache. But the caller then has to create, refresh and delete caches themselves, track expiry and invalidate when tools or instructions change. They also have to strip the cached fields from each request, because Gemini rejects a request that sends them alongside CachedContent. That is the lifecycle this feature owns.
  • A plugin. Plugins see the request but cannot persist per-agent cache state on events or reach the model's client cleanly. The other ADKs put this in the flow and the model layer for that reason.

Willingness to contribute

Yes — I'd like to implement this, and I'm starting on it now. The core logic (fingerprinting, cache validation and invalidation, the request processor, and httprr-recorded tests) doesn't depend on the open questions below, so I'll have a PR ready to open shortly after we agree on them.

Following the guidance on #245, the PR will stay focused and reviewable on its own: config, metadata, the request processor and the Gemini cache manager, with unit tests, recorded tests and an example. StaticInstruction would be a separate follow-up. If you'd rather see it split further (for example, types and the processor first, then the Gemini manager), I'm happy to do that.

Proposed API / Implementation

r, err := runner.New(runner.Config{
	AppName:        "app",
	Agent:          rootAgent,
	SessionService: sessionService,
	ContextCache: &contextcache.Config{
		CacheIntervals: 10,
		TTL:            30 * time.Minute,
		MinTokens:      4096,
	},
})
// On model.LLMResponse, and therefore on session.Event.
type CacheMetadata struct {
	CacheName       string    // empty in the fingerprint-only state
	ExpireTime      time.Time
	InvocationsUsed int
	CreatedAt       time.Time
	Fingerprint     string
	ContentsCount   int
}

Questions for maintainers:

  1. Package placement. Should the config and metadata types go in a stable package (for example model or a new contextcache package)? Or should the feature start under exp/, given that adk-python still marks the manager @experimental? The runner.Config and LLMResponse fields have to be stable API either way.
  2. Persistence. session/database stores response metadata in per-field columns, so CacheMetadata needs its own column or has to ride along in an existing JSON column. Which do you prefer? Session services that drop the field degrade safely: every turn is fingerprint-only and nothing is cached.
  3. Workflows and resumption. The processor scopes metadata by agent name, as in adk-python. Does that hold for the same agent placed in several workflow nodes, or should the key include the branch?

Additional Context

Activity

  1. indurireddy-TF commented on Oct 23, 2026

    @indurireddy-TF
    Contributor

    Hi @kdroste-google , tagging you to review this feature request when you get a chance. It outlines an enhancement for Gemini context caching (ContextCacheConfig) to align Go with the Python and Java/Kotlin SDKs.

  2. karthik-0306 commented on Oct 23, 2026

    @karthik-0306
    ContributorAuthor

    @kdroste-google I've opened the implementation in #1951 . Since the design questions on this issue are still open, I've answered each one in the PR so you have something concrete to approve or change:

    1. Package placement: the config and metadata types are in model (model.ContextCacheConfig, model.CacheMetadata), because LLMRequest and LLMResponse reference them and every model implementation needs them. A separate package would only be imported back into model. The PR is all stable API, with nothing under exp/. The hooks (runner.Config.ContextCache, LLMResponse.CacheMetadata) have to be stable either way, and the design is a direct port of what already ships in Python, Java and Kotlin.
    2. Persistence: session/database gets a nullable cache_metadata column, following what fix(session/database): persist input and output transcriptions #1662 did for the transcription columns. Vertex AI carries the field in raw_event. This does mean database users who manage their own schema must add the column before upgrading, even with caching off. I've put that first in the PR description and would like your call on whether it needs the breaking-change label.
    3. Workflows and branches: the author name alone isn't enough. Parallel copies of one agent would delete each other's caches. The PR scopes cache state to author, branch and isolation scope. It matches the boundary the contents processor already uses.

    A few things go beyond a straight port of Python. They're listed under "Differences from adk-python", each with its own tests, and any of them can be dropped if you'd rather keep the first PR closer to the reference.

    One limit on testing: I could only run it against a free-tier Gemini key, which refuses cache creation, so the successful-create path is covered by tests but not yet confirmed live. If someone with a billing-enabled key can run go run ./examples/contextcache/, that would close the gap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions