#8956·jan

epic: prefix-cache discipline for Jan Agent

Author: thinhlpgCreated Sep 16, 2026Updated Sep 17, 2026

Jan Agent re-sends a few thousand identical leading tokens on every turn — system prompt, tool schemas, accepted history. Providers reuse that region only when the leading bytes are byte-identical to the previous request. Jan changes those bytes on nearly every turn, so most of that region is re-billed at full price.

This epic collects the changes that make the head of a Jan Agent request hold still.

Why the hit rate is the bill

DeepSeek published price card, peak rate, deepseek-flash:

cache hit     $0.006 / M   ▎
cache miss    $0.300 / M   ██████████████████████████████████████████████████   50×
output        $1.200 / M

A miss costs 50× a hit. So on a long agent session the hit rate — not the token count, not the model choice — sets the invoice:

100 calls · 40k prompt tokens each · 1k output tokens each

  no reuse    4.0M × $0.30  = $1.200   +  0.1M × $1.20 = $0.120   →   $1.320
  90% reuse   3.6M × $0.006 = $0.022   +  0.4M × $0.30 = $0.120
                                       +  0.1M × $1.20 = $0.120   →   $0.262

                                                          saving   −80.2%

On providers that bill cache writes (Anthropic, Bedrock), a prefix that is rewritten every turn is worse than no cache at all: a write costs 1.25× uncached input and a read 0.10×, so a written-never-read prefix is a 25% surcharge on the whole prompt.

Anatomy of a Jan Agent request today

        ┌───────────────── reusable prefix — should never change ─────────────────┐  ┌── tail ──┐
        │                                                                          │  │          │
 wire   │  "Today's date is 2026-09-16."  │  system prompt  │  memory block  │ tools │  │ history  │
        │              ▲                  │        ▲        │       ▲        │   ▲   │  │          │
        └──────────────┼──────────────────┴────────┼────────┴───────┼────────┴───┼───┘  └──────────┘
                       │                           │                │            │
              rotates at midnight,        replaced wholesale   recomputed     HashMap
              and differs per timezone      on every run      per user query  iteration
                                                                               order
              loop.rs:2022-2031           upstream.rs:312-321  loop.rs:2008-2021  upstream.rs:599-621
                   (#8957)                      (#8960)             (#8958)          (#8959)

Four independent writers above the cache line. Any one of them changing invalidates everything to its right.

What one turn looks like

mermaid
sequenceDiagram
    autonumber
    participant J as Jan Agent
    participant P as Provider cache

    Note over J: turn 1 · 09:00
    J->>P: [date=09-16][system][memory(q1)][tools] + history
    P-->>J: cold miss — whole prompt billed at full price

    Note over J: turn 2 · 09:01 — only the user text changed
    J->>P: [date=09-16][system][memory(q2)][tools] + history
    P-->>J: MISS again — memory block differs at byte ~2,400

    Note over J: turn 3 · next day, or a different timezone
    J->>P: [date=09-17][system][memory(q3)][tools] + history
    P-->>J: MISS at byte 0 — nothing after it can be reused

The correct shape is one cold miss per session, and a hit on every turn after it.

Sub-issues

# Change Where it breaks today
#8957 Move the date out of the first bytes of the system prompt loop.rs:2022-2031, context.rs:184-223
#8958 Stop per-query memory recall from rewriting the system prompt loop.rs:2008-2021
#8959 Make the advertised tool array byte-stable across runs upstream.rs:599-621, state.rs:68
#8960 Append system updates instead of replacing message 0 upstream.rs:312-321
#8961 Trigger compaction on a context ratio, not on a provider error loop.rs:2585-2631
#8962 Keep a canonical transcript separate from the provider projection compaction.rs:78-106, loop.rs:2621-2628
#8963 Declare prompt-cache capability per provider route state.rs:28-45 carries api_type, nothing carries a cache policy
#8964 Read and surface provider cache counters events.rs:288-303
#8965 Byte-level prefix regression tests loop.rs:3722-3734 — assertions index the object graph
#8966 A permission for who may write above the cache line project.rs:197-203 governs tools, not prompt composition

#8957–#8960 are the cheap, high-value ones. #8964 is the one that keeps working after the rest are done, because a provider cache is best-effort and client discipline alone cannot prove a hit.

Prior art — four other agents already paid for these lessons

Project What it does What it cost to learn
DeepSeek-Reasonix Canonical transcript separate from the provider projection; compaction preflight at compact_ratio 0.80; live hit rate in the TUI designed in
DeepSeek Harness systemPromptUpdate: 'in-history' — a changed prompt is appended as a new system node, never swapped at the head; per-route capability flags reports 98.09% hit rate over 155.9M prompt tokens
oh-my-pi Richest cache tooling of any of these — per-provider markers, 24h retention, cacheRead/cacheWrite cost split #7324: a date/cwd line closing the system block missed 11,239 of 20,968 requests (54%). #11897: two prompt injectors → 80.2% overspend, $130.02 where $25.69 was due, 98.7% of input tokens written to cache and never read
OpenAI Codex The strictest client of the five: content-hashed prefix item IDs, an explicit prompt_cache_key, volatile state quarantined in an <environment_context> tail fragment, and a request comparison the compiler forces you to update still reports a 78% hit rate and ~$210 lost in one session (#35925), because the provider cache is best-effort

Codex is the load-bearing lesson: a perfect client still loses money if it cannot see the provider's cache. That is why #8964 is not optional.

Jan is also the only one of the five that must satisfy three different cache models in one build — DeepSeek's automatic prefix units, OpenAI-compatible implicit reuse, and local llama.cpp — which is exactly why #8963 exists.

Acceptance for the epic

  • A session of N turns on a DeepSeek route shows one cold miss and a hit on every subsequent turn, measured from prompt_cache_hit_tokens / prompt_cache_miss_tokens.
  • Restarting the app and resuming a session does not produce a cold miss caused by client-side ordering.
  • Crossing midnight mid-session does not invalidate the prefix.
  • Compaction costs exactly one scheduled prefix break, not a rejected request followed by a retry.
  • The live hit rate is visible to the user during a run.
  • A regression test fails if any new field is added above the cache line without a decision about it.

Not in scope

  • Router-side and cross-provider concerns (a mid-session provider switch, usage normalization in the facade). Those are tracked separately.
  • llama.cpp cache_prompt / KV-cache reuse for local models — related, but a different mechanism from remote prefix caching.

References

All Jan code references are pinned to main@9fd9903.