Skip to main content
Glama
dshakes

distil

by dshakes


๐Ÿš€ Use it now

One command sets you up and tells you what to do next:

pipx install distil-llm
distil onboard      # detects your agent + billing, wires the status line, prints a guided tour

It detects your environment (Claude Code ยท Codex ยท Gemini CLI; metered vs subscription) and hands you the exact commands. Or wrap your agent directly โ€” no config, no code change:

# Claude Code on a metered API key โ€” saves real $$:
distil wrap --expand -- claude

# Claude Code on a Pro/Max subscription โ€” flat-rate, ToS-safe (trims context, not $):
distil wrap --lossless-only -- claude

# Codex, Gemini CLI, aider โ€” same pattern; env var auto-selected per agent:
distil wrap --expand -- codex     # โ†’ OPENAI_BASE_URL
distil wrap --expand -- gemini    # โ†’ GOOGLE_GEMINI_BASE_URL
distil wrap --expand -- aider     # โ†’ OPENAI_BASE_URL

# Headless too โ€” print mode, CI, and Agent SDK scripts route the same way:
distil wrap -- claude -p "summarise this diff"
distil wrap -- python my_agent_sdk_script.py

Each recognized agent (claude / codex / gemini / aider) auto-selects the right env var and upstream โ€” no --env-var or --upstream flag needed. Prints preset: <agent> detected โ†’ <VAR> on start. Explicit flags always win.

Tired of typing distil wrap every time? Make it the default โ€” once:

distil default            # adds a managed shell alias so `claude` always routes through distil
distil default --undo     # remove it anytime (backed up before any change)

It detects your shell (zsh / bash / fish / PowerShell) and billing mode, writes the right line to the rc file your shell actually reads, and tells you what it detected. Want every SDK covered (not just the agent you type)? distil default --always-on runs a persistent proxy service โ€” powerful, but it's a daemon you keep alive.

Then watch genuine savings from your traffic โ€” measured, not estimated:

distil leaderboard          # cumulative tokens + $ saved, from the local ledger
distil dashboard            # live terminal TUI โ€” token-trim + decision-equiv bars, Ctrl-C to exit
distil dissect             # per-session deep-dive: savings, digest inventory, anomalies (--html/--serve)

Validate it on your traffic. --shadow runs a fraction of requests twice (compressed and full) and compares the agent's chosen next action:

distil wrap --shadow 0.1 -- claude   # wrap + shadow 10% of requests
distil shadow-stats                  # live decision-equivalence rate

Honest scope: that's next-action equivalence โ€” a proxy, not task success (E7 shows it doesn't fully transfer under aggressive lossy compression). Distil fails safe to full context.

Will it save money? Only on metered billing (API key) โ€” fewer tokens, fewer dollars. On a flat-rate subscription it trims context + latency, not the bill. Coding agents: short sessions ~7%, big wins on long, many-turn sessions the model never re-reads.


Related MCP server: PlanckBot

๐Ÿ’ก Why Distil is different

You don't need byte-equivalence โ€” you need decision-equivalence: your agent taking the same actions with compressed context. That's measurable and certifiable.

  • Certified, not estimated โ€” a strategy ships only if a non-inferiority test passes; can't certify โ†’ full context.

  • Certified end-to-end, too โ€” distil certify-trajectories bounds how many solvable tasks compression can cost (no other compressor certifies either level).

  • Reversible, not lossy โ€” digests behind a handle, keeps the original, hands the agent a distil_expand tool. Compress fearlessly.

  • Keeps the answer, folds the noise โ€” a per-content-type keep policy pins each kind's load-bearing lines (a log's pass/fail verdict, a traceback's frames, a diff's hunk headers); repeated near-identical error spam is deduped, and on a green run dedup tightens further since that noise didn't fail anything.

  • Query-aware โ€” keeps the line you're actually asking about โ€” distil is a proxy, so it sees the agent's intent (its tool_use args + latest ask) in the same request as the output. The line matching what you searched for (a grep hit, a config value, a SHA) is pinned even in arbitrary output โ€” additively, so reversibility and the certificate are untouched. No post-hoc compressor has that query/output pairing. It also goes semantic, and always-on: a zero-dependency bridge โ€” morphology, a curated technical synonym map, and char-trigram fuzz โ€” pins lines that answer the query without sharing a word with it. Ask "the retry limit?" and it keeps max_attempts = 5; ask "the connection timeout?" and it keeps deadline_ms. Two more layers grow from your own traffic, never from a shipped blob: associations distil learns from its content-free expand flywheel (hashed pairs, --expand sessions), and a learned relevance model that is promoted only after its held-out recall beats the lexical baseline on your labels โ€” until promotion, the lexical + bridge layers are exactly what runs. An optional distributional-vector table can be supplied too (pure-Python cosine; none ships). Every layer is additive โ€” it can only widen keeps, so reversibility and the certificate are untouched โ€” and it needs no embeddings or model to work.

  • Lossless even on a flat-rate plan โ€” subscription/lossless mode isn't just verbatim: it minifies JSON, collapses duplicate runs, and folds tabular tool output into a compact self-describing table (~70โ€“79% smaller, ToS-safe, no lossy digest). Recent tool outputs stay byte-exact.

  • See exactly what happened โ€” distil dissect turns a wrap session into a report: savings by model/mechanism, the digest inventory, billed-usage calibration, latency by path, and a worth-your-attention anomaly list that catches silent failures automatically.

  • Compounds on outcomes โ€” expansions and matched failures teach the policy what to protect (signatures only, never content) โ€” always more conservative.

  • Streams like it isn't there โ€” SSE relays chunk-by-chunk; TTFT preserved โ€” including recoverable digest, which speculatively streams and only intercepts an actual distil_expand call mid-stream, splicing the recovery in without buffering the turn (no TTFT tax on the reversible tier).

Fidelity tiers: lossless (--verbatim) ยท reversible (byte-recoverable on demand โ€” default) ยท lossy (every other tool). Only Distil certifies the reversible tier (Headroom ships an uncertified retrieve; Distil's recovery is agent-facing โ€” the model expands mid-task โ€” and gated by the decision-equivalence certificate).


โšก Prove the numbers yourself โ€” no API key

Don't take the table above on faith. distil bench re-certifies savings and decision-equivalence on a bundled 8-domain corpus, offline, in seconds โ€” the same gate that runs in CI. How we evaluate โ€” and why a compression ratio without a task-success delta is meaningless โ€” is written up in docs/EVALUATION.md, including our own negative result:

uvx --from distil-llm distil bench      # certify savings + quality across 8 domains, in seconds
distil verify                           # byte-fidelity: every compression is exactly reversible
distil validate                         # adversarial real-path gate: invariants on hostile inputs
distil retention                        # fact recall: what stays visible vs expand-recoverable
distil retention --dataset hotpotqa     # graded against a PUBLIC benchmark's ground truth
distil fidelity                         # state probes: artifact state, overclaim, continuation

Five gates, all in CI: bench (non-inferiority on the corpus), verify (byte-fidelity), retention (fact-level recall), fidelity (state probes, below), and validate โ€” which drives the compressor against adversarial inputs (huge/unicode/nested/malformed/marker-injection/secret-looking) and asserts reversibility, reject-if-bigger, recency-exactness, fail-open, and content-free telemetry hold on every one. That last gate exists because a green unit suite kept coexisting with real-traffic bugs; validate is the adversarial layer that catches them.

Recall is not enough, and here's the case that proves it. A trajectory creates net/scratch_bench.py at turn 2 and deletes it at turn 4. Compress away turn 4 and every path token is still present โ€” string recall reads 100% โ€” while the agent now believes a file exists that doesn't, and will plan around it. distil fidelity folds tool calls into a file-state ledger and grades the final state, separating lost (path gone โ€” the agent can see the gap) from stale (path present, state wrong โ€” the agent acts confidently on a falsehood). On that case: string recall 100%, state fidelity 0%.

It reports three more things recall can't see: overclaim ("approximately 4200 ms" โ†’ "4200 ms" โ€” the value survives, its uncertainty doesn't), continuation (does the agent still know what's left to do?), and error propagation (does a loss at turn k show up as a behaviour change at turn k+n?). The gate is on silent failures only โ€” CI runs --max-silent 15 โ€” because loud loss is already retention --max-lost's job, and gating one regression twice hides which property broke. The bound is the measured one, not zero: Tier-1 digests hedged spans behind restore handles and drops the qualifier on 9 of 171 claims, so gating at zero would assert a property the compressor does not have. Full methodology, including what these probes found wrong with our own corpus, in docs/EVALUATION.md ยง6; how to run everything, in docs/RUNNING-EVALS.md.

Recall, and a number you can check yourself. The three gates above are graded on our corpus against our oracle โ€” rigorous, but not checkable by you. distil retention --dataset hotpotqa grades against ground truth written by someone else (HotpotQA's gold supporting sentences, amid 8 distractor paragraphs), next to a truncation baseline tuned to distil's own savings on the same case:

HotpotQA, n=100

savings

answer recall

gold-sentence recall

distil (reversible)

14.3%

100.0%

100.0%

truncation @ matched savings

14.1%

91.6%

82.7%

distil retention also splits recall into visible (in front of the model) and recoverable (one distil_expand away, verified against the handle's restore bytes). On the corpus that's 100% true recall with 0 lost, and being reversible instead of lossy is worth 21.4% recall โ€” the mean across all 8 domains, each counted once. That's deliberately the macro average: the fact-weighted one reads 62.6%, but it's set by whichever domain carries the most probes, and one HTML fixture moved it from 9.8% to 62.6% without the compressor changing at all โ€” the moat, as a measurement rather than an argument. distil retention --live reports the same on your own traffic; the meter stores counts only, never content.

And it found a real hole. The first thing the recall harness caught was not a regression but a missing capability: distil was compressing 0.0% of HTML tool results โ€” minified markup is one long line, so line-folding had nothing to fold. Agents with a fetch or browser tool were paying full price for <script>, <style>, and nav chrome. Now:

real page

before

after

saved

facts lost

Wikipedia article

281,093 tok

14,260 tok

94.9%

0

Python docs page

32,322 tok

4,229 tok

86.9%

0

Reversible, which is the part a lossy extractor can't offer: the exact original stays behind the handle, so a bad heuristic call costs one distil_expand instead of the content.

To be precise about what each layer proves: the per-commit gates grade decision-equivalence with an offline deterministic oracle over the committed corpus (fast, free, runs on every push โ€” but synthetic). A nightly live-cert job re-certifies the same trajectories against a real model (distil certify --runner anthropic), budget-capped with a hard --max-live-calls ceiling so an unattended run can never spend silently. The empirical results above (SWE-bench n=500, live head-to-head n=200) were graded by real models; the per-commit badge alone doesn't claim that.

domain            trajectory                $ saved   distil   aggr  pruned
---------------------------------------------------------------------------
ops/sre           sre-disk-incident           32.8%     PASS   FAIL     615
coding            coding-bugfix               25.5%     PASS   FAIL     736
support           support-refund              32.6%     PASS   FAIL     765
research          research-synthesis          25.7%     PASS   FAIL     809
data-analysis     data-analysis-sql           18.1%     PASS   FAIL     965
devops            devops-rollback             22.8%     PASS   FAIL     857
finance           finance-reconcile           24.9%     PASS   FAIL    1014
---------------------------------------------------------------------------
aggregate: distil cuts $0.14212 -> $0.10610 (25.3% cheaper) reversibly; 5761 tokens causally prunable.
GATE: PASS โ€” every trajectory certified non-inferior; aggressive rejected on all.

Why trust the number? Token-savings numbers are easy to fake โ€” measure quality at low compression, advertise savings at high compression. Distil refuses that: accuracy and compression are measured on the same trajectories, and a strategy that can't pass non-inferiority doesn't ship.

distil certify --strategy distil       # VERDICT: PASS  (100% decision-equivalence)
distil certify --strategy aggressive   # VERDICT: FAIL  (mean diff โˆ’1.0, blocked)

distil eval plots the certified compression frontier โ€” a savings-vs-quality curve where every point carries its certification verdict, locating the cliff past which lossy compression drops decisions. The artifact no competitor publishes: benchmark.html.


๐Ÿ“Š The proof

Three results, all reproducible, all published with caveats:

  • Live head-to-head vs real llmlingua / headroom-ai (graded by claude-opus-4-8): 83.2% savings at 0% decision-change, ~1,000ร— faster (no ML model loaded vs. competitors' local transformer inference). The live proxy behavior is pinned to the certified strategy by tests/test_live_certified_equivalence.py; the one reviewed delta is a recency carve-out that keeps the last few tool-result turns verbatim (an agent needs its freshest output byte-exact). โ†’ benchmark

  • E7 (SWE-bench Verified): aggressive lossy compression craters task success (52% โ†’ 16%) โ€” a per-step certificate doesn't transfer to multi-turn. The reversible tier survives (56% vs 52%). We publish it because it's true. โ†’ E7

  • E8โ€“E14 (500-instance agent): the reversible tier is the only compressor non-inferior to full context, generalizes across 5 models / 3 vendors, and the newest digest matches full within noise (42.0% vs 39.2%). โ†’ E8โ€“E14

Full methodology, McNemar tests, per-instance data: docs/PAPER.md ยท PDF.


๐Ÿ“ก See it working

Measured on your traffic, never estimated, nothing leaves your machine:

  • Per request: x-distil-* response headers (tokens-saved, mode, compressible-tokens, expanded).

  • Per machine: distil leaderboard (--html for a page).

  • Shadow mode: distil proxy --shadow 0.05 reports the live decision-change rate โ€” streaming-aware.

  • Org-wide: distil proxy sidecar + set ANTHROPIC_BASE_URL once; every client routes through it.

  • Community: an opt-in census (distil census on) shares your numbers-only totals โ€” preview the exact payload with distil census show before consenting; TELEMETRY.md has the frozen schema. Default remains: nothing is sent.

Dashboard, status-line plugin, federated leaderboard: Deploy & observability.

๐Ÿ”Œ Works with every SDK

One proxy. Point any base_url-honoring client at it โ€” Python, TypeScript, any language โ€” and get cache-aware reversible compression with no code change.

distil proxy --upstream https://api.anthropic.com   # localhost:8788
// JS/TS: npm i distil-llm  โ†’ helper so you don't hardcode the URL
import Anthropic from "@anthropic-ai/sdk";
import { distilBaseURL } from "distil-llm";
const client = new Anthropic({ baseURL: distilBaseURL() });

SDK / framework

Change

Example

Anthropic SDK (Py/TS)

base_url="http://127.0.0.1:8788"

examples/python_anthropic.py ยท examples/js_anthropic.ts

Claude Agent SDK / claude -p (headless)

distil wrap -- <cmd> or ANTHROPIC_BASE_URL

examples/python_claude_agent_sdk.py

OpenAI SDK (Chat + Responses)

base_url="http://127.0.0.1:8788/v1"

examples/python_openai.py

Vercel AI SDK

createAnthropic({ baseURL: 'โ€ฆ:8788' })

examples/js_vercel_ai_sdk.ts

LangChain (py/js) ยท LangGraph

anthropicApiUrl / base URL ยท pre_model_hook

examples/js_langchain.ts

LiteLLM

api_base="http://127.0.0.1:8788"

examples/python_litellm.py

Google Gemini

--upstream https://generativelanguage.googleapis.com

examples/python_gemini.py

Codex ยท aider ยท Cursor-agent ยท any base_url client

distil wrap -- <agent> or OPENAI_BASE_URL

โ€”

Anything that speaks the Anthropic / OpenAI / Gemini wire format works โ€” the proxy is framework-agnostic, so CrewAI, AutoGen, Agno, Strands, Bedrock, etc. route through it unchanged by pointing their client's base URL at distil.

Prefer in-process? Wrap the client directly โ€” still no call-site change:

from distil.adapters.anthropic import wrap
client = wrap(anthropic.Anthropic())   # compresses the request, keeps the cache warm

(OpenAI โ€” Chat Completions and Responses API โ€” and Gemini route through the proxy: distil wrap -- codex, or point OPENAI_BASE_URL at it. An in-process client wrap exists for the Anthropic SDK only.)

Framework hooks (no proxy, no network hop) โ€” for agent frameworks that own the message list, compress it where it lives:

Framework

Hook

Example

LiteLLM

distil.integrations.litellm.compress(kwargs)

examples/python_litellm.py

LangChain

distil.integrations.langchain.compress_messages(msgs)

โ€”

LangGraph

pre_model_hook=pre_model_hook() (compresses graph state before the model node)

examples/python_langgraph.py

LangChain / LangGraph โ€” langchain-distil

Listed in LangChain's own community middleware integrations. If you came from there, this is the package:

pip install langchain-distil
from langchain_distil import compress_messages, pre_model_hook, as_runnable

msgs = compress_messages(msgs)                 # compress a message list in place of the call
graph = create_react_agent(..., pre_model_hook=pre_model_hook())   # LangGraph: before the model node
chain = as_runnable() | llm                    # or drop it into a chain (lazy langchain-core import)

Tool and function messages get the reversible Tier-1 digest, human and system messages are Tier-0 lossless, and assistant messages are never rewritten โ€” a model's own words are not distil's to edit. Every digest is byte-exact recoverable. Pass verbatim=True for Tier-0-only when no recovery tool is available.

It is a thin wrapper over the hooks in the table above, so it inherits the same certified compression path โ€” nothing is re-implemented. distil-llm is a dependency; you do not install both by hand.


๐Ÿง  MCP server โ€” give your agent a recall tool

Distil ships a Model Context Protocol server so an agent can compress its own tool output and get the exact bytes back later. Zero dependencies (stdlib JSON-RPC over stdio, no SDK), fully local โ€” content never leaves the machine.

Add it in one line:

claude mcp add distil -- distil mcp
{
  "mcpServers": {
    "distil": { "command": "distil", "args": ["mcp"] }
  }
}

Haven't installed distil? Run it straight from PyPI โ€” no install step:

{
  "mcpServers": {
    "distil": { "command": "uvx", "args": ["--from", "distil-llm", "distil", "mcp"] }
  }
}

Config lives in ~/Library/Application Support/Claude/claude_desktop_config.json (Claude Desktop, macOS), .cursor/mcp.json (Cursor), or .vscode/mcp.json (VS Code). Restart the client after editing.

Verify it's up โ€” no client needed:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | distil mcp

The three tools

Tool

Does

Your agent reaches for it when

distil_compress(text)

Returns a compact digest + an 8-hex handle; stores the original locally (encrypted, 0600)

A tool returned something huge and carrying it verbatim is wasteful

distil_expand(handle)

Returns the exact original bytes โ€” not a summary

The digest lost a detail it now needs: a line, a value, a stack frame

distil_savings()

Cumulative tokens/dollars from the local ledger

You ask "how much has distil saved me?"

Every tool is annotated (readOnlyHint, idempotentHint, openWorldHint: false), so a well-behaved client knows distil_expand is a safe, repeatable, offline read without having to guess from prose.

This is the recall path, not the savings path. The MCP server doesn't compress your agent's traffic โ€” distil wrap -- <agent> does that, transparently, with no tool calls. What the MCP server adds is the other half: any agent, including one you didn't wrap, can call distil_expand on a handle it sees in context and get the original back. Handles persist across sessions and processes, and age out after DISTIL_RESTORE_TTL_DAYS (default 14).


๐Ÿ“ฆ Install your way

New here? pipx install distil-llm, then distil onboard โ€” it sets you up and guides you (see Use it now). Want to see it prove itself first instead? distil bench runs the certified gate in ~10s, no API key. The matrix below is for picking an install format โ€” everything in it is an alternative, not a requirement.

โš ๏ธ The one gotcha โ€” the name. The PyPI package is distil-llm but the command is distil (the bare name was taken). So pipx install distil-llm โ†’ run distil โ€ฆ. pip install distil installs something else.

๐Ÿ”ง Seeing Could not find a version that satisfies the requirement distil-llm (from versions: none)? The package is on PyPI โ€” that error means your pip/pipx is on a Python older than the package's floor, so pip filters every release out. Distil now supports Python 3.9+ (the version macOS ships), so a current install just works; if you still hit this on a very old Python, let uv provision one for you: uvx --python 3.12 --from distil-llm distil bench (or uv tool install --python 3.12 distil-llm). Check yours with python3 --version.

๐Ÿ”ง Got an old version (e.g. 0.25.1) instead of the latest? Public PyPI always serves the newest (pip index versions distil-llm lists them). If you got an older one, your pip/pipx is not resolving against public PyPI โ€” almost always a stale internal mirror (Artifactory / CodeArtifact / Nexus that hasn't synced the latest yet โ€” common right after a release) or a <1.0 version pin in a constraints file / pip.conf. Diagnose and fix:

pip index versions distil-llm     # stops at an old version? โ†’ your index/mirror is stale
pip config list ; env | grep -i pip   # look for an index-url or PIP_CONSTRAINT pin
# unblock now โ€” force public PyPI:
pipx install --pip-args="--index-url https://pypi.org/simple/" distil-llm
# (or, if you must use the mirror, ask your platform team to sync distil-llm; it exists upstream)

Format

Command

Prereq

Zero install

uvx --from distil-llm distil bench

uv โ€” auto-provisions Python 3.9+

Isolated CLI

pipx install distil-llm โ†’ distil bench

Python 3.9+ (else pipx install --python python3.12 distil-llm)

Homebrew

brew install dshakes/tap/distil

Homebrew

Docker

docker run ghcr.io/dshakes/distil:latest bench (or docker build -t distil .)

Docker

Single file

make pyz โ†’ python dist/distil.pyz bench

Python 3.9+

In a venv

pip install distil-llm (inside an active virtualenv)

Python 3.9+

Node / JS / TS

npx distil-llm wrap -- <agent> ยท npm i distil-llm for baseURL helpers

Node 18+ (bridges to Python via uv/pipx)

The import package and CLI are distil; the PyPI distribution is distil-llm (the bare name was taken โ€” so uvx/pip must reference distil-llm, not distil). Distil is a CLI: install it isolated (pipx/uv/brew/Docker), because modern macOS/Linux block system-wide pip install (PEP 668). Node / JS / TS: npx distil-llm wrap -- <agent> (the npm package bridges to the CLI), or npm i distil-llm for distilBaseURL() helpers to point any SDK at the proxy โ€” or just set base_url yourself.


๐Ÿงฐ Cheat-sheet

Basics are in Use it now and Works with every SDK. Beyond that:

Goal

Command

Set up + a guided tour (start here)

distil onboard

Make distil the default (no per-session wrap)

distil default ยท undo: distil default --undo

Remove distil's footprint (before uninstalling)

distil offboard ยท also clear data: distil offboard --purge

Diagnose your setup (ledger, shadow, proxy self-test, wiring)

distil doctor

Wire the savings status line into Claude Code

distil setup (compact segment: DISTIL_STATUSLINE=minimal)

Watch genuine savings accumulate

distil leaderboard ยท live TUI: distil dashboard

Session summary on exit (tokens, cost, shadow, restorability)

printed automatically by distil wrap โ€” opt out with DISTIL_NO_LEDGER=1

Deep-dive one session (savings, anomalies)

distil dissect (--html / --serve)

Live decision-equivalence on real traffic

distil wrap --shadow 0.1 -- claude โ†’ distil shadow-stats

Certify on your domain

distil ingest --input prod.jsonl --out ./mycorpus โ†’ distil conformal --corpus ./mycorpus

Recover digested detail from any agent (MCP)

distil mcp

Self-improving keep policy

distil learn / distil online

Status line โ€” one pattern in every state: distil ยท <live> ยท total โ–ผ<lifetime>.

state

you see

means

saving

distil ยท โฌข digest ยท โ–ผ12.0K ยท 40% smaller ยท $0.31 ยท total โ–ผ27.0M ยท de 99%

compressing (mode chip: โฌข digest ยท โ—‡ lossless ยท โ–ช verbatim; de = decision-equivalence)

watching

distil ยท โœ“ on ยท waiting for a large read ยท total โ–ผ27.0M

on, but no large content yet โ€” savings come from big file/command output

idle

distil ยท โœ“ on ยท total โ–ผ27.0M

set up and on, no recent traffic

not routed

distil ยท off โ€” session not routed ยท total โ–ผ27.0M

this session's requests go straight to the provider โ€” start it with distil wrap (or the always-on env) to compress

bypassing

distil ยท โš  wrapped, agent bypassing proxy ยท total โ–ผ27.0M

the wrap is up but zero requests reached its proxy in 3+ minutes โ€” the agent pinned its own endpoint. Fix: restart the wrap. Seen mostly with claude.ai-subscription (OAuth) sessions; routing those through a custom base URL is undocumented upstream, and a session occasionally ignores it. scripts/soak-report.sh captures evidence if it persists

The de segment is live decision-equivalence evidence: a โœ“/โš /โœ— rate once 50 A/B samples + 30 A/A samples accrue (A/B = compressed-vs-original; A/A = same request replayed against itself โ€” the sampling-noise baseline), de n/50 while collecting. Shadow sampling is on by default at 2% (--shadow 0 disables; --shadow 1.0 samples every request โ€” proves equivalence in minutes at ~3ร— token cost, then drop back to the default 2%).

Measured: In live validation (signature v3 / 1.13.0), distil preserved the agent's next decision on 100% of 116 sampled production requests (0 changes); temperature-0 A/A self-agreement of 31/31 confirms this is compression fidelity, not sampling noise. Validated result โ€” not a guarantee for all workloads.

โ–ผ = tokens saved ยท total = lifetime ยท de = decision-equivalence (verdict once 50 A/B + 30 A/A shadow samples accrue). Sharing the line with git/cwd/model? DISTIL_STATUSLINE=minimal โ†’ distil โ–ผ7.8K ยท 27M total. On a flat-rate subscription, dollars are notional and auto-hidden (DISTIL_SUBSCRIPTION=0/1).

Compression modes โ€” in plain English

You usually don't need to pick. distil onboard detects your billing and sets the right mode for you โ€” it writes it into your setup so every session just works. Pass a flag to override for a specific session.

  • digest (the default) โ€” Distil shortens long things (big files, command output, past steps) into short summaries, and can pull back the full original the moment the AI needs it. You save the most, and nothing is truly gone โ€” originals are kept and restored automatically. Most people should just use this.

  • expand โ€” Same shortening as digest, but Distil also gives the AI a "show me the full version" button it can press on its own. Best when the AI runs for a long time autonomously (e.g. long coding sessions). Picked automatically if you pay per use (API key).

  • lossless-only (a.k.a. --safe) โ€” The cautious setting: Distil only trims things it can rebuild perfectly (like extra blank space), and never summarizes. You save less, but there's zero chance of losing any detail. Picked automatically on a flat monthly subscription.

  • verbatim โ€” The lightest touch: just tidies formatting, changes nothing else. Almost no savings. Use it when you want to see or audit exactly what's being sent.

For the technical breakdown:

Mode

What it does

Savings

Safety

Auto-selected when

--expand

Digest + injected expand tool so the model recovers content on demand

Most

Lossy-but-recoverable

Metered / API-key (PAYG)

(default) digest

Tier-1 digest only โ€” no tool injection

High

Reversible via RestoreStore

No flag passed

--lossless-only / --safe

Lossless transforms only โ€” no digests, no tool injection

Fewer

Zero unrecoverable content

Subscription / flat-rate

--verbatim

Whitespace + JSON normalization only

Minimal

Most conservative

Debugging / auditing

Subscription users should not force --expand; it crosses the lossless safety boundary. Coding re-reads? Add --session-delta either way.


๐Ÿง  How it works

Two techniques carry most of the win โ€” they target where the money actually is in an agent loop, not where it looks like it is.

โ‘  Cache-aware compression โ€” the dominant lever

You re-send the growing context every step. With prompt caching a cache read is ~10ร— cheaper than fresh input, so the real cost is cache misses, not context size. Distil keeps the prefix byte-stable (schema canonicalization + lifting volatile fields like timestamps/UUIDs out of the prefix) and compresses only the volatile tail.

Naive recompression sends fewer tokens yet costs more than not compressing at all, because it rewrites the cached prefix every turn. Distil doesn't โ€” that's the whole game most tools miss.

โ‘ก Causal / counterfactual pruning โ€” the discovery engine

The eval isn't a ruler bolted on the side; it's a discovery engine. Remove a context block, replay, did any decision change? Blocks that never change a decision are provably free to drop.

distil prune
# doc-0   PRUNE (causally inert)     # speculative retrieval, never cited
# obs-0   keep (changed a decision)  # carries the decision-driving signal

๐ŸŽ“ The certificate (DERC)

The gate answers "is this strategy non-inferior on my corpus?". The Decision-Equivalence Risk Certificate answers the operational one: "for a risk budget I choose (say โ‰ค5% decision-change), how hard can I compress with a guarantee that holds on my real traffic?"

distil conformal --corpus ./mycorpus --alpha 0.05 --delta 0.05
# โœ” CERTIFIED 'lossless' โ†’ 57.4% savings; decision-change โ‰ค 5.0% at 95% confidence (Learn-Then-Test)

Every certificate names the oracle that graded it. A certificate is evidence, and evidence that doesn't say what produced it isn't evidence โ€” so Certificate.grader is stamped from the runner and printed in the guarantee. The default offline gate is graded by a deterministic synthetic oracle, not a model, and it says so verbatim: Graded by: deterministic (synthetic DECISION: oracle โ€” NOT a model). Real-model evidence comes from distil certify --runner anthropic, and its certificates name that runner instead. You can always tell which layer a number came from, because the number carries it.

It's conformal risk control (Learn-Then-Test / CRC โ€” distribution-free, finite-sample), not a heuristic threshold. The one load-bearing caveat: the guarantee requires exchangeability (calibration traffic โ‰ˆ live traffic) and is marginal over that distribution โ€” recalibrate on drift. Full theory + citations: Concepts ยท docs/PAPER.md.

๐Ÿ” The trajectory-level certificate

DERC certifies the step; this certifies the task. Our E7 experiment โ€” and the 2024โ€“26 agent-compression literature โ€” shows per-step fidelity can pass while end-to-end success collapses, so distil also certifies the level users actually feel: run your eval suite twice (full context vs compressed), feed the matched outcomes in, and get a distribution-free bound on how many solvable tasks compression may cost you:

distil certify-trajectories outcomes.jsonl --alpha 0.05 --delta 0.05
# each line: {"task_id": "...", "full_success": true, "compressed_success": true}
# โ†’ With confidence 95%, compression degrades at most 5.0% of tasks the full
#   context would have solved (observed 0.5% over 200 matched trajectories).

It refuses to certify on small samples, states its exchangeability assumptions in the certificate itself, and ships an anytime-valid drift monitor (trajectory_risk.drift_monitor) that tells you when live traffic has shifted enough that the certificate is stale. Matched failures also feed the outcome-guided policy (distil.compress.guideline): content classes that break tasks when digested get protected byte-exact, automatically.

๐Ÿงฉ What's inside

40+ shipped capabilities, all real (no stubs): the cache-aware cost engine, causal pruning, the TOST gate + conformal certificate, the proxy + Anthropic/OpenAI/Gemini first-class adapters (Chat Completions, Responses API, and Gemini generateContent), an MCP server, LiteLLM/LangChain/LangGraph hooks, per-agent wrap presets, the Proof Ledger end-of-session printout, the multi-tenant gateway with issued keys and rate limits, encrypt-at-rest for the restore store, learned keep-models, output compression, and an optional Rust hot-path core (build-from-source via maturin; published wheels run the pure-Python engine, same API) โ€” with zero runtime dependencies in the core.

Full module-by-module map: Architecture ยท Techniques ยท CLI reference.

๐Ÿ”’ Security & deployment

  • Localhost-only by default โ€” the proxy binds 127.0.0.1 and forwards only to the single configured upstream (no SSRF).

  • No secret/body logging โ€” request bodies and credentials are never logged.

  • Auth-mode gating โ€” a detected subscription/OAuth session auto-selects --lossless-only (Tier-0 verbatim: no Tier-1 digest stubs, no tool injection โ€” provider-ToS-safe); distil wrap -- claude is safe by default, no flag needed. An explicit --expand opts into the recoverable digest even there (you authorized the recovery tool, so nothing is irreversibly lost โ€” issue #28). Without an injected expand tool the agent cannot recover a stub, so --lossless-only folds directly into verbatim.

  • Encrypted at rest โ€” digest originals in ~/.distil/restore/ are encrypted with HMAC-SHA256-CTR (encrypt-then-MAC, DSTL1 header, key at chmod 0600), protecting against backup/sync leakage and cross-user reads on shared filesystems. A same-UID attacker who can read both the data files and the key file is explicitly out of scope (see THREAT_MODEL.md). Legacy plaintext files load transparently. DISTIL_NO_ENCRYPT_AT_REST=1 opts out; handles age out after DISTIL_RESTORE_TTL_DAYS (default 14). No data is forwarded upstream.

  • Ops-ready โ€” unauthenticated GET /distil/health liveness probe on every entry point (never touches the billed upstream); gateway accounting checkpoints to disk every 30 s (crash-safe, not just on graceful shutdown); DISTIL_DEBUG=1 surfaces everything the fail-open compression path swallows.

  • Upgrades apply to live sessions โ€” distil wrap supervises its proxy as a subprocess on a wrap-owned socket; when a new version lands on disk (pipx/pip upgrade) the wrap hot-swaps in a fresh worker โ€” same port, in-flight streams finish on the old one, the agent never restarts. Health-checked with automatic rollback: a broken upgrade keeps the old worker serving. POSIX; kill -USR1 <wrap pid> forces it, DISTIL_HOT_SWAP=0 opts out. On Windows the wrap keeps the historical in-thread proxy (no seamless swap) and warns on version skew instead โ€” upgrades there apply on the next session.

  • OpenTelemetry GenAI spans (opt-in) โ€” pip install 'distil-llm[otel]' and every proxied call emits a GenAI semantic-convention span (gen_ai.request.model, gen_ai.usage.input_tokens) plus distil's own story: distil.tokens.original vs distil.tokens.compressed, distil.compression.ratio, distil.shadow.sampled, and distil.session.id for per-session trace correlation โ€” your existing OTel backend sees exactly what compression did to each request. Without the extra installed it's a single boolean check, zero overhead, and an OTel failure can never break the request path. The same numbers also export as OTel counters (distil.requests, distil.tokens.baseline/.sent/.saved), recorded at the same instrumentation point as the span attributes โ€” so tracing and metrics can't disagree โ€” and recorded before the span check, so they still work with tracing sampled off.

  • Prometheus endpoint (gateway) โ€” GET /distil/metrics serves the standard text exposition format (distil_tokens_saved_total, distil_dollars_saved_total, distil_compression_ratio, โ€ฆ), written against the stdlib, so the scrape path adds no dependency and cannot fail to import. Series are labelled by tenant, so the endpoint sits behind exactly the same admin gate as /distil/stats: open on loopback for local use, and on any non-loopback bind it requires --admin-token and refuses without one. That gate is the point โ€” an unauthenticated tenant-labelled /metrics is precisely the LiteLLM leak class of bug, and it is tested for directly (403 unauthenticated, 401 on a wrong token, plus label-injection and no-secrets-in-exposition tests). Full reference: docs/metrics.html.

  • Vision โ€” repeated screenshots stop costing full price โ€” a 1024ร—1024 image is ~1,400 input tokens, and an agent that screenshots a UI or polls a dashboard pays that on every turn the block stays in context. Distil elides only byte-identical repeats, replacing each with a recoverable reference: the first occurrence and every distinct image are untouched, nothing is re-encoded or downscaled, and distil_expand returns the original source byte-exact. URL sources are never treated as duplicates โ€” two occurrences of one URL are not evidence of the same pixels. The prevailing alternative resizes, which is lossy by construction and unverifiable; this is certified at 100% decision-equivalence against a live vision model (A/A floor 100%, TOST p<0.0001), and the certificate ships in the package stating its own scope. Certify your own workload with distil certify --strategy vision --runner anthropic โ€” your result outranks ours, including a failure. DISTIL_VISION=0 disables it. Full reference: Techniques ยง Vision.

  • Supply-chain hardening โ€” releases carry PEP 740 Sigstore attestations (via PyPI trusted publishing), a CycloneDX SBOM on every GitHub release, and OpenSSF Scorecard weekly on main. The release job fails if PyPI does not report an attestation bundle for the version it just published, so this line cannot drift into being false. Verified for every release back to 1.19.0. Don't take our word for it: curl -s https://pypi.org/integrity/distil-llm/<version>/<filename>/provenance (note the integrity API โ€” /pypi/<pkg>/<ver>/json does not carry an attestations field, and reading it there reports a false negative), or uvx pypi-attestations verify pypi --repository https://github.com/dshakes/distil pypi:distil_llm-<version>-py3-none-any.whl.

See Deploy & security for topologies (local sidecar, container sidecar, shared gateway) and the threat model.


โœ… What we won't pretend

  • Self-calibrating token counts โ€” the offline heuristic is directionally accurate; the compression ratio is exact regardless. distil is a proxy, so it sees the provider's real usage.* on every response โ€” it learns the systematic correction from that (content-free, no network) and calibrates the absolute counts to your model + content mix automatically. The leaderboard shows "calibrated to your billed usage (N requests, ยฑX%)" once enough traffic has flowed; until then it's the raw heuristic (identity, so no skew). For per-string exactness there's still --tokenizer anthropic.

  • Default runner is a deterministic stand-in (offline gate with ground truth). Non-circular eval grades real agent traces with a real model โ€” proof harness.

  • Credible grading, enforced: majority-vote (single samples let grader noise look like a decision change), a same-family grader, and grading the reversible tier with its distil_expand recovery loop.

  • No fabricated weights โ€” the keep-model is a real logistic classifier (96.4% held-out accuracy, 0.98 F1; the committed metrics.json regenerates byte-identically from python -m distil.codec.learned, seed-pinned). The optional transformer codec ships no checkpoint in the package โ€” a demo checkpoint is attached to the v0.1.0 release, and production means retraining on your own traces (distil train-transformer).

Deliberately not a platform

Distil is a compression engine with a correctness gate, not a context suite. We declined what can't go under the certificate:

Adjacent feature

Our stance

Persistent memory / knowledge graph

Out of scope โ€” a lossy store is the opposite of byte-reversible.

Hosted semantic cache

Out of scope โ€” we make the provider's prompt cache pay off, not a second lossy one.

Editor/Copilot auth

Out of scope โ€” Distil sits on the wire or in-process; never brokers credentials.

What we did adopt (it survives the gate): a pluggable salience scorer to protect entities, cache-prefix observability, and framework hooks.


๐ŸŽฏ Both sides of the bill

Distil compresses input/context (comprehensive) and output โ€” generation-side verbosity shaping (PAYG, measured with distil output-savings) plus a reversible output-on-re-entry digest, so verbose past answers stop costing full price as history. Details: Output & I/O.

๐Ÿ”ฌ Reproducible evaluation & the paper

Every number reproduces from the bundled corpus (distil bench, no key). The non-circular proof harness grades real agent traces with a real model (ฯ„-bench / SWE-bench): benchmarks/PROVE.md. Compiled paper, LaTeX source, and all committed results: docs/PAPER.md ยท docs/paper/ ยท paper PDF. Step-by-step: Reproduce the Numbers โ†’


โญ If distil saved you tokens

A star is how the next engineer finds provable savings instead of a lossy guess โ€” and distil stats --badge gives you a shareable badge of your own measured number to show alongside it. That badge + this repo are the whole marketing department.

๐Ÿค Contributing

PRs welcome โ€” see CONTRIBUTING.md. The one rule that matters: a new compression strategy must pass make gate (non-inferior on every domain, byte-reversible). No green gate, no merge. That's the whole philosophy in one sentence.

Beta program โ€” want early access to v1.20.0 and to help close the GA gate? See docs/BETA.md.

๐Ÿ’ฌ Community & support

  • Questions, ideas, bug reports โ†’ open an issue. Every question is a docs gap we haven't closed yet.

  • See who's saving tokens โ†’ the live, opt-in adoption board โ€” exact community totals, no projection, content-free by construction.

  • Watch releases โ†’ releases ship on a fast cadence; pip install -U distil-llm (or uv tool upgrade distil-llm) tracks them.

License

Apache-2.0 ยท โ€œSame potency, less volume.โ€

Install Server
A
license - permissive license
A
quality
A
maintenance

Maintenance

โ€“Maintainers
22hResponse time
0dRelease cycle
102Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Deterministic AI agent microtools, no accounts/API keys. fetch_extract: 98% token cut. 38 tools.

  • A paid remote MCP for OpenAI Codex context compressor, built to return verdicts, receipts, usage log

  • Stamp content with permanent, verifiable provenance. Hash locally, verify free forever.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dshakes/distil'

If you have feedback or need assistance with the MCP directory API, please join our Discord server