Cache Manager
Anthropic prompt-cache manager for OpenCode V2: per-session 5m / 1h / hybrid TTL, keepalive pings for idle sessions, and a sidebar cache status
0
0
20.0
Multi-signal model
4 hours ago
2026-10-21
Install and configure
opencode.jsonWrites to this project's opencode.json — applies to this repository only.
opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@noppu-labs/opencode-cache-manager@0.2.2"]
}Writes to ~/.config/opencode/opencode.json — applies to every project.
~/.config/opencode/opencode.json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@noppu-labs/opencode-cache-manager@0.2.2"]
}If you want to modify the plugin locally, install it into the project and reference the local path.
shell
pnpm add -D @noppu-labs/opencode-cache-managerOpenCode loads npm dependencies through its embedded runtime on startup and caches them locally — no manual global install needed.
An OpenCode plugin that manages the Anthropic prompt cache from inside the TUI. It lets you pick the cache TTL (the standard 5-minute cache, the 1-hour extended cache, or a hybrid of the two) per session, and it can keep an idle session's cache warm with small hidden pings so it doesn't expire while you are away.
OpenCode marks cache breakpoints automatically (tools, system prompt, the last message) but always sends the default ttl: "5m". There is no config knob for it. This plugin rewrites the outgoing request after OpenCode's own cache policy runs, so the TTL can be changed per session without forking the provider package.
When a session sits idle past its TTL, the next prompt has to write the whole context into the cache again at 1.25x (5m) or 2x (1h) the base input price, instead of reading it at about 0.1x. Keepalive avoids that by sending a ping shortly before the cache would expire.
Published to npm under two names with identical contents:
@noppu-labs/opencode-cache-manageropencode-cache-manager
The three modes
| Mode | Tools + system | Messages | Write cost |
|---|---|---|---|
5m (default) |
5m ephemeral | 5m ephemeral | 1.25x base input |
1h |
1h ephemeral | 1h ephemeral | 2x base input |
hybrid |
1h ephemeral | 5m ephemeral | mixed |
Reads cost about 0.1x base input in every mode. hybrid exists because Anthropic requires 1-hour breakpoints to come before any 5-minute ones in the request: putting the long TTL on the stable prefix (tools + system) and keeping the rolling message tail on 5m keeps the cheap write cost for the part of the context that changes every turn.
Requests in 1h or hybrid mode also get the anthropic-beta: extended-cache-ttl-2025-04-11 header merged in.
Install
Add the package to the plugins array in ~/.config/opencode/opencode.jsonc. OpenCode installs it automatically at startup (no manual npm install):
{
"$schema": "https://opencode.ai/config.json",
"plugins": [
"@noppu-labs/opencode-cache-manager@^0.2.0"
// or: "opencode-cache-manager@^0.2.0" -- same package, unscoped name
]
}
Local checkout instead
If you prefer running from a clone (or while hacking on it), symlink the repo's src/ directory into the plugins directory. OpenCode resolves server.ts / tui.ts / rpc.ts from the directory itself, so the link has to point at src/, not the repo root:
git clone https://github.com/noppu-labs/opencode-cache-manager.git
ln -s "$(pwd)/opencode-cache-manager/src" ~/.config/opencode/plugins/anthropic-cache-ttl
Do not keep both the npm spec and a local copy installed at the same time, or the plugin loads twice.
Usage
Run /cache-manager in the TUI. The command also appears in the palette as "Cache Manager".
With no argument it opens a picker with two sections:
Session (only shown inside a session)
TTL mode 1h · global
Keepalive on · global
Pings per idle 3
Min context 50k · global
Reset session to global
Global
TTL mode 1h
Keepalive on
Pings per idle 5
Min context 50k
Session rows that show · global inherit the global value; a row without it has a session override. Picking a row opens a list of values; the numeric settings also offer "Custom…" for a typed value.
With an argument, the command edits the current session only, never the global settings:
/cache-manager 5m -> session TTL mode 5m
/cache-manager 1h -> session TTL mode 1h
/cache-manager hybrid -> session TTL mode hybrid
/cache-manager keepalive on|off -> turn keepalive on or off for this session
/cache-manager pings 3 -> at most 3 pings per idle stretch (0-100)
/cache-manager min 50k -> minimum context size to ping (50000 or 50k)
/cache-manager reset -> drop the session's overrides
/cache-manager status -> toast with the session's effective settings
Typed arguments need an open session. Global settings are changed from the picker's Global section or in the plugin options (see Settings).
Session overrides and global settings persist in plugin storage across restarts.
The server side registers the same command, so explicit arguments also work headless; the reply is posted as a synthetic message instead of a toast. With no argument the server-side command toggles the session's TTL mode between 5m and 1h.
Per-model override
Model variants can force a TTL regardless of the session or global mode, via the x-cache-ttl sentinel header:
"variants": [
{ "id": "high", "settings": { "reasoningEffort": "high" } },
{ "id": "high-1h", "settings": { "reasoningEffort": "high" }, "headers": { "x-cache-ttl": "1h" } }
]
The plugin reads the header before the request goes out, applies that TTL, and strips the header. opencode.jsonc at this repo's root is a full sample provider config with a complete set of -1h variants for four Claude models.
Keepalive
Keepalive sends a hidden request shortly before a session's cache expires. The request reads the cached prefix, which restarts the TTL. A ping costs roughly a cache read of the session's context, which is much less than the cold re-write you pay when the cache has expired.
- Hidden. The ping is a transient request that uses the session's context. Nothing is added to the conversation history, and the model is told to reply
OKwithout using tools. - Schedule. The ping goes out 4 minutes after the last request on a 5m cache, and 50 minutes after it on a 1h cache.
hybridfollows the 5m schedule, because its message tail is cached for 5 minutes. A ping is itself a request, so it restarts the clock and the next ping is scheduled the same way. - Idle stretches. Any real request on the session (your prompt, a tool-loop request, compaction) cancels the pending ping and resets the ping count to 0.
- Limit. After
maxPingspings (default 5) in one idle stretch, keepalive stops and the cache expires. Failed pings count towards the limit. - Never late. A ping is never sent after the cache would have expired. A failed ping is retried after 30 seconds, but only if it can still land before the original expiry.
- Hit or miss. A ping that reads at least 90% of the context from cache is a hit. If it reads less, it is a miss: keepalive stops for that session until the next real request, so a provider that doesn't reuse the cache can't turn into a stream of paid pings.
Keepalive skips:
- subagent (child) sessions
- sessions waiting on a tool call, such as a running subagent or a long build; they are pinged again once the next request has gone out
- sessions whose context is below
minContextTokens(default 50,000) - sessions with keepalive turned off
- sessions where no cache activity has been seen, such as sessions on non-Anthropic models
All eligible top-level sessions are kept warm at once, not only the one in view. Timers live in memory and are not restored when OpenCode restarts.
Detected TTL. The schedule uses the TTL the provider actually granted, read from the cache-write breakdown in the response usage. Some providers (for example Claude Code-backed ones) grant 1h regardless of the mode requested. If a response has both 5m and 1h writes, the shorter one wins. Reads don't change the TTL. When the provider gives no breakdown, the requested mode is used. The sidebar labels the source as detected or requested.
Cache hits for pings. A ping reuses the exact conversation bytes of the session's last agent request, so it hits the cache even when other plugins rewrite the history. Those bytes are kept in memory only, never written to disk.
Pings cost tokens. Each one is a real provider request that reads the whole context from cache. Keepalive saves money when you come back after a long gap and stay within the ping limit; it costs money for sessions you never return to.
Don't combine this with OpenCode's built-in
warming: true. Both would ping the same session, so you would pay for double the pings. Use one or the other.
Settings
Plugin options set the defaults. They live in the plugin entry in ~/.config/opencode/opencode.jsonc:
{
"plugins": [
{
"package": "@noppu-labs/opencode-cache-manager",
"options": {
"defaultMode": "1h",
"keepalive": {
"enabled": true, // default true
"maxPings": 5, // default 5
"minContextTokens": 50000 // default 50000
}
}
}
]
}
| Option | Default | Meaning |
|---|---|---|
defaultMode |
"5m" |
TTL mode when nothing else is set |
keepalive.enabled |
true |
Whether keepalive runs |
keepalive.maxPings |
5 |
Pings per idle stretch |
keepalive.minContextTokens |
50000 |
Smallest context worth pinging |
The effective value of each setting is the first one set, in this order:
- the session's override (
/cache-managertyped argument, or the picker's Session section) - the global value stored from the picker's Global section
- the plugin option (
defaultModeorkeepalive.*) - the built-in default
A global value stored from the picker overrides the plugin option for the same setting, so changing defaultMode in opencode.jsonc has no effect once the picker has stored a global value for that setting.
Changes take effect immediately. Turning keepalive off cancels the pending ping, and lowering pings to or below the pings already used ends the run. A TTL mode change is picked up by the next request.
The x-cache-ttl model-variant header (see Per-model override) still wins over the session mode for the requests it is on.
Sidebar
Inside a session, the sidebar shows a Cache section. It ticks once a second from timestamps the server sends, so countdowns stay live without server traffic.
Cache
• status warm
expires 14:52 (52m)
ttl 1h (detected)
next ping 14:42 (42m)
pings 2/5 ✓ hit
statusiswarmwith a green bullet, amber once the pings are used up, andexpiredwith a red bullet once the cache has expired (thenexpired 12m agoreplaces theexpiresrow).expiresis the clock time and time left until the cache expires.ttlis the TTL being scheduled and where it came from (detectedorrequested). Before the first response, including right after a restart, the section readswaiting for first response.next pingis when the next ping goes out. It readsnoneonce the limit is reached.pings 2/5counts pings used in this idle stretch, followed by the last result:✓ hit,✗ missor✗ failed. No result is shown before the first ping.- When keepalive isn't running, a
keepaliverow says why:off · 31k < 50k(context below the minimum),off · disabled,waiting · session busy(waiting on a tool call), orstopped · last ping missed.
The section is hidden for subagent sessions and for sessions with no cache activity.
Upgrading from 0.1.x
Two changes break existing habits:
/cache-ttlis now/cache-manager. The palette entry is "Cache Manager" and the old command name is gone, with no alias.- Typed commands change only the current session.
/cache-ttl 1hused to set the global mode;/cache-manager 1hsets the mode for the session you are in. To change the global default, use the picker's Global section. A global mode stored by 0.1.x is kept and takes precedence overdefaultMode, so editingdefaultModealone will not change it; set it from the picker's Global section instead. There is nothing else to migrate.
Development
bun install
bun test # logic, keepalive engine, RPC, TUI command paths, package tarball
bun run typecheck
Layout:
src/
├── server.ts # server entry: wires the command, HTTP hooks and RPC handlers
├── tui.ts # TUI entry: palette + slash command, sidebar
├── rpc.ts # the RPC contract (settings, keepalive snapshot, events)
├── config/ # settings shape, defaults, option parsing, layer merging
├── constants/ # Anthropic header values, keepalive timings
├── models/ # shared types (TTL modes, usage, keepalive snapshot)
├── controllers/ # /cache-manager command and RPC handlers
├── middleware/ # http.request / http.response hooks
├── services/ # TTL rewrite, keepalive engine, ping alignment, usage parsing, settings store
├── ui/ # picker, typed command, sidebar
└── utils/ # small shared helpers
tests/ # mirrors src/
docs/ # design notes from the original build
The suite includes a package smoke test: it runs npm pack, then replays the exact entrypoint resolution OpenCode performs for a configured npm plugin (Bun.resolveSync("<name>/server" | "<name>/tui" | "<name>/rpc") against the extracted tarball) and checks the shipped files are byte-identical to the tested sources.
Releases are cut by pushing a v* tag; the release workflow tests, packs, publishes both npm names via OIDC trusted publishing (no npm token is stored), and opens a GitHub Release.
License
MIT -- see LICENSE.
Similar plugins
Cache Manager
opencode-cache-manager
Anthropic prompt-cache manager for OpenCode V2: per-session 5m / 1h / hybrid TTL, keepalive pings for idle sessions, and a sidebar cache status
Usage Monitor
opencode-usage-monitor
OpenCode v2 TUI sidebar plugin that displays API usage quotas for OpenAI, Z.AI (GLM), and DeepSeek providers
Peak Badge
opencode-peak-badge
OpenCode TUI plugin: live [PEAK]/[OFF-PEAK] badge for any model with configured peak hours. Provider-agnostic; ships with DeepSeek defaults.