compression with a quality contract

Integrations — one proxy, every SDK

Start distil proxy once and point any SDK's baseURL at it. No library changes, no monkey-patching — compression happens at the network layer.

Cross-SDK proxy diagram

How it works

The proxy (distil proxy, default http://127.0.0.1:8788) is a local HTTP server. It intercepts the three compressible paths across all major LLM APIs:

All other paths and HTTP verbs pass through unchanged. Your API key travels in the request headers exactly as normal — the proxy never logs or stores it.

Zero SDK changes beyond baseURL. The proxy is transparent: the SDK sees the same wire format it always expects. Auth headers, streaming, tool use, and all other features work as-is.

SDK integration matrix

SDK / Framework Language Setting Value Example
Anthropic Python SDK Python base_url= http://127.0.0.1:8788 python_anthropic.py
Anthropic TypeScript SDK (@anthropic-ai/sdk) TypeScript baseURL in new Anthropic({…}) http://127.0.0.1:8788 js_anthropic.ts
Claude Agent SDK / claude -p (headless) Python / TS / CLI distil wrap -- <cmd> or ANTHROPIC_BASE_URL http://127.0.0.1:8788 python_claude_agent_sdk.py
OpenAI Python SDK Python base_url= http://127.0.0.1:8788/v1 python_openai.py
LiteLLM Python api_base= http://127.0.0.1:8788 python_litellm.py
Cursor (agent/chat panel only) IDE Settings → Override OpenAI Base URL http://127.0.0.1:8788/v1 below
CrewAI Python base_url= in LLM({…}) http://127.0.0.1:8788/v1 below
Agno Python base_url= in OpenAILike({…}) http://127.0.0.1:8788/v1 below
Strands Agents Python client_args={"base_url": …} in OpenAIModel({…}) http://127.0.0.1:8788/v1 below
Vercel AI SDK (@ai-sdk/anthropic) TypeScript baseURL in createAnthropic({…}) http://127.0.0.1:8788 js_vercel_ai_sdk.ts
LangChain.js (@langchain/anthropic) TypeScript anthropicApiUrl in ChatAnthropic({…}) http://127.0.0.1:8788 js_langchain.ts
Google Gemini REST (google-generativeai) Python / curl api_endpoint in client_options http://127.0.0.1:8788 (upstream: https://generativelanguage.googleapis.com) python_gemini.py

Snippets

Anthropic Python SDK

import anthropic

client = anthropic.Anthropic(
    api_key="sk-ant-…",
    base_url="http://127.0.0.1:8788",
)
response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}],
)

OpenAI Python SDK

import openai

client = openai.OpenAI(
    api_key="sk-…",
    base_url="http://127.0.0.1:8788/v1",
)
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Hello!"}],
)

LiteLLM

import litellm

response = litellm.completion(
    model="claude-opus-4-5",
    api_base="http://127.0.0.1:8788",
    api_key="sk-ant-…",
    messages=[{"role": "user", "content": "Hello!"}],
)

Running the standalone LiteLLM Proxy instead?

Same mechanism, one YAML field: point each model's api_base at distil proxy, and every request routed through the LiteLLM Proxy is compressed transparently.

# Terminal 1 — distil sits in front of the real upstream
distil proxy --port 8788 --upstream https://api.anthropic.com

# config.yaml — Terminal 2
model_list:
  - model_name: claude-opus-4-5
    litellm_params:
      model: anthropic/claude-opus-4-5
      api_base: http://127.0.0.1:8788
      api_key: os.environ/ANTHROPIC_API_KEY

# Terminal 2 — start the LiteLLM Proxy against that config
litellm --config config.yaml

Cursor

Settings → enable Override OpenAI Base URL, set it to the proxy, and add your model. The key field is labelled "OpenAI API Key" but is sent to whatever endpoint you configure.

Partial coverage — know this before you rely on it. Cursor routes its agent and chat panel through the overridden base URL, but tab completion and ⌘K inline edit stay on Cursor's own backend and never reach the proxy. So your savings cover the long-context agent traffic (which is where the tokens are) and not the keystroke-level features. Also note that turning this on has been reported to break Anthropic BYOK models with 422s, since Claude traffic is then sent OpenAI-shaped to your endpoint.

CrewAI

CrewAI's LLM takes base_url directly. Point it at the proxy and prefix the model with its provider.

$ pip install crewai
$ distil proxy --port 8788 --upstream https://api.openai.com &

import os
from crewai import Agent, LLM

llm = LLM(
    model="openai/gpt-4o",               # provider prefix is required
    base_url="http://127.0.0.1:8788/v1",  # ← the only change; end at /v1
    api_key=os.environ["OPENAI_API_KEY"],
)
agent = Agent(llm=llm, ...)
Pass the LLM everywhere, not just to the agents. CrewAI resolves planning_llm, function_calling_llm and any manager LLM separately — leave those unset and those calls go straight to the provider, bypassing the proxy entirely. You would see partial savings and no error. Set base_url to the /v1 root, not /v1/chat/completions; LiteLLM appends the route itself.

Agno

Agno's OpenAILike is the class it recommends for any OpenAI-compatible endpoint — it takes the same parameters as OpenAIChat and relaxes the OpenAI-only assumptions. Point base_url at the proxy and every model call the agent makes is compressed on the way out.

$ pip install agno openai
$ distil proxy --port 8788 --upstream https://api.openai.com &

import os
from agno.agent import Agent
from agno.models.openai.like import OpenAILike

agent = Agent(
    model=OpenAILike(
        id="gpt-4o",
        api_key=os.environ["OPENAI_API_KEY"],
        base_url="http://127.0.0.1:8788/v1",   # ← the only change
    )
)
agent.print_response("...")

Strands Agents

Strands' OpenAIModel forwards client_args straight to the underlying openai client, so base_url goes there. Nothing else in the agent changes.

$ pip install strands-agents
$ distil proxy --port 8788 --upstream https://api.openai.com &

import os
from strands import Agent
from strands.models.openai import OpenAIModel

model = OpenAIModel(
    client_args={
        "api_key": os.environ["OPENAI_API_KEY"],
        "base_url": "http://127.0.0.1:8788/v1",  # ← the only change
    },
    model_id="gpt-4o",
)
agent = Agent(model=model)
Why there is no distil-agno or distil-strands package. Both of these are two-line base_url redirects, and so is every other framework that speaks an OpenAI- or Anthropic-shaped API. distil is a proxy, so a framework is supported the moment it lets you set an endpoint — there is nothing to version, nothing to break on the framework's next release, and no per-framework code to maintain. The in-process hooks further down exist only for the cases where you want compression without running a proxy at all.

Vercel AI SDK (TypeScript)

import { createAnthropic } from "@ai-sdk/anthropic";
import { generateText } from "ai";

const anthropic = createAnthropic({
  baseURL: "http://127.0.0.1:8788",
  apiKey: process.env.ANTHROPIC_API_KEY,
});

const { text } = await generateText({
  model: anthropic("claude-opus-4-5"),
  prompt: "Hello!",
});

LangChain.js (TypeScript)

import { ChatAnthropic } from "@langchain/anthropic";

const model = new ChatAnthropic({
  model: "claude-opus-4-5",
  apiKey: process.env.ANTHROPIC_API_KEY,
  anthropicApiUrl: "http://127.0.0.1:8788",
  // older versions: clientOptions: { baseURL: "http://127.0.0.1:8788" }
});

const response = await model.invoke([
  ["human", "Hello!"],
]);

Google Gemini REST

# Start proxy pointing at the Gemini API
distil proxy --port 8788 --upstream https://generativelanguage.googleapis.com
import google.generativeai as genai

genai.configure(
    transport="rest",
    client_options={"api_endpoint": "http://127.0.0.1:8788"},
)
model = genai.GenerativeModel("gemini-1.5-pro")
response = model.generate_content("Hello!")

See examples/python_gemini.py for a full runnable example including tool-use turns. The proxy transparently compresses text parts (Tier-0 lossless) and functionResponse payloads (Tier-1 reversible digest). functionCall, inlineData, fileData, model-authored text, and systemInstruction are always passed through unchanged.


Claude Code plugin

The first-class integration: a session-first savings status line plus slash commands, shipped as a marketplace-format plugin (plugins/distil). Wire it in one step with distil setup (or distil onboard), then route the agent through compression with distil wrap -- claude.

CommandWhat it does
/distil-onboardSet up distil + a guided, tailored tour
/distilSavings report + how to route more traffic through distil
/distil-statsFull breakdown — tokens, cost, runs, per-trajectory bars
/distil-shadowLive decision-equivalence: did compression preserve the next action?
/distil-dashboardHTML savings page — session, lifetime, and decision-equivalence cards
/distil-doctorDiagnose the setup — ledger, shadow validation, proxy round-trip, wiring
/distil-certifyTrajectory-level certificate: bound how many solvable tasks compression may cost
/distil-badgeShareable badge of your measured savings

One pattern in every state: distil · <live> · total ▼<lifetime>. The live segment aggregates recent activity (last 15 min, all terminals), so it never flickers between sessions:

StateYou seeMeans
savingdistil · ▼12.0K · 40% smaller · $0.31 · total ▼27.0M · ✓eq 99%compressing your recent traffic
watchingdistil · ✓ on · waiting for a large read · total ▼27.0Mon, but no large content yet — savings come from big file/command output
idledistil · ✓ on · total ▼27.0Mset up and on, no recent traffic
not routeddistil · off — session not routed · total ▼27.0Mthis session goes straight to the provider — start it with distil wrap (or the always-on env) to compress. "on" always means routed, never merely installed.

= tokens saved · total = lifetime · ✓eq = decision-equivalence (shown only past 25 shadow samples — a rate over a handful is noise). Sharing the line with git/cwd/model? Set DISTIL_STATUSLINE=minimal for a two-fact segment: distil ▼75.0K · 27.0M total.


MCP server

A zero-dependency Model Context Protocol server (stdlib only — no SDK) exposes distil's reversible compression to any MCP client (Claude Desktop, IDEs, agents) over stdio:

claude mcp add distil -- distil mcp   # Claude Code — one line, done

Every other client takes the same stdio config — Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json), Cursor (.cursor/mcp.json), VS Code (.vscode/mcp.json):

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

No distil install? Run it from PyPI on demand — "command": "uvx", "args": ["--from", "distil-llm", "distil", "mcp"]. Restart the client after editing. To check the server without a client at all:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | distil mcp
ToolWhat it doesCalled when
distil_compress(text)Compact digest + an 8-hex handle; the original is stored locally (encrypted, 0600) and never returned until asked A tool returned something huge and carrying it verbatim is wasteful
distil_expand(handle)The exact original bytes — not a summaryThe digest lost a detail the agent now needs: a line, a value, a stack frame
distil_savings()Cumulative tokens/dollars from the local ledger Someone asks what distil has saved

Each tool carries MCP annotations (readOnlyHint, idempotentHint, openWorldHint: false), so a client knows distil_expand is a safe, repeatable, offline read instead of inferring it from prose.

This is the recall path, not the savings path. The MCP server does not compress your agent's traffic — distil wrap does that transparently, with no tool calls. What the MCP server adds is the other half: any agent, including one you never wrapped, can call distil_expand on a handle it sees in context and get the original back. Handles survive restarts and cross processes, and age out after DISTIL_RESTORE_TTL_DAYS (default 14).


In-process hooks (LiteLLM · LangChain · LangGraph)

Prefer not to run a sidecar? Compress the request in-process — the same reversible compression, no proxy. Every helper lazy-imports (or duck-types) its framework, so distil stays zero-dep.

# LiteLLM — drop-in for litellm.completion
from distil.integrations import litellm as distil_litellm
resp = distil_litellm.completion(model="claude-opus-4-8", messages=[...],
                                 distil_verbatim=True)  # optional, Tier-0 only

# LangChain — compress a message list before the model call (duck-typed)
from distil.integrations.langchain import compress_messages
msgs = compress_messages(state["messages"], verbatim=True)

# LangGraph — compress graph state right before the model node
from distil.integrations.langgraph import pre_model_hook
agent = create_react_agent(model, tools, pre_model_hook=pre_model_hook())

Tool/function messages get the reversible Tier-1 digest; human/system messages get Tier-0 lossless; the model's own words are never rewritten. compress() (LiteLLM), compress_messages() (LangChain), and pre_model_hook() (LangGraph) are framework-free and unit-tested. The LangGraph hook returns only the updated message list, so every other state field is left intact.


Observability headers

The proxy adds up to 9 response headers per compressed response. The first two appear on every compressed request; the rest are conditional:

HeaderMeaningCondition
x-distil-compressed: 1 Compression was applied this turn. Always (on compressed requests)
x-distil-tokens-saved: <n> Estimated input tokens saved (heuristic tokenizer). Always (on compressed requests)
x-distil-expanded: 1 A digest was resolved via the transparent expand loop. When --expand fired
x-distil-cache-prefix-msgs: <n> Leading messages left byte-identical vs the previous turn (the prompt-cache-read region) — the verifiable benefit of a prefix-freeze router, content-free. With --session-delta
x-distil-cache-refs: <n> Total cache-delta references this turn. With --session-delta
x-distil-cache-delta: <n> Delta-encoded references this turn. With --session-delta
x-distil-cache-tokens-saved: <n> Tokens saved by cache-delta encoding. With --session-delta
x-distil-output-shaping: light|aggressive Output shaping was injected at this level. When --shape-output fired
x-distil-shadow: sampled This request was sampled for shadow-mode decision-equivalence. When --shadow rate triggered

The managed gateway (distil gateway) additionally adds x-distil-tenant: <id> for per-tenant accounting.


Node.js / TypeScript

Distil's universal path is the proxy — it's language-agnostic, so JS/TS stacks need no Distil-specific package. Start the proxy (or wrap your agent) and point your SDK's baseURL at it:

# start the proxy (needs Python 3.11+ — see Install)
distil proxy --port 8788 --upstream https://api.anthropic.com
// then in your Node/TS app — no code change beyond baseURL
const client = new Anthropic({ baseURL: "http://localhost:8788" });

Or wrap an existing CLI/agent in one shot: distil wrap -- <your-command> starts the proxy and injects ANTHROPIC_BASE_URL automatically.

Homebrew

brew tap dshakes/tap
brew install dshakes/tap/distil
distil proxy --port 8788