Skip to main content
ChatCLI ships a set of optimizations that keep token consumption in check during long /agent and /coder sessions. This page explains what runs out of the box and which knobs are available when the default behaviour doesn’t fit your workflow.
Every optimization on this page works across all supported providers (Anthropic direct, Bedrock, OpenAI, xAI, ZAI, MiniMax, Moonshot (Kimi), Google AI, Ollama, Copilot, GitHub Models, OpenRouter, OpenAI Responses, StackSpot). Providers with explicit prompt caching (Anthropic, Bedrock Anthropic) or automatic prefix caching (OpenAI, xAI) benefit most.

The real problem

A poorly calibrated ReAct loop can turn a trivial question into huge token bills. Without the optimizations below, a query like β€œwho won the last Flamengo game” can easily burn 20k+ tokens because:
  1. The full system prompt is re-sent on every turn (uncached).
  2. Tool definitions (15+ JSON schemas) are also re-sent per turn.
  3. Nothing breaks the loop when the model repeats the same tool_call without converging.
  4. Large @webfetch bodies land in the context raw.
Aggregated, each turn carries 4-8k tokens of pure overhead Γ— 3-5 turns in a typical session = 12-40k tokens before we even count the useful answer.

1. Structured prompt caching

Anthropic caching (and OpenAI/xAI prefix auto-caching) works by prefix: a breakpoint only hits when every byte before it is identical to a prior request. The golden rule is simple β€” stable blocks first, volatile blocks last. The system prompt is assembled exactly that way: Stable prefix (cached, cache_control: ephemeral): Volatile suffix (no cache marker β€” changes every turn):
Why ordering matters (a fixed defect): previously the workspace+memory block carried cache_control but held the wall-clock timestamp to the second plus hint-driven retrieval β€” both volatile β€” near the top of the prompt. That guaranteed a cache miss on that block every turn and poisoned every cached block below it (/context attachments, pinned skills, MCP catalog): you paid cache creation (1.25Γ—) each time and never earned a read. The timestamp now lives in its own trailing block, and the volatile memory left the prefix. The genuinely stable blocks form a contiguous, cacheable prefix.
Each stable block carries cache_control: ephemeral for Anthropic-family providers (respecting the 4-breakpoint cap, with automatic coalescing). For providers with prefix auto-caching (OpenAI, xAI), the stable ordering ensures cache hits naturally. Chat follows the same ordering; being tool-less, it does not pull memory on demand (see section 7).

Cache on tool definitions

The last tool definition sent to Anthropic also carries cache_control: ephemeral, turning the whole tools array into a cacheable prefix. In a /coder session with 15 coder tools + 2 web tools, this is ~19KB of JSON that stops being re-tokenized every turn.

Cache visibility

OpenAI cached tokens require no opt-in β€” prompt caching is automatic on gpt-4o and newer (including o-series and GPT-5), triggered when the prompt prefix is β‰₯1,024 tokens, with hits served in 128-token increments. For streaming Chat Completions, ChatCLI sets stream_options: {include_usage: true} so the terminal usage chunk arrives before [DONE]; for the Responses API, usage rides on the response.completed SSE event and needs no extra flag.
Verify real session impact via /cost β€” cache hits appear as a separate line. The chat envelope also shows N↑ M↓ on the right border for every provider that surfaces usage, including all OpenAI APIs.

2. Stagnation early-exit

When the model enters a reflection loop β€” emitting exactly the same batch of tool_calls turn after turn without new information β€” ChatCLI detects it and breaks the loop.

How it works

Each turn, the tool_calls fingerprint (name + normalized args, order-independent, truncated SHA-256) is computed. Three consecutive turns with the same fingerprint β†’ the loop is stopped with a clear message.

Parameters

The fingerprint is order-independent: [read A, read B] and [read B, read A] hash to the same value, so cosmetic re-ordering doesn’t fool the detector.

3. Smart chat ↔ agent routing

Not every query deserves a full ReAct loop. Conversational / factual questions (β€œwhat is a mutex?”, β€œdifference between slice and array”) are answered by a single chat-mode turn. The classifier identifies trivial queries from lexical signals:
  • Question leaders (what, why, how does, explain, …)
  • Absence of task verbs (create, build, run, fix, …)
  • No workspace references (@file, @git, paths, code extensions)
  • Short length + a question mark

Modes

/coder is never rerouted β€” that mode exists for structured tasks. Even seemingly trivial questions there are treated as work requests.

Example


4. Smart auto-save in WebFetch

@webfetch is tuned to never dump giant pages into the context. See WebFetch & WebSearch for complete documentation.

Escape hatch

Auto-save always persists the full pre-filter body to $CHATCLI_AGENT_TMPDIR, and the return carries:
The agent typically issues a read_file against that path with the right start/end, paying only for the lines that matter.

5. Slimmer system prompts

Mode-specific prompts were condensed without semantic loss β€” every original rule remains, only redundancy and repeated examples were removed: Because these prompts live in the core cache block, cache-enabled providers (Anthropic/Bedrock/OpenAI) see the reduction only on the first turn of a session. Providers without caching save those tokens on every turn.

6. Proactive tool result compaction

Old tool results (file reads, search, git-diff, etc.) are progressively compressed in the history to keep the payload lean. See Tool Result Management for the full pipeline. Defaults are conservative to protect multi-turn workflows with cross-references (large refactors, review sessions). Users who want to squeeze harder can tune:
For chat/lookup sessions where speed and low tokens matter more than long-term recall, try:

7. Pull-first memory (index + recall)

Pushing the whole memory into the system prompt every turn doesn’t scale: the cost grows with the store size and is re-sent each turn. ChatCLI now defaults to a pull model: it injects only a stable digest and lets the agent pull detail on demand via the @memory recall tool. Controlled by CHATCLI_MEMORY_MODE: The index is stable (turn-independent, no timestamp), so it lives in the cached prefix (section 1) and is size-capped regardless of store size. @memory recall uses the full retrieval stack (HyDE + vector cosine search + keyword extraction), so pulled detail matches the quality of the old push. Proactive auto-recall (CHATCLI_MEMORY_AUTORECALL, on by default) adds a tiny hint-driven top-3-facts block per turn β€” but it rides in the uncached trailing region next to the wall-clock context, never in the stable digest, so the cached prefix stays byte-identical and none of the savings above are given back. See Bootstrap and Memory for details.

Measured impact

Measured against a real store of 500 facts (MEMORY.md ~32KB, fact index ~270KB): βˆ’87.7% on the per-turn memory block β€” and unlike full (capped by CHATCLI_MEMORY_RETRIEVAL_BUDGET), the index does not grow as memory grows.
Chat is tool-less by design and cannot pull on demand: there index automatically degrades to full, and only off suppresses memory. The active mode is shown in /config memory.
In index mode the agent/coder no longer sees the whole memory automatically β€” it must call @memory recall. The index gives it the β€œmap” (what exists) so it knows what to pull. If you notice the agent missing context it should recall, switch back to CHATCLI_MEMORY_MODE=full (the section 1 cache savings still apply).

Measuring impact

Run your session normally and check /cost at the end:
Signals that the optimizations are active and working:
  • Cache read > 0 and growing per turn β†’ structured caching is hitting the prefix.
  • Few/no FORMAT ERROR in logs β†’ reminders are holding format on smaller models.
  • Turns with tool_calls = 0 followed by quick completion β†’ early-exit detected convergence.
  • [auto-saved: response was N bytes] markers in @webfetch results β†’ inline budget is protecting context.

Variable summary

Every variable on this page in one place: