{"id":22805,"date":"2026-07-28T21:52:03","date_gmt":"2026-07-28T21:52:03","guid":{"rendered":"https:\/\/scannn.com\/prompt-caching-in-agents-earendil\/"},"modified":"2026-07-28T21:52:03","modified_gmt":"2026-07-28T21:52:03","slug":"prompt-caching-in-agents-earendil","status":"publish","type":"post","link":"https:\/\/scannn.com\/lv\/prompt-caching-in-agents-earendil\/","title":{"rendered":"Prompt Caching In Agents | EARENDIL"},"content":{"rendered":"\n<div>\n<p>Large language models are often thought of like functions: send in some text,<br \/>\nreceive some text.  That is a useful abstraction, but it ignores one of the most<br \/>\nimportant parts of running a coding agent: most of the input is the same as last<br \/>\ntime.  In other words we mostly append to it.<\/p>\n<p>A coding agent sends the model its system prompt, tool definitions, project<br \/>\ninstructions, conversation history, tool calls, and tool results.  On the next<br \/>\nturn it sends almost all of that again, plus a small amount of new material.<br \/>\nOnce a session has grown to tens or hundreds of thousands of tokens, recomputing<br \/>\nthe whole prompt for every turn is slow and expensive.<\/p>\n<p>Prompt caching is what makes this somewhat economic, but it is also quite<br \/>\nfragile.  A changed tool definition, a model switch or a provider routing<br \/>\ndecision can turn what one would expect to be a cheap incremental request into a<br \/>\nfull replay of the context.<\/p>\n<p>For coding agents, cache behavior is therefore not just an implementation detail or<br \/>\noptimization.  It affects latency, cost, tool design, session design, and even<br \/>\nwhich product features should be made available.<\/p>\n<h2>What a KV Cache Contains<\/h2>\n<p>A transformer processes a prompt in two broad phases.  During <strong>prefill<\/strong>, it<br \/>\nreads the input tokens and computes attention state for them.  During <strong>decode<\/strong>,<br \/>\nit produces new tokens one at a time.<\/p>\n<p>At each attention layer, every processed token produces a key and a value. These<br \/>\nare not quite like key-value lookups in a hash table: both are arrays of<br \/>\nnumbers, usually floats or lower-precision quantized values.  When processing a<br \/>\nnew token, the model compares that token&#8217;s <strong>query<\/strong> with the earlier <strong>keys<\/strong><br \/>\nto determine how relevant each earlier token is.  It then uses those relevance<br \/>\nscores to form a weighted mixture of the corresponding <strong>values<\/strong>.  In that<br \/>\nsense, a key is what the model matches against, while a value is the information<br \/>\nit retrieves (but the lookup is fuzzy rather than &#8220;returning a single exact<br \/>\nmatch&#8221; like a dictionary lookup.)<\/p>\n<p>Those keys and values are retained so that the next generated token can attend<br \/>\nto everything that came before without recomputing the earlier tokens. This<br \/>\nretained state is the <strong>KV cache<\/strong>.<\/p>\n<p>Conceptually, a request looks like this:<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"22\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">request 1:\n\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">[system]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">[tools]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">[user]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">[assistant]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">[tool result]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 750ms\">[user]\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 855ms\">&lt;--------------------- prefill --------------------&gt;\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 960ms\">                       |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1065ms\">                       K and V tensors per token and layer\n\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1170ms\">request 2:\n\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1275ms\">[system]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1380ms\">[tools]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1485ms\">[user]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1590ms\">[assistant]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1695ms\">[tool result]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1800ms\">[user]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1905ms\">[new]\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 2010ms\">&lt;---------------- reusable prefix ----------------&gt;<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 2115ms\">&lt;---&gt;\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 2220ms\">                                                    |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 2325ms\">                                                    new work\n<\/span><\/code><\/pre>\n<p>The real representations are more complicated, model-specific, and &#8220;quite&#8221;<br \/>\nlarge.  The important property is that they correspond to a particular token<br \/>\nprefix.  Two prompts that mean the same thing but tokenize differently do not<br \/>\nshare a KV cache.  If a token changes in the middle, everything after that token<br \/>\nis a different continuation.<\/p>\n<p>Prompt caching extends the lifetime of this state beyond one generation.  When<br \/>\nthe next API request from the coding agent begins with the same tokens, the<br \/>\ninference system can reuse the stored work for the matching prefix and prefill<br \/>\nonly the new suffix.  So far, the theory.<\/p>\n<h2>Where the Cache Lives<\/h2>\n<p>In order for a cache to work it needs to be stored somewhere, and it needs to be<br \/>\naddressable.  There are two broad ways inference systems make KV caches<br \/>\navailable to a later request.<\/p>\n<p>The simpler approach is <strong>session affinity<\/strong>.  It works by keeping the KV cache<br \/>\non or near the GPU that computed it, and routing the next request back to the same<br \/>\nworker.  A session ID or prompt-cache key becomes a trivial routing hint and so<br \/>\nyou can potentially even deal with this problem on the HTTP load balancer level<br \/>\nwithout having to look into the payload.<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"8\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">request(session-42) <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">--&gt; router <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">--&gt; worker 7 <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">--&gt; GPU 7 KV cache\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">next(session-42)    <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">--&gt; router <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 750ms\">--&gt; worker 7 <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 855ms\">--&gt; GPU 7 KV cache\n<\/span><\/code><\/pre>\n<p>This avoids moving a very large cache over the network.  It is fast when it<br \/>\nworks, but it constrains scheduling.  The selected worker can become overloaded,<br \/>\nrestart, or evict the entry.  A router may also decide that balancing the fleet<br \/>\nis more important than preserving one session&#8217;s cache.  It is however a very<br \/>\nattractive solution because it works with little extra deployed infrastructure<br \/>\nand hardware.<\/p>\n<p>The other approach is to <strong>distribute the cache<\/strong>.  KV blocks can be stored in<br \/>\nanother memory tier or made available across workers, so a request is not tied<br \/>\nas tightly to one GPU.<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"9\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">                         +--------------------+\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">request <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">--&gt; scheduler <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">--&gt;| worker 3 \/ GPU 3   |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">             |           +--------------------+\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">             |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 750ms\">             +----------&gt; distributed KV blocks\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 855ms\">             |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 960ms\">             +----------&gt; worker 9 \/ GPU 9\n<\/span><\/code><\/pre>\n<p>That improves scheduling flexibility and recovery, but moving, indexing, and<br \/>\nretaining KV blocks is itself a systems problem.  Implementations mix GPU memory,<br \/>\nhost memory, local storage, remote storage, prefix-aware routing, and eviction<br \/>\npolicies in different ways.<\/p>\n<p>To put KV caches into perspective: they can be large but they are in some ways<br \/>\nsmaller than one would assume.  With various tricks, the size of KV caches can<br \/>\nbe reduced to a handful of gigabytes, even for long conversations.<\/p>\n<h2>Caches and Prefixes<\/h2>\n<p>Pi sessions are trees, not lists.  <code>\/tree<\/code> can move the active conversation back<br \/>\nto an earlier point and continue along another branch.  A rewind can discard the<br \/>\nactive suffix without deleting it from the session file.  A new branch can share<br \/>\nmost of the old context, a little of it, or effectively none of it.  This design<br \/>\nis not unique to Pi, quite a few coding agents have something at least<br \/>\nconceptually similar.  Even if you do not represent the session as a tree,<br \/>\nit&#8217;s not uncommon for agents to have some form of rewinding.<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"9\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">                             +-- E -- F  another branch\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">                             |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">session S: root <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">-- A <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">-- B <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">-- C <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 750ms\">-- D  current branch\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 855ms\">                   |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 960ms\">                   +-- Z  branch near the start\n<\/span><\/code><\/pre>\n<p>All three branches can have the same Pi session ID.  From the router&#8217;s<br \/>\nperspective they are one session.  From the prompt cache&#8217;s perspective they are<br \/>\nthree token sequences with only partial prefix overlap.<\/p>\n<p>If the cache keeps reusable prefix blocks, jumping from <code>D<\/code> to <code>F<\/code> may still<br \/>\nreuse <code>root -&gt; C<\/code>.  If it only retains the hottest continuation, if the shared<br \/>\nblocks were evicted, or if the request is routed elsewhere, the hit can be much<br \/>\nsmaller.  Jumping to <code>Z<\/code> may preserve only the system prompt and initial tool<br \/>\ndefinitions even though it starts from <code>A<\/code>.  The precise cache management<br \/>\nbehavior here depends greatly on the providers.<\/p>\n<p>The reverse can also happen. <code>\/fork<\/code> or a new session can produce a new session<br \/>\nID while carrying over a large amount of identical context.  A routing system<br \/>\nthat isolates caches by session key may fail to notice that useful overlap.<\/p>\n<p>The reusable prefix determines what work can be cached.  Session identity merely<br \/>\nhelps the infrastructure find likely content.  On some systems the routing key<br \/>\nis crucial to manage caches, on others it&#8217;s merely an optimization.<\/p>\n<h2>Explicit vs Automatic Prefix Caching<\/h2>\n<p>Provider APIs expose caching in two main styles.<\/p>\n<p>Anthropic&#8217;s traditional interface uses explicit <code>cache_control<\/code> points.  The<br \/>\nclient marks boundaries after stable parts of the request, such as the system<br \/>\nprompt, tool definitions, or the latest cacheable conversation content.  The<br \/>\nserver can then write or look up the prefix ending at those points.  The boundary<br \/>\nis explicit, but reuse still requires the content before it to match.  Not only<br \/>\nare the cache points explicit, so is the pricing.  You pay for cache writes, and<br \/>\nyou get to choose for how long which comes at different price points.<\/p>\n<p>Other APIs use automatic prefix caching.  The client sends the request normally,<br \/>\nand the provider finds a reusable prefix without client-placed breakpoints.  A<br \/>\nprompt-cache key or session header may improve routing or grouping, but it does<br \/>\nnot make different prefixes equal.<\/p>\n<h2>Why Tool Loadouts Trash Caches<\/h2>\n<p>Tool definitions usually appear before the conversation and they are &#8220;folded&#8221;<br \/>\ninto the system prompt internally.  Their names, descriptions, and JSON schemas<br \/>\nare model input just like any other text.  Adding one tool, removing one,<br \/>\nchanging its schema, or even serializing the tools in a different order can move<br \/>\nthe first mismatch close to the start of the prompt.<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"16\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">turn 1: <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">[system]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">[read]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">[write]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">[bash]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">[conversation...........]\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 750ms\">turn 2: <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 855ms\">[system]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 960ms\">[read]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1065ms\">[write]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1170ms\">[bash]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1275ms\">[deploy]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1380ms\">[conversation...]\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1485ms\">                                   |\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1590ms\">                                   old conversation is now\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1695ms\">                                   after a mismatch\n<\/span><\/code><\/pre>\n<p>This is a common surprise with plugin systems and MCP-style tool catalogs.<br \/>\nLoading a tool only when it becomes relevant sounds efficient because fewer<br \/>\nschemas are sent initially.  On most models, however, the newly expanded loadout<br \/>\ninvalidates the cached conversation that follows it.  Saving a few tool-schema<br \/>\ntokens can cause tens of thousands of conversation tokens to be processed again.<\/p>\n<p>Some newer model APIs support <strong>additive tool loading<\/strong>.  A tool can become<br \/>\navailable at a specific tool result inside the transcript instead of being<br \/>\ninserted into the original tool list.  The old prefix remains unchanged:<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"6\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">[system]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">[initial tools]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">[conversation]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">[new tool]<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">[next turn]\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">&lt;--------- cached prefix -----------&gt;\n<\/span><\/code><\/pre>\n<p>Pi nowadays supports this for models with native deferred-tool mechanisms.  When<br \/>\nan extension makes a purely additive change with <code>setActiveTools()<\/code>, Pi records<br \/>\nthe added names on the tool result.  For supported Anthropic models it uses<br \/>\ndeferred definitions and a <code>tool_reference<\/code> and for supported OpenAI models it<br \/>\nemits the corresponding tool-search items.  Other models get a safe fallback: Pi<br \/>\nsends the complete active tool list on the next request, which works<br \/>\nfunctionally but may wipe the prompt cache.<\/p>\n<p>The word <strong>additive<\/strong> matters as removing tools, replacing one loadout with<br \/>\nanother, or changing prompt snippets still changes earlier input.  An extension<br \/>\nthat rebuilds the system prompt, shuffles tool order, injects timestamps, or<br \/>\nchanges active tools every turn can accidentally defeat caching for the entire<br \/>\nsession.<\/p>\n<p>Extensibility means Pi cannot guarantee cache stability on behalf of every<br \/>\nextension.  We can provide cache-friendly mechanisms; extensions still have to<br \/>\nuse them and from what we have seen, for many extensions cache efficiency is an<br \/>\nafterthought.  This is, in part, because when you pay on a fixed subscription the<br \/>\nassociated cost with cache misses is not quite as obvious.<\/p>\n<h2>Interruptions and TTLs<\/h2>\n<p>Some important prompt caches have short default lifetimes.  Anthropic&#8217;s default<br \/>\nfive-minute cache is particularly important because it is shorter than many<br \/>\nnormal coding activities.  If you go to sip a coffee when using Fable, and you<br \/>\ncome back 10 minutes later, a single &#8220;say hi&#8221; message will cost you more money<br \/>\nthan you expect.<\/p>\n<p>That&#8217;s because while the user may think of a coding session as continuously<br \/>\nactive, the inference provider sees a sequence of isolated requests:<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"4\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">model request <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">--&gt; run tests for 7 minutes <\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">--&gt; model request\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">                  no cache traffic here\n<\/span><\/code><\/pre>\n<p>A long build, a test suite, lunch, a meeting, or simply stopping to review a<br \/>\ndiff can outlive the cache.  The next request contains the same prompt, but the<br \/>\nstored KV state is gone and the prefix is billed again as input.<\/p>\n<p>Since Pi is currently not a permitted harness on Anthropic&#8217;s subscription we&#8217;re<br \/>\nfollowing the 5 minute default that Anthropic recommends for API users.  However<br \/>\nfrom looking at Claude Code&#8217;s codebase we know that for their own subscription<br \/>\nusers, they are increasing that cache timeout to one hour.  The increased cost<br \/>\nof this however often is not worth it, when you need to pay API token prices.<\/p>\n<p>But you can opt into this.  Some providers such as Anthropic expose longer<br \/>\nretention controls.  For supported direct APIs, Pi users can set<br \/>\n<code>PI_CACHE_RETENTION=long<\/code> to request them.  That is still only a request: Pi<br \/>\ncannot force a gateway to retain an entry, prevent eviction under memory<br \/>\npressure, or keep a cache alive while no model request is being made.<\/p>\n<h2>The Price of a Miss<\/h2>\n<p>Providers usually price uncached input, cache writes, and cache reads<br \/>\ndifferently.  Cache reads are commonly discounted because the expensive prefill<br \/>\nwork has already happened.  Cache writes can carry a premium because the provider<br \/>\nis promising to retain state for later use.<\/p>\n<p>Imagine a coding session with 100,000 tokens of history followed by a short new<br \/>\nrequest as the Fable example above.  When the cache works, almost all of that<br \/>\nhistory is charged at the lower cache-read price.  Only the small amount of new<br \/>\nmaterial needs to be processed at the regular input price and potentially<br \/>\nwritten to the cache.<\/p>\n<p>When the cache misses, the provider has to process the entire 100,000-token<br \/>\nhistory again at the regular input price.  It may also charge to write that<br \/>\nhistory back into the cache.  This is why a short request such as <code>continue<\/code> can<br \/>\nbe surprisingly expensive after a cache expires.  In a long coding session,<br \/>\nre-reading the old input can cost much more than generating the next answer.<\/p>\n<p>Caching also has a chance to create non-obvious incentives.<\/p>\n<p>The user should want high hit rates because they reduce latency and price.  An<br \/>\ninference operator that owns the GPUs should want them too: less prefill work<br \/>\nmeans more requests served with the same hardware.  A well-designed cached-token<br \/>\ndiscount can align both sides while leaving the operator with better margins.<\/p>\n<p>A gateway or reseller can have a different incentive.  If it earns revenue from<br \/>\ninput tokens billed at the uncached rate, a cache miss can produce a larger<br \/>\ncustomer invoice than a hit.  Whether that also produces more profit depends on<br \/>\nits upstream costs, contracts, and who operates the cache.  In a badly aligned<br \/>\nstack, the party responsible for routing may not bear the full cost of misses,<br \/>\nwhile the party billing the user earns more revenue when they happen.<\/p>\n<p>That does not mean providers sabotage caches but it means cache performance<br \/>\nshould be observable.  Users should not have to infer that only from a<br \/>\nsurprisingly large bill.  Understanding if something odd is going on with caches<br \/>\ncan be an important insight.<\/p>\n<p>Strict cache adherence also means less flexibility for a gateway to route you to<br \/>\nthe best option in-between turns.  You might want to take a cache hit to continue<br \/>\nwith a different model which from that point onwards might be more economical,<br \/>\nor it might be the case that you might be better off load balancing to another<br \/>\nprovider.<\/p>\n<h2>Why Pi Does Not Prune Aggressively<\/h2>\n<p>Now that you&#8217;ve made it this far, you probably have an idea why Pi does not prune<br \/>\ntool calls.  It is tempting to control cost by continuously deleting old tool<br \/>\nresults or rewriting history and sometimes that is necessary, especially near<br \/>\nthe context-window limit.  But as we have learned, pruning has a cache cost of<br \/>\nits own.<\/p>\n<p>Deleting content from the middle changes the prefix at the deletion point.  All<br \/>\nsurviving conversation after it may need to be processed again.  The immediate<br \/>\ncost of rewriting a long cached context can exceed the future savings from<br \/>\nremoving a small number of cheaply cached tokens.<\/p>\n<p>A rough break-even comparison is:<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"4\"><code class=\"language-text\"><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">one-time rewrite cost\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">    ~= surviving tokens after the edit * (uncached price - cache-read price)\n\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">future savings per turn\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">    ~= pruned tokens * cache-read price\n<\/span><\/code><\/pre>\n<p>This is not only an accounting question as old tool results often contain the<br \/>\nevidence the model used to make later decisions.  Removing them can degrade<br \/>\nbehavior even if a summary preserves the gist.<\/p>\n<p>Pi therefore prefers a stable, append-oriented transcript and does not treat<br \/>\nevery old token as waste.  Compaction is available when context pressure<br \/>\njustifies a lossy rewrite.  Because compaction deliberately creates new context<br \/>\nrather than accidentally re-billing an unchanged prompt, Pi treats it as a cache<br \/>\nreset rather than a cache failure in its session statistics.<\/p>\n<p>The goal is not the smallest possible prompt but the best trade-off among model<br \/>\ncontext, cache reuse, latency, and price.<\/p>\n<p>Simultaneously there can be a case for pruning too.  If you are working with<br \/>\nproviders that do not discount you for good cache usage, or it&#8217;s for whatever<br \/>\nreason not possible to get high cache rates, it might be preferable to prune.<br \/>\nIt definitely improves the opportunity for the router to balance between<br \/>\ndifferent backends as caches are not transferable.<\/p>\n<h2>What Pi Can and Cannot Do<\/h2>\n<p>Pi works to keep stable inputs stable.  It passes a consistent session ID and<br \/>\nprovider-specific cache hints, places explicit cache points for APIs that<br \/>\nrequire them, records cache-read and cache-write usage, and supports<br \/>\nmessage-anchored additive tool loading where models allow it.  Its default<br \/>\ntranscript behavior also avoids gratuitously rewriting old context.<\/p>\n<p>Pi cannot control every layer after the request leaves the machine.  It cannot<br \/>\nchoose a provider&#8217;s eviction policy, extend a cache beyond what the API permits,<br \/>\nkeep a particular GPU alive, or guarantee that a gateway honors affinity.  It<br \/>\nalso cannot preserve a prefix that an extension changes.<\/p>\n<p>What it can do is make cache health visible.<\/p>\n<p>The interactive footer shows cumulative cache reads and writes as <code>R<\/code> and <code>W<\/code>,<br \/>\nplus <code>CH<\/code> for the latest request&#8217;s cache-hit rate.  The <code>\/session<\/code> command gives<br \/>\na fuller view: total cached and uncached input, cumulative hit rate, cost, and<br \/>\nan estimate of tokens and dollars re-billed by <a href=\"https:\/\/github.com\/earendil-works\/pi\/blob\/34f3719a942ecbf3e6d23e67098f47ba2867de0a\/packages\/coding-agent\/src\/core\/cache-stats.ts#L50-L90\">significant cache misses<\/a>.<\/p>\n<pre class=\"code-reveal\" data-code-reveal=\"\" data-reveal-steps=\"14\"><code><span class=\"code-reveal__step\" style=\"--reveal-delay: 120ms\">Messages\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 225ms\">Total: 178\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 330ms\">User: 6\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 435ms\">Assistant: 58\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 540ms\">Tools: 114 calls, 114 results\n\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 645ms\">Tokens\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 750ms\">Input: 7,129,883\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 855ms\">  Cached: 6,776,832 (95.0%)\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 960ms\">  Uncached: 353,051\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1065ms\">Output: 30,013\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1170ms\">Total: 7,159,896\n\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1275ms\">Cost\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1380ms\">Total: $6.054\n<\/span><span class=\"code-reveal__step\" style=\"--reveal-delay: 1485ms\">Cache Re-billed: $0.728 (161,744 tokens, 2 misses)\n<\/span><\/code><\/pre>\n<p>Users who want misses called out as they happen can enable <strong>Show cache miss<br \/>\nnotices<\/strong> in <code>\/settings<\/code>, corresponding to <code>showCacheMissNotices<\/code> in<br \/>\n<code>settings.json<\/code>.  Pi then inserts a warning after a significant miss, including<br \/>\nthe estimated re-billed tokens and cost.  When it can observe a model switch or<br \/>\nan idle gap beyond the usual short TTL, it says so.  For other misses it reports<br \/>\nthe fact without pretending to know what happened inside the provider.<\/p>\n<h2>Common Reasons for Worse Cache Performance<\/h2>\n<p>When a session&#8217;s cache-hit rate looks wrong, the usual causes are:<\/p>\n<ol>\n<li><strong>Idling.<\/strong> A command, review, or conversation pause exceeds the provider&#8217;s retention window.<\/li>\n<li><strong>Model or provider switches.<\/strong> KV state is model-specific and generally does not move across providers.<\/li>\n<li><strong>Branch navigation.<\/strong> <code>\/tree<\/code>, rewinds, forks, and alternate branches can change the active token sequence even when the session ID remains the same.<\/li>\n<li><strong>Compaction or manual history rewriting.<\/strong> These intentionally replace part of the prompt and establish a new prefix.<\/li>\n<li><strong>Tool and reasoning level changes.<\/strong> Adding, removing, reordering, or editing tool definitions changes an early part of the request unless the model supports message-anchored loading and the change is purely additive. Reasoning level changes usually have the same effect.<\/li>\n<li><strong>Dynamic system prompts.<\/strong> Timestamps, random values, changing project context, and extension-provided prompt snippets can invalidate everything after them.<\/li>\n<li><strong>Extension context transforms.<\/strong> An extension that modifies old messages or provider payloads can make an apparently stable Pi transcript unstable on the wire.<\/li>\n<li><strong>Provider routing and eviction.<\/strong> The prompt can be identical and still miss because the relevant KV blocks are no longer available where the request lands.<\/li>\n<\/ol><\/div>\n<p><a href=\"https:\/\/earendil.com\/posts\/prompt-caching\/?utm_source=tldrai\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Large language models are often thought of like functions: send in some text, receive some text. That is a useful abstraction, but it ignores one of the most important parts of running a coding agent: most of the input is the same as last time. In other words we mostly append to it. A coding [&hellip;]<\/p>\n","protected":false},"author":16,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[143],"tags":[],"class_list":["post-22805","post","type-post","status-publish","format-standard","hentry","category-ai"],"_links":{"self":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22805","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/users\/16"}],"replies":[{"embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/comments?post=22805"}],"version-history":[{"count":0,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/posts\/22805\/revisions"}],"wp:attachment":[{"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/media?parent=22805"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/categories?post=22805"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scannn.com\/lv\/wp-json\/wp\/v2\/tags?post=22805"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}