Core Concepts

Request Compression (the Proxy)

LeanCTX works on two planes. The read path compresses what your agent reads; the wire path is an optional local proxy that compresses every request to the model — system prompt, full history and tool results — before it is sent, with prompt-cache safety and measured spend. You do not need a separate request-compression proxy on top.

LeanCTX works on two planes. Most of the docs describe the read path (agent → data): file reads, code search and shell output, compressed before your agent ever sees them. This page is about the other plane — the wire path (agent → model): an optional local proxy that compresses every request before it is sent to the provider. Read-side savings trim what enters the context window; wire-side savings trim every request you pay for, turn after turn.

What the proxy is

A local reverse proxy that sits between your AI tool and the model provider — Anthropic (/v1/messages), OpenAI (/v1/chat/completions and /v1/responses), Gemini, plus a Codex WebSocket bridge. It binds to 127.0.0.1 only, authenticates every request, and forwards your tool's own provider key verbatim — it never injects credentials of its own. It is compiled into the default binary; you just turn it on.

Local proxy vs. team gateway: this page describes the proxy at laptop scale — one person, loopback bind, your own provider key. The same wire path can serve a whole org as the AI Gateway (team mode): non-loopback bind with mandatory per-person Bearer keys, org-held upstream credentials, Postgres metering, an org model catalog (/v1/models) and signed policy gates. Everything below applies to both; the gateway adds the multi-seat layer on top.

What it compresses

On every request, the proxy compresses the parts that grow without bound: the system prompt, the full conversation history, and tool results. This is the request-side counterpart to read-side compression: the same engine that shrinks a file read also shrinks the payload leaving for the model. After 10–15 turns a chat can carry 100K+ tokens of history with every new request — the proxy is what keeps that in check automatically, instead of asking you to start fresh chats.

Prompt-cache safe by design

Naive request compression breaks provider prompt caching (Anthropic, OpenAI), which would cost more than it saves. The proxy avoids that: history pruning and a cold-prefix repack only rewrite the parts of the request that are not a live cache prefix, so the cache stays warm and the discount is preserved. Compression that would invalidate a hot cache is skipped.

Active prompt-cache breakpoints v3.8.14

Beyond preserving a cache the client set up, the proxy can create one a raw API client left on the table. With proxy.cache_breakpoint enabled (env LEAN_CTX_PROXY_CACHE_BREAKPOINT, off by default), it adds a single ephemeral cache_control marker to the system field of an Anthropic request — but only when the client set none of its own. A large, stable system prompt then bills later turns at the cached rate instead of full price every turn.

It is Anthropic-only by construction (OpenAI and Gemini cache prefixes automatically and ignore the marker, so those paths stay byte-unchanged), deterministic (a pure function of the body, so the prefix it creates is itself byte-stable), never adds a second breakpoint, and is skipped below Anthropic's minimum cacheable size so it never churns bytes for no cache. Every injection is counted on the breakpoints_injected gauge in /status — a pure win signal, never against the cache-safe ratio.

A companion measurement, proxy.cache_aligner (on by default — it is measurement-only and strictly cache-safe), scans each unanchored system prompt for cache-busting volatile tokens — today's date, a fresh UUID, a git SHA — and reports how many it found (volatile_fields_detected) so you can quantify how much prompt-cache your prompt leaks. The request body is never mutated; set it to false to drop the per-request scan.

proxy.cache_policy — cache-economics, also on by default — adds the diagnosis layer: every anchored turn is classified by why it missed the prompt-cache (cold start, TTL lapse, or prefix change) and surfaced as cumulative /status gauges, paired with a net-cost gate that refuses to re-seed a prefix too small to ever be cached. Both halves are strictly safe — the telemetry never touches the body and the gate only makes a repack more conservative — so they ship on out of the box.

Deterministic structural crushing v3.8.14

Tool results are often array-heavy JSON — API responses, kubectl get -o json, DB dumps, RAG chunks — that repeat the same keys and values on every row. The json_crush engine factors that redundancy out: every key shared by all items is hoisted to a _defaults block and only per-row deviations are kept, so the result is exactly reconstructible and never inflates. The output is a pure function of the input (candidate keys and value frequencies walk ordered sets), so it is byte-stable and never leaks hash-map order — the determinism that keeps the prompt-cache prefix warm.

On the read and shell path the same core powers the generic JSON fallback and an opt-in mode for otherwise-verbatim data commands (gh api, jq, curl JSON) via crush_verbatim_json, firing only when it at least halves the payload. A dropped high-entropy column (timestamps, UUIDs) stays recoverable through a content-addressed ctx_expand handle, so a datum is never lost.

The same columnar core now reaches beyond JSON. A CSV/TSV crusher hoists every column that repeats a single value across all rows into a _const block and keeps the rest positionally; a YAML crusher folds YAML into the compact JSON form and factors its shared structure. Both are deterministic and never inflate, and on their lossy path any dropped high-entropy column stays recoverable through the same ctx_expand handle. The ctx_compare tool previews any pipeline before you trust it — original vs the exact bytes lean-ctx would emit, with token counts and a line diff.

Adaptive aggressiveness v3.8.14

Compression that is too aggressive shows itself: the agent keeps pulling back the originals it dropped. LeanCTX treats ctx_expand/ctx_retrieve re-fetches as exactly that signal and, combined with read/run correction loops, dials a session's compression down to Lite (3+ signals) then Off (5+), recovering only when the pressure clears. The level is server state that feeds future decisions — never part of any tool-output body, so output stays deterministic.

Cross-provider effort control — cache-safe by design

The proxy can pin one reasoning-effort level across every provider with a single setting (proxy.effort = minimal | low | medium | high). LeanCTX translates that one level into each provider's native parameter — OpenAI reasoning_effort / reasoning.effort, Anthropic output_config.effort, and Gemini thinkingConfig (thinkingLevel on 3.x, thinkingBudget on 2.5) — so you dial reasoning depth, and the reasoning-token bill that comes with it, once instead of per tool and per model.

Crucially the level is a constant for the whole conversation, not a per-turn decision — and that is deliberate. Providers list a change in reasoning effort as a cache-invalidation cause (OpenAI), and Anthropic breaks its message-cache breakpoints when the thinking configuration changes between turns. So "effort routing" that flips the level turn-by-turn quietly throws away the 50–90% prompt-cache discount — usually costing more than the reasoning tokens it saves. LeanCTX keeps the level byte-stable across turns, so the cached prefix stays warm and only the model's reasoning depth changes.

It is opt-in and conservative: off (the default) is a strict no-op; it never overrides an effort the client set itself; it only ever touches models that accept the parameter, so it can never turn a working request into a 400. On Anthropic it dials only a request that already asked for adaptive thinking, and on Gemini it skips 2.5 flash-lite (thinking off by default) and never sends both thinking fields — so it never adds reasoning cost you didn't request. lean-ctx proxy status reports the active level and how many requests were steered per provider.

Turn it on

lean-ctx proxy enable      # config flag + autostart service + endpoint wiring (port 4444)
lean-ctx proxy status      # requests, compression ratio, tokens saved, measured USD spend
lean-ctx proxy disable     # restore the original endpoint

enable wires your AI tool's ANTHROPIC_BASE_URL / OPENAI_BASE_URL to the local proxy and installs the autostart service. The proxy is opt-in: until you enable it, LeanCTX only touches the read path. A Claude Pro/Max subscription authenticates against api.anthropic.com directly and is deliberately not routed; set ANTHROPIC_API_KEY to route Claude through the proxy.

Measured, not estimated, spend

Because every response flows back through the proxy, it reads the real billed tokens from upstream — including cache reads/writes and reasoning tokens — and reports your actual provider bill per model. Token savings shown are request-side (tokens removed before forwarding); lean-ctx proxy status is the single source of truth for "where is my traffic going, and what did it cost".

Gateway cost headers v3.9.2 — when a corporate gateway or LiteLLM sits between the proxy and the model provider, it may report the billed USD in a response header. The proxy reads x-litellm-response-cost by default and any custom header via proxy.cost_response_header. Body-reported cost (OpenRouter usage.cost) always beats the header; junk values are silently dropped. This gives the proxy measured-accuracy spend even when it cannot parse the provider's native usage response.

The same engine, callable from your app

The proxy exposes POST /v1/compress — a deterministic, prompt-cache-friendly messages-in / messages-out contract. It is the same compression you get from the published SDK, so you can compress a chat array yourself before it reaches any model:

# pip install lean-ctx-sdk
from lean_ctx import compress
messages = compress(messages, model="claude-sonnet-4")

LiteLLM wire compatibility v3.9.0

POST /v1/compress speaks LiteLLM's prompt-compression guardrail contract: the response includes tokens_before, tokens_after and compression_ratio, so lean-ctx plugs into an existing LiteLLM proxy as its compression backend — no config changes on the client side.

CCR round-trips through LiteLLM's agentic loop: compressed tool results carry a hash=<24hex> marker, and GET /v1/retrieve/{hash} resolves it back to the verbatim original. The model asks for an expand, LiteLLM routes the retrieve call, and the full content returns — one hop, no MCP required on the client.

# Compress through LiteLLM's guardrail pre-call hook
POST http://localhost:4444/v1/compress
Content-Type: application/json

{ "messages": [...], "model": "claude-sonnet-4" }

# Response includes the LiteLLM-compatible fields:
# { "messages": [...], "tokens_before": 12400, "tokens_after": 3100, "compression_ratio": 0.75 }

# Retrieve a CCR-referenced original
GET http://localhost:4444/v1/retrieve/a1b2c3d4e5f6a1b2c3d4e5f6

Do I need a separate request-compression proxy?

No. A standalone request-compression proxy and the LeanCTX proxy occupy the same plane — the wire path — and LeanCTX already ships it, prompt-cache-safe and metered. Run lean-ctx proxy enable and you have read-side and wire-side compression from one local binary, with one verifiable savings ledger across both.

Where to go next