@computermotivators/paperclip-claude-cache
v2026.5.2-9.1
Published
Paperclip adapter for Anthropic Claude API with explicit prompt caching (cache_control breakpoints)
Maintainers
Readme
@paperclipai/adapter-claude-cached
Paperclip adapter that calls the Anthropic Messages API directly with explicit
cache_control: { type: "ephemeral" } breakpoints on the system prompt and skills
blocks — guaranteeing cache hits where the standard claude_local (Claude Code) adapter
relies only on implicit caching.
Why this adapter?
| | claude_local | claude_cached |
|---|---|---|
| Runtime | Spawns claude CLI | Direct Anthropic SDK call |
| Caching | Implicit (best-effort) | Explicit cache_control |
| Tool ecosystem | Full Claude Code tools | Text/thinking only (extend for tools) |
| cachedInputTokens telemetry | Via Claude Code output parsing | ✅ Native from API response |
| Best for | Code-editing agents | Long system-prompt agents (Opus 4.x) |
Opus 4.5 list pricing (approximate):
- Input: $15.00 / MTok
- Cached read: $1.50 / MTok ← 10× cheaper
- Cache write: $18.75 / MTok (one-time per 5-min TTL window)
- Output: $75.00 / MTok
Installation
Drop this package into your Paperclip monorepo under packages/adapters/claude-cached/
and add it to pnpm-workspace.yaml if not already covered by a glob.
# pnpm-workspace.yaml
packages:
- packages/adapters/*Registration
1. Server (server/src/adapters/registry.ts)
import { execute as claudeCachedExecute, sessionCodec as claudeCachedSessionCodec, testEnvironment as claudeCachedTest } from "@paperclipai/adapter-claude-cached/server";
import { agentConfigurationDoc as claudeCachedDoc, models as claudeCachedModels } from "@paperclipai/adapter-claude-cached";
const claudeCachedAdapter: ServerAdapterModule = {
type: "claude_cached",
execute: claudeCachedExecute,
testEnvironment: claudeCachedTest,
sessionCodec: claudeCachedSessionCodec,
models: claudeCachedModels,
supportsLocalAgentJwt: true,
agentConfigurationDoc: claudeCachedDoc,
};
// Add claudeCachedAdapter to the adaptersByType map alongside existing adapters.2. UI (ui/src/adapters/registry.ts)
First, create ui/src/adapters/claude-cached/index.ts:
import type { UIAdapterModule } from "../types";
import { parseClaudeCachedStdoutLine, buildClaudeCachedConfig } from "@paperclipai/adapter-claude-cached/ui";
import { ClaudeCachedConfigFields } from "./config-fields";
export const claudeCachedUIAdapter: UIAdapterModule = {
type: "claude_cached",
label: "Claude (Cached API)",
parseStdoutLine: parseClaudeCachedStdoutLine,
ConfigFields: ClaudeCachedConfigFields,
buildAdapterConfig: buildClaudeCachedConfig,
};Then create ui/src/adapters/claude-cached/config-fields.tsx:
// Minimal config fields component — extend with your UI primitives as needed.
import React from "react";
import type { AdapterConfigFieldsProps } from "../types";
export function ClaudeCachedConfigFields({ values, set }: AdapterConfigFieldsProps) {
return (
<div>
<label>API Key
<input
type="password"
value={String(values["apiKey"] ?? "")}
onChange={e => set("apiKey", e.target.value)}
placeholder="${secrets.anthropic_api_key}"
/>
</label>
<label>Model
<input
value={String(values["model"] ?? "claude-opus-4-5")}
onChange={e => set("model", e.target.value)}
/>
</label>
<label>Max tokens
<input
type="number"
value={String(values["maxTokens"] ?? 8192)}
onChange={e => set("maxTokens", Number(e.target.value))}
/>
</label>
<label>Max turns per heartbeat
<input
type="number"
value={String(values["maxTurns"] ?? 10)}
onChange={e => set("maxTurns", Number(e.target.value))}
/>
</label>
</div>
);
}Then register in ui/src/adapters/registry.ts:
import { claudeCachedUIAdapter } from "./claude-cached";
// Add to adaptersByType map3. CLI (cli/src/adapters/registry.ts)
import { printClaudeCachedStreamEvent } from "@paperclipai/adapter-claude-cached/cli";
const claudeCachedCLIAdapter: CLIAdapterModule = {
type: "claude_cached",
formatStdoutEvent: printClaudeCachedStreamEvent,
};
// Add to adaptersByType mapAgent config example
{
"name": "Research Analyst",
"role": "analyst",
"adapterType": "claude_cached",
"adapterConfig": {
"apiKey": "${secrets.anthropic_api_key}",
"model": "claude-opus-4-5",
"maxTokens": 8192,
"maxTurns": 15,
"thinkingBudget": 0,
"timeoutSec": 0,
"graceSec": 15,
"systemPrompt": "You are {{agent.name}}, a research analyst at {{company.name}}. Your task: {{context.taskId}}. Wake reason: {{context.wakeReason}}."
}
}How caching works in this adapter
System message blocks sent to API
┌──────────────────────────────────────────────┐
│ Block 1: systemPrompt text │
│ cache_control: { type: "ephemeral" } ◄── CACHED (TTL 5min)
├──────────────────────────────────────────────┤
│ Block 2: Skills content (if skillsDir set) │
│ cache_control: { type: "ephemeral" } ◄── CACHED (TTL 5min)
├──────────────────────────────────────────────┤
│ Conversation history (user/assistant turns) │
│ (no cache_control — changes every turn) │
└──────────────────────────────────────────────┘On the first call in a 5-minute window: Anthropic writes the cache. On subsequent calls within 5 minutes: cache_read tokens are billed at $1.50/MTok instead of $15.00/MTok — a 10× reduction on the stable prefix.
The usage.cachedInputTokens field in AdapterExecutionResult is populated from
cache_read_input_tokens in the API response and fed to Paperclip's budget tracking.
Limitations
- No built-in tool dispatch (bash, file I/O, etc.). This is intentional — extend
execute.tswith a tool-call loop if you need tools. - Cache TTL is 5 minutes (Anthropic's limit). Agents that sleep longer than 5 minutes between heartbeats will miss the cache window.
- Conversation history is trimmed to the last 40 role-turns to avoid unbounded growth.
Adjust the slice constant in
execute.tsif needed.
