[FEATURE] Mods: prompt caching for $.model.complete (content blocks + cache_control)
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
A mod that asks a model the same kind of question again and again over a growing conversation cannot use the prompt cache. $.model.complete sends the prompt as one string with no cache_control, so every call is billed as uncached input, however much of it the previous call already sent.
Measured on 2.1.280 (sonnet): two identical 30,000-token prompts sent back to back both report cache_read_input_tokens: 0 and cache_creation_input_tokens: 0. The API caches only with cache_control, top-level or on a block, and ModelCompleteRequest has no field for either. And because prompt is a single string, even a breakpoint on it would hit only when the whole prompt repeats: the cache is looked up at block boundaries, so a prompt that grows by appending never matches its previous version.
Claude Code's own auto-mode classifier does this for itself: it builds its request with breakpoints after its rules, CLAUDE.md and the transcript, so each classification reads the previous one's prefix from the cache. A mod making the same kind of side judgement has no way to.
Proposed Solution
Let $.model.complete take cacheable input, for example:
type ModelCompleteRequest = {
model: string
system?: string | TextBlock[]
prompt: string | TextBlock[]
// maxTokens, effort, timeoutMs as today
}
type TextBlock = { type: 'text'; text: string; cache_control?: { type: 'ephemeral'; ttl?: '5m' | '1h' } }
or, smaller: prompt: string | string[] (one text block each) plus cache?: boolean, which sets top-level automatic caching. Either way the engine keeps its own TTL policy for side requests, and usage already reports the cache split, so nothing else has to change.
Priority
Medium - Would be very helpful
Feature Category
API and model interactions
Use Case Example
We maintain a mod (taskcut) that moves compaction to sub-task boundaries. Claude Code compacts when the window fills, which is usually in the middle of a task: live detail gets summarised away while detail from work finished an hour ago is kept. The case it is built for is a list of tasks handed over in one message and worked through in one long turn nobody watches.
How it uses $.model.complete:
- Past a context floor (35% by default), after each step of the main loop (
turn.step), it asks a judge model one question: does the work move on here from a finished piece to another? The last piece of a list, a step that asks the person something, and work still in progress are all "no". - On "yes" it ends the turn before the next request, calls
$.session.compact()and submitsContinue., which is what a person watching would do with Esc,/compactand "continue". - The judge reads what the auto-mode classifier reads: the person's messages, the tool calls other than read-only lookups, CLAUDE.md, never any tool output, plus the step being judged. The prompt is built in a fixed order (rules, CLAUDE.md, the conversation oldest first, the step last) and never trimmed, so each prompt starts with everything the previous one sent. That is the shape the classifier caches.
What it costs today: replaying a real 160-step session through the judge, a judgement is 11,132 input tokens at the median and 25,124 at most, all uncached, while 97% of every prompt is a prefix of the previous one. With a cacheable $.model.complete, nearly all of each judgement would bill at the cache-read rate.
Additional Context
- Claude Code 2.1.280, macOS, subscription login.
- Related feedback in #91870 asked for
result.usageandeffortonmodel.complete, both of which have since shipped; caching has not been asked for.