Skip to content

fix(llm): don't emit explicit cache_control markers for Gemini - #3586

Merged
VascoSch92 merged 3 commits into
mainfrom
3585-gemini-prompt-caching-regression-explicit-cache_control-markers-3090-freeze-cache-at-static-prefix-614-cost
Jun 9, 2026
Merged

fix(llm): don't emit explicit cache_control markers for Gemini#3586
VascoSch92 merged 3 commits into
mainfrom
3585-gemini-prompt-caching-regression-explicit-cache_control-markers-3090-freeze-cache-at-static-prefix-614-cost

Conversation

@VascoSch92

@VascoSch92 VascoSch92 commented Jun 9, 2026

Copy link
Copy Markdown
Member

AGENT:

Validation is deterministic + offline. The regression is a cache-billing behavior, so the proof is twofold:

  • Code-level proof (this PR's test): test_gemini_prompt_caching_emits_no_markers builds a multi-turn Gemini conversation where the system message (index 0) and the last user message (index 3) are non-adjacent — the exact
    shape that froze the cache — and asserts is_caching_prompt_active() is False and that no cache_control marker appears anywhere in the formatted payload. No markers → no explicit cachedContent attached → Gemini stays on
    Google's implicit-caching path.
  • Behavioral proof (issue Gemini prompt caching regression: explicit cache_control markers (#3090) freeze cache at static prefix (~6–14× cost) #3585): the production SWE-bench/GAIA A/B straddling the regression shows cache-hit ratio dropping ~83% → ~36% and spend rising ~6× (SWE-bench) / ~14× (GAIA) once markers were emitted. I did not
    re-run a live Gemini eval for this PR; the offline test reproduces the root cause deterministically.

Commands run:

  • uv run pytest tests/sdk/llm/test_model_features.py::test_prompt_cache_support tests/sdk/llm/test_prompt_caching_cross_conversation.py → 28 passed
  • uv run pytest tests/sdk/llm/test_model_features.py tests/sdk/llm/test_llm_completion.py → 165 passed (confirms the prompt-cache-too-small retry tests, which use claude-sonnet as the real model, are unaffected)

Why

#3090 added gemini-2.5/gemini-3 to PROMPT_CACHE_MODELS, which made the SDK emit Anthropic-style cache_control markers for Gemini. For Anthropic, the second marker on the last user/tool message extends the cached prefix
every turn. For Gemini via LiteLLM it does the opposite:

  • LiteLLM's Vertex/Gemini context-caching transform (separate_cached_messages) keeps only the first contiguous marked block. The SDK marks two non-adjacent messages (system + last user/tool), so after turn 1 the cache
    collapses to just the static system prefix — frozen for the whole trajectory.
  • Attaching any explicit cachedContent to the request disables Google's implicit caching on the growing conversation body, so the body is re-billed as fresh input every turn.

Net effect: Gemini runs since post-#3090 silently cost ~6–14× more, with no resolve-rate benefit. Before #3090 (no markers), Gemini's implicit caching covered the full growing prefix (~75–83% hit).

Summary

  • Remove gemini-2.5/gemini-3 from PROMPT_CACHE_MODELS so supports_prompt_cache is False for Gemini → no cache_control markers → Gemini falls back to Google's implicit prefix caching (pre-Enable Gemini prompt cache markers #3090 behavior). Leaves a guard
    comment so it isn't re-added.
  • Update the now-stale _apply_prompt_caching comment (the moving breakpoint is Anthropic-only).
  • Tests: flip the three Gemini rows in test_prompt_cache_support to False; replace the old "Gemini emits markers" test with a multi-turn regression test asserting no markers are emitted.

Issue Number

#3585

How to Test

  1. cd software-agent-sdk && uv sync
  2. uv run pytest tests/sdk/llm/test_model_features.py::test_prompt_cache_support tests/sdk/llm/test_prompt_caching_cross_conversation.py
  3. Optional live check: run any multi-turn Gemini conversation and inspect metrics.token_usages[*].cache_read_tokens per call — it should grow with prompt_tokens instead of freezing at the static system+tools prefix.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes

  • Anthropic caching is untouched — the change only narrows which models receive explicit markers.
  • No public API change; downstream (agent-canvas, OpenHands, cloud) pick this up on the next SDK bump with no client changes.
  • A global LiteLLM kill-switch (disable_anthropic_gemini_context_caching_transform) exists but was rejected as the fix because it also disables Anthropic explicit caching; gating at the SDK feature table is the surgical
    equivalent.

Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:8317a84-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-8317a84-python \
  ghcr.io/openhands/agent-server:8317a84-python

All tags pushed for this build

ghcr.io/openhands/agent-server:8317a84-golang-amd64
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-golang-amd64
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-golang-amd64
ghcr.io/openhands/agent-server:8317a84-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:8317a84-golang-arm64
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-golang-arm64
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-golang-arm64
ghcr.io/openhands/agent-server:8317a84-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:8317a84-java-amd64
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-java-amd64
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-java-amd64
ghcr.io/openhands/agent-server:8317a84-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:8317a84-java-arm64
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-java-arm64
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-java-arm64
ghcr.io/openhands/agent-server:8317a84-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:8317a84-python-amd64
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-python-amd64
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-python-amd64
ghcr.io/openhands/agent-server:8317a84-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:8317a84-python-arm64
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-python-arm64
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-python-arm64
ghcr.io/openhands/agent-server:8317a84-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:8317a84-golang
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-golang
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-golang
ghcr.io/openhands/agent-server:8317a84-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:8317a84-java
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-java
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-java
ghcr.io/openhands/agent-server:8317a84-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:8317a84-python
ghcr.io/openhands/agent-server:8317a84715b4f1712c8250d573fee2e58007dedd-python
ghcr.io/openhands/agent-server:3585-gemini-prompt-caching-regression-explicit-cache-control-markers-3090-freeze-cache-at-static-prefix-614-cost-python
ghcr.io/openhands/agent-server:8317a84-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 8317a84-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 8317a84-python-amd64) are also available if needed

@VascoSch92 VascoSch92 added the run-eval-50 Runs evaluation on 50 SWE-bench instances label Jun 9, 2026
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/llm
   llm.py75610885%529, 545, 578, 861–862, 865–869, 871, 879–881, 885, 902–903, 907, 909–910, 912–914, 1037, 1160, 1353, 1362–1364, 1463, 1474, 1515, 1527–1529, 1532–1535, 1541, 1599, 1610, 1653, 1666–1668, 1671–1674, 1680, 1859–1864, 1980–1981, 2316–2317, 2326, 2332, 2337, 2378, 2380–2385, 2387–2404, 2407–2411, 2413–2414, 2420–2429, 2486, 2488
openhands-sdk/openhands/sdk/llm/utils
   model_features.py64198%38
TOTAL29638840271% 

@juanmichelini
juanmichelini self-requested a review June 9, 2026 17:36

@juanmichelini juanmichelini left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

…it-cache_control-markers-3090-freeze-cache-at-static-prefix-614-cost
@juanmichelini
juanmichelini marked this pull request as ready for review June 9, 2026 17:37

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ QA Report: PASS

The SDK now suppresses explicit cache_control markers for Gemini prompt formatting while preserving Anthropic explicit caching behavior.

Does this PR achieve its stated goal?

Yes. I exercised the SDK's public LLM/message formatting path before and after the PR with a multi-turn, non-adjacent system/last-user conversation: on main, Gemini models reported prompt caching active and emitted 2 cache_control markers; on this PR, the same Gemini models reported prompt caching inactive and emitted 0 markers. The same PR run still emitted 2 markers for claude-sonnet-4-20250514, so the Anthropic path remains active.

Phase Result
Environment Setup make build completed and installed the uv-managed workspace successfully
CI Status ⚠️ PR Description Check is failing; several Agent Server build/push jobs and coverage were still in progress when checked
Functional Verification ✅ Public SDK LLM formatting behavior verified before/after; no functional issues found
Functional Verification

Test 1: Gemini explicit cache markers are removed

Step 1 — Reproduce / establish baseline without the fix:
Ran git checkout --detach origin/main && uv run python - <<'PY' ... with a script that imports LLM, Message, and TextContent, builds a 4-message Gemini conversation, calls format_messages_for_llm(), and counts cache_control keys.

Relevant output:

{
  "gemini-2.5-pro": {
    "cache_control_markers": 2,
    "is_caching_prompt_active": true
  },
  "gemini-3.1-pro-preview": {
    "cache_control_markers": 2,
    "is_caching_prompt_active": true
  },
  "litellm_proxy/gemini-3.1-pro-preview": {
    "cache_control_markers": 2,
    "is_caching_prompt_active": true
  }
}

This confirms the regression shape exists on the base branch: Gemini prompt caching is active and the formatted payload contains explicit markers.

Step 2 — Apply the PR's changes:
Checked out PR commit 8317a84715b4f1712c8250d573fee2e58007dedd.

Step 3 — Re-run with the fix in place:
Ran the same SDK script on the PR commit.

Relevant output:

{
  "gemini-2.5-pro": {
    "cache_control_markers": 0,
    "is_caching_prompt_active": false
  },
  "gemini-3.1-pro-preview": {
    "cache_control_markers": 0,
    "is_caching_prompt_active": false
  },
  "litellm_proxy/gemini-3.1-pro-preview": {
    "cache_control_markers": 0,
    "is_caching_prompt_active": false
  }
}

This confirms the PR delivers the stated SDK behavior: Gemini no longer receives explicit cache_control markers even when caching_prompt=True.

Test 2: Anthropic explicit prompt caching still works

The same before/after script included claude-sonnet-4-20250514 as a control model. On the PR commit it returned:

{
  "claude-sonnet-4-20250514": {
    "cache_control_markers": 2,
    "is_caching_prompt_active": true
  }
}

This confirms the PR did not disable explicit prompt caching for the Anthropic model path.

Unable to Verify

I did not run a live Gemini request or inspect real cache_read_tokens billing behavior because that requires provider credentials/cost-bearing external LLM calls. This QA verified the SDK request-formatting behavior that determines whether explicit cache markers are sent. Future QA guidance in AGENTS.md could document whether QA agents are allowed to spend Gemini credits and which environment variable should be used for live cache-metric checks.

Issues Found

None from functional QA.

This review was created by an AI agent (OpenHands) on behalf of the user.

@VascoSch92
VascoSch92 merged commit c07f24c into main Jun 9, 2026
38 of 39 checks passed
@VascoSch92
VascoSch92 deleted the 3585-gemini-prompt-caching-regression-explicit-cache_control-markers-3090-freeze-cache-at-static-prefix-614-cost branch June 9, 2026 18:55
@ak684 ak684 mentioned this pull request Jun 10, 2026
pengzhang-create pushed a commit to Pyromind-Dynamics/software-agent-sdk that referenced this pull request Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

run-eval-50 Runs evaluation on 50 SWE-bench instances

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gemini prompt caching regression: explicit cache_control markers (#3090) freeze cache at static prefix (~6–14× cost)

3 participants