OpenCode plugin for LLM prompt caching, token savings, and KV cache reuse
Stops prompt-cache busting when agent sessions change across agents, providers, models, projects, and workspaces.
Stable system blocks go first. Volatile handoff, memory, date, and workspace state go later.
Boost prompt cache hit rates by 40β88%.
Zero config for OpenCode. Zero content knowledge. CLI-agnostic core.
English | δΈζ
LLM providers such as DeepSeek, Anthropic, OpenAI, and Google use prefix-match KV caching: the beginning of the prompt must match a previous request before the provider can reuse cached key-value states.
Agent prompts usually have the opposite shape. Dynamic state is injected first, while stable but expensive blocks come later:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β β‘ HANDOFF block (changes every session) β β Cache BUSTED
β β‘ REMEMBER / MEMORY (changes throughout day) β β Cache BUSTED
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
CLAUDE.md (changes weekly) β β Never reached
β β
Agent definitions (static) β β Never reached
β β
MCP / Skills / Tools (static) β β Never reached
β β‘ currentDate (changes daily) β
β β‘ Memory injection (changes per query) β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Result: 0% cache reuse across sessions. Every session recomputes the entire system prompt from scratch, even though 70-90% of it hasn't changed.
This gets worse in real agent workflows:
| Scenario | What breaks the prefix cache | What this project does |
|---|---|---|
| Multi-agent runs | Planner/fixer/explorer handoffs change before shared rules | Tracks stability per provider__model__agent scope |
| Multi-provider/model routing | DeepSeek, Anthropic, OpenAI, and model variants warm differently | Keeps scoped cache metrics and warm hashes instead of one global guess |
| Project/workspace/worktree switching | Repo paths, memories, dates, and workspace context move around | Moves stable repo/tool/agent blocks before volatile workspace state |
| Large MCP/tool/skill prompts | Huge static tool schemas are recomputed when dynamic text is first | Splits large blocks and pins stable hashes near the front |
| Handoff and memory-heavy sessions | Conversation state changes every task | Keeps handoff/memory useful but moves it after cacheable blocks |
It does not compress prompts, summarize instructions, or read prompt content. It only hashes system blocks, learns which blocks are stable, and reorders them so provider prefix caching can work.
agent-cache-optimizer reorders the system prompt at runtime:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
CLAUDE.md (stable) β Cached β
β β
Agent definitions (stable) β Cached β
β β
MCP / Skills / Tools β Cached β
β β
Tool definitions β Cached β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β‘ currentDate β
β β‘ HANDOFF / REMEMBER / MEMORY β
β β‘ Memory injection β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Stable blocks first β prefix survives session changes β 40-88% cache reuse.
{
"plugin": ["agent-cache-optimizer"]
}Add to ~/.config/opencode/opencode.json. OpenCode auto-installs from npm on next startup.
# Or via CLI
opencode plugin agent-cache-optimizer --globalRestart OpenCode β done. Zero config. Works immediately for DeepSeek, Anthropic, OpenAI, and any provider with prefix-match KV caching.
Claude Code does not expose a runtime prompt-transform hook, so this package loads as a Claude Code companion plugin for status and cache-friendly prompt guidance rather than automatic system block reordering.
From this checkout:
PLUGIN_DIR="$(pwd)"
# Validate plugin metadata without starting a model session
claude plugin validate "$PLUGIN_DIR"
# Confirm Claude Code can see the session-only plugin
claude --plugin-dir "$PLUGIN_DIR" plugin list
claude --plugin-dir "$PLUGIN_DIR" plugin details agent-cache-optimizer
# Start Claude Code with the plugin loaded for this session
claude --plugin-dir "$PLUGIN_DIR"Inside Claude Code, use /agent-cache-status to inspect local optimizer status.
For cache-friendly Claude sessions, structure stable CLAUDE.md content first
and use Claude Code's --exclude-dynamic-system-prompt-sections option when you
are using the default system prompt.
Codex CLI loads reusable workflows as skills or plugins. This package ships a
Codex plugin manifest and the agent-cache-status skill, so it can be exposed
through a local Codex plugin marketplace during development.
From this checkout:
PLUGIN_DIR="$(pwd)"
MARKETPLACE_ROOT="${HOME}/.local/share/agent-cache-optimizer-codex"
# Copy this plugin into a local Codex marketplace layout.
mkdir -p "$MARKETPLACE_ROOT/plugins/agent-cache-optimizer"
mkdir -p "$MARKETPLACE_ROOT/.agents/plugins"
rsync -a --delete \
--exclude .git \
--exclude node_modules \
"$PLUGIN_DIR"/ "$MARKETPLACE_ROOT/plugins/agent-cache-optimizer"/
cat > "$MARKETPLACE_ROOT/.agents/plugins/marketplace.json" <<'JSON'
{
"name": "agent-cache-optimizer-local",
"interface": {
"displayName": "Agent Cache Optimizer Local"
},
"plugins": [
{
"name": "agent-cache-optimizer",
"source": {
"source": "local",
"path": "./plugins/agent-cache-optimizer"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
JSON
# Register the marketplace and install the plugin.
codex plugin marketplace add "$MARKETPLACE_ROOT"
codex plugin add agent-cache-optimizer@agent-cache-optimizer-local
# Confirm Codex installed and enabled it.
codex plugin list --jsonInside Codex, invoke $agent-cache-optimizer:agent-cache-status or use
/skills to select the skill. Start a new Codex thread after installing or
updating the plugin so the skill list is refreshed.
# OpenCode: check plugin is loaded
opencode debug config | grep agent-cache-optimizer
# OpenCode: watch reorder activity in real time
tail -f ~/.cache/opencode/agent-cache-optimizer/diag.log
# Claude Code: inspect the session-only plugin
claude --plugin-dir "$(pwd)" plugin details agent-cache-optimizer
# Codex CLI: confirm the skill is visible in the prompt input
codex debug prompt-input "Use agent-cache-optimizer status" \
| grep 'agent-cache-optimizer:agent-cache-status'agent-cache-optimizer status # text dashboard
agent-cache-optimizer status --json # JSON for scriptsThe dashboard reports active scopes, stable hashes, real provider cache hit rate, and estimated savings.
The plugin never reads the content of your prompts. It only hashes each system block and tracks which hashes stay the same vs change across calls.
Session 1: [H1, CLAUDE-A, AGENT-X, TOOLS-V1, DATE-1, MEM-1]
Session 2: [H2, CLAUDE-A, AGENT-X, TOOLS-V1, DATE-2, MEM-2]
Session 3: [H3, CLAUDE-A, AGENT-X, TOOLS-V1, DATE-2, MEM-3]
After 3 observations:
H1/H2/H3 at position 0 change every time β score 0.0 (dynamic)
CLAUDE-A at position 1 never changes β score 1.0 (stable)
AGENT-X at position 2 never changes β score 1.0 (stable)
...
Large blocks (>4KB) are split at structural boundaries β JSON arrays, Markdown headings, XML elements, and long lists β using a robust brace-depth parser that handles arbitrary nesting and fenced code blocks.
Cold-start heuristics detect volatile metadata patterns (currentDate,
session ID, timestamp) and cap their scores to prevent structural
boosts from misclassifying them as stable.
ββββββββββββ
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ β STABLE β
β HANDOFF β β CLAUDE β β TOOLS β β MEMORY β βββΆ β CLAUDE β
β (dynamic)β β (stable) β β (stable) β β (dynamic)β β TOOLS β
ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ ββββββββββββ
β‘ β
β
β‘ β DYNAMIC β
β HANDOFF β
β MEMORY β
ββββββββββββ
| Phase | Trigger | Method |
|---|---|---|
| Cold start | First 2 calls per scope | Universal position/size/structure heuristics |
| Warm | 3+ calls | Hash-based stability scores |
The cold-start heuristics use only structural signals (position, size, delimiters, line density) β no keyword matching, no config awareness. This means the plugin works immediately with any agent setup.
Real cache hit rates are tracked from OpenCode provider events β no
estimation needed. cache-metrics.json records per-scope and
total cacheReadTokens, cacheWriteTokens, and cacheHitRate.
All session and message IDs are content-hashed for privacy.
Tested on a realistic OpenCode orchestrator prompt (~25KB system prompt):
| Scenario | Cacheable prefix | Improvement |
|---|---|---|
| Original (no reorder) | 0 KB (0%) | β |
| Cold start (heuristics) | 21.8 KB (88%) | +88% |
| Content-addressed (v0.5) | 52.9 KB (100%) | +100% |
Production results (155 observations, deepseek-v4-pro):
| Phase | S | U | D | Stable KB |
|---|---|---|---|---|
| Pre-v0.5 (position-based) | 1 | 0 | 24 | ~2 KB |
| v0.5 (content-addressed) | 25 | 0 | 0 | 52.9 KB |
The v0.6.1 tag documents provider-reported cache metrics from a local
OpenCode run, filtered with the same command used during testing:
cat ~/.cache/opencode/agent-cache-optimizer/diag.log \
| grep hit \
| grep 2026-06-26 \
| grep deepseek__deepseekThe filtered run contained 309 cache-hit metric samples:
| Scope | Samples | Hit rate observed | Latest metric |
|---|---|---|---|
deepseek-v4-pro / orchestrator |
86 | 98.0-98.3% | 98.1% hit rate, 39,084,800 cache-read tokens |
deepseek-v4-flash / fixer |
196 | 86.1-99.2% | 99.0% hit rate, 21,218,432 cache-read tokens |
deepseek-v4-flash / explorer |
27 | 0.0-90.3% | 90.3% hit rate after warm-up, 1,229,696 cache-read tokens |
The explorer scope shows cold-start behavior: the first sample was 0.0%
before the provider cache warmed, and later samples reached 90%+ hit rate.
| Platform | Status | Adapter |
|---|---|---|
| OpenCode | β Plugin | src/index.ts (native) |
| Claude Code | β Companion plugin + guidelines | adapters/claude-code.md |
| Codex | β Companion plugin + skill | .codex-plugin/plugin.json + skills/ |
| Gemini CLI | π Planned | Google context caching |
Check any config file for patterns that bust the KV cache:
bash scripts/check-cache-friendly.sh CLAUDE.md
bash scripts/check-cache-friendly.sh --opencode
bash scripts/check-cache-friendly.sh --allQ: Does this change my prompts? A: Only the ORDER of system blocks. Content is never modified.
Q: Will it break my agent? A: No. The LLM sees the same blocks, just in a different order. System prompts are position-independent by design.
Q: Does it work with non-OpenCode agents? A: The core engine is CLI-agnostic. Adapters exist for OpenCode (plugin) and Claude Code (companion plugin + guidelines). Codex ships a companion plugin and status skill. Gemini CLI guidance is planned.
Q: What if my prompts change? A: The hash-based tracking adapts automatically. If a previously-stable block starts changing, its score drops and it moves to dynamic. If a new stable block is added, it converges to stable after a few observations.
Q: Does this work with Anthropic's prompt caching?
A: Yes β the chat.headers hook adds the prompt-caching-2024-07-31 beta
header automatically for Anthropic providers.
MIT β use it, fork it, ship it, star it β
Built with β€οΈ for the LLM CLI ecosystem. If this saved you tokens,
drop a β on GitHub β it helps more people find it.