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
Microsoft AutoGen Python base_url= in OpenAIChatCompletionClient({…}) http://127.0.0.1:8788/v1 below
LlamaIndex Python api_base= in OpenAI({…}) 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

Agents distil wrap routes for you

No configuration and no code change: distil wrap -- <agent> starts a proxy, points the agent at it, and restores everything on exit. Most read an environment variable; a few have no such contract and route through a config file wrap manages for that one session. This table and the next are generated from distil/targets.py — distil wrap --list prints the same thing in your terminal.

AgentCommandMechanismRouting knobWire shape
aiderdistil wrap -- aiderenvironment variableOPENAI_API_BASEOpenAI Chat Completions
Claude Codedistil wrap -- claudeenvironment variableANTHROPIC_BASE_URLAnthropic Messages
Codex CLIdistil wrap -- codexenvironment variableOPENAI_BASE_URLOpenAI Responses
Gemini CLIdistil wrap -- geminienvironment variableGOOGLE_GEMINI_BASE_URLGemini generateContent
GitHub Copilot CLIdistil wrap -- copilotenvironment variableCOPILOT_PROVIDER_BASE_URLAnthropic Messages
goosedistil wrap -- gooseenvironment variableOPENAI_HOSTOpenAI Chat Completions
Grok CLIdistil wrap -- grokenvironment variableGROK_MODELS_BASE_URLOpenAI Chat Completions
Kilo Code CLIdistil wrap -- kiloenvironment variableKILO_CONFIG_CONTENTAnthropic Messages or OpenAI Chat Completions
Kimi CLIdistil wrap -- kimienvironment variableKIMI_BASE_URLOpenAI Chat Completions
Mistral Vibedistil wrap -- vibeenvironment variableVIBE_PROVIDERSOpenAI Chat Completions
OpenCodedistil wrap -- opencodeenvironment variableOPENAI_BASE_URLOpenAI Responses
OpenHandsdistil wrap -- openhandsenvironment variableLLM_BASE_URLOpenAI Chat Completions
Qwen Codedistil wrap -- qwenenvironment variableOPENAI_BASE_URLOpenAI Chat Completions
Clinedistil wrap -- clineconfig fileproviders.json → providers.<id>.settings.baseUrlAnthropic Messages or OpenAI Chat Completions
Continuedistil wrap -- cnconfig fileconfig.yaml → models[].apiBase (via --config)Anthropic Messages or OpenAI Chat Completions
Crushdistil wrap -- crushconfig filecrush.json → providers.<id>.base_urlAnthropic Messages or OpenAI Chat Completions
Factory Droiddistil wrap -- droidconfig filesettings.local.json → customModels[].baseUrlOpenAI Chat Completions
Oh My Pidistil wrap -- ompconfig filemodels.yml → baseUrlAnthropic Messages or OpenAI Chat Completions

Agents it cannot reach — and what was checked

Each of these was read against its own primary documentation on the date shown. Where a base-URL setting exists, point it at the URL distil proxy prints (http://127.0.0.1:8788/v1) and verify with distil doctor; where it says — there is no such setting to point. Full reasoning: IDE-AGENTS.md.

AgentIts own base-URL settingWhy wrap cannotVerified
Amp— (HTTP_PROXY/HTTPS_PROXY only)re-checked: the CLI settings reference still has no base-URL key; amp.url belongs to the VS Code extension, not the CLIverified 2026-09-16
Augment (auggie)—AUGMENT_SESSION_AUTH carries the session token; no base-URL variable or config key is documentedverified 2026-09-16
Continue (VS Code extension)~/.continue/config.yaml → models[].apiBaseapiBase 'can be used to override the default API base', but the extension is started by the editor — no argv to wrap, and the file is editor-wide rather than per-session. The Continue CLI is a different tool and `distil wrap -- cn` does reach itverified 2026-09-16
Cursor CLI— (HTTP_PROXY/HTTPS_PROXY only)cli-config.json publishes no base-URL field; the only network knob is a whole-process HTTP proxy, not a per-request base URL. Its binary is `agent`, a name too generic for distil to claimverified 2026-09-16
Google Antigravity—models are plan-selected from a fixed list; no BYOK and no endpoint override is documentedverified 2026-09-16
JetBrains Juniemodel profile → baseUrldocs render client-side and return nothing over a plain fetch; the config shape could not be verifiedverified 2026-09-16
OpenClaw~/.openclaw/openclaw.json → models.providers.<id>.baseUrlthe knob is verified, but OpenClaw's own README puts the model connection in a Gateway the CLI merely 'connects to' — one local control plane shared with Discord/WhatsApp/Slack channels and, on a team install, other people. A session-scoped patch would reconfigure a daemon serving them, and restore it mid-flight when one terminal exitsverified 2026-09-16
Roo CodeAPI Provider → "OpenAI Compatible" → Base URLa VS Code extension with no CLI, whose configuration profiles live in VS Code's own Secret Storage ('stored securely in VSCode's Secret Storage and never exposed in plain text') — there is no config file to patchverified 2026-09-16
Snowflake Cortex Code—no published base-URL overrideverified 2026-09-16
Sourcegraph Codysite config → modelConfiguration.providerOverrides (server-side)the override is an admin setting on the Sourcegraph instance, not on the client — a distil gateway in front of that instance is the fit, not wrapverified 2026-09-16
Tabnine—clients point at a Tabnine server, not at an LLM endpoint; the CLI docs publish no model base-URL overrideverified 2026-09-16
TraeSettings → Models → custom modelcustom models exist, but every docs path returns the same client-rendered shell over a plain fetch — the config shape could not be verified against an authoritative sourceverified 2026-09-16
VS Code Copilot (extension)—the extension terminates at GitHub's own service and exposes no endpoint override; BYOK is a Copilot CLI feature, and that CLI IS wrappedverified 2026-09-16
WarpSettings → custom inference endpoint (public HTTPS URL only)Warp DOES publish an endpoint override now (the older 'no override at all' note was stale) — but the agent harness runs on Warp's servers and the docs reject localhost and private addresses, so a local distil proxy cannot be the targetverified 2026-09-16
WindsurfSettings → Cascade → custom endpointBYOK accepts a provider API KEY only (Claude 4 family), with no endpoint field in the documented flowverified 2026-09-16
ZCode (z.ai)Settings → Providers → Base URL (Anthropic or OpenAI protocol)z.ai's own page calls it an Agentic Development Environment, a desktop app with no CLI — the Base URL field is verified and does take a local proxy, but there is no process for wrap to launch or scope a config change toverified 2026-09-16
Zed agentsettings.json → language_models.anthropic_compatible.<name>.api_url (or openai_compatible)no single key redirects it: the BUILT-IN anthropic provider documents only available_models, never an api_url, so routing means ADDING an anthropic_compatible provider the user must then pick by hand in the model dropdown — and Zed's own Agent Settings page writes settings.json while it runs, so a session-scoped patch would be racing the editor for the fileverified 2026-09-16

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)

Microsoft AutoGen

AutoGen's OpenAIChatCompletionClient takes base_url directly, same as the raw OpenAI client it wraps.

$ pip install "autogen-agentchat" "autogen-ext[openai]"
$ distil proxy --port 8788 --upstream https://api.openai.com &

import os
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient

client = OpenAIChatCompletionClient(
    model="gpt-4o",
    api_key=os.environ["OPENAI_API_KEY"],
    base_url="http://127.0.0.1:8788/v1",  # ← the only change
)
agent = AssistantAgent("assistant", model_client=client)

Prefer no sidecar at all? distil.integrations.autogen wraps a ChatCompletionClient or a FunctionTool callable in-process — see AutoGen × Distil for both.

Why there is no distil-agno, distil-strands, or distil-autogen package. All three 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 (and AutoGen's own page) exist only for the cases where you want compression without running a proxy at all.

LlamaIndex

LlamaIndex's OpenAI/Anthropic LLM classes take api_base directly, same as the raw client each one wraps.

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

from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-5", api_base="http://127.0.0.1:8788/v1")  # ← the only change
query_engine = index.as_query_engine(llm=llm)

Prefer no sidecar at all? distil.integrations.llamaindex gives you a node postprocessor for retrieved context, an LLM wrapper, and a FunctionTool callable wrapper, all in-process — see LlamaIndex × Distil for all three.

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 · ⚠de 97.5% (398)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 · de = decision-equivalence (shown only past the reporting floor, 50 A/B + 30 A/A shadow samples — a rate over a handful is noise; ✓ at 99% and above, ⚠ under 99%, ✗ under 95%). 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 · AutoGen · LlamaIndex)

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())

# AutoGen — compress a ChatCompletionClient's outgoing messages, or one tool's output
from distil.integrations.autogen import DistilModelClient, compressing_tool
client = DistilModelClient(OpenAIChatCompletionClient(model="gpt-4o"))
tool = FunctionTool(compressing_tool(get_weather), description="...")

# LlamaIndex — compress retrieved nodes, an LLM's outgoing calls, or one tool's output
from distil.integrations.llamaindex import DistilNodePostprocessor, DistilLLM
query_engine = index.as_query_engine(
    llm=DistilLLM(OpenAI(model="gpt-5")),
    node_postprocessors=[DistilNodePostprocessor()],
)

Tool/function messages and retrieved nodes 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), pre_model_hook() (LangGraph), DistilModelClient/compressing_tool() (AutoGen), and DistilLLM/DistilNodePostprocessor (LlamaIndex) are framework-free and unit-tested. The LangGraph hook returns only the updated message list, so every other state field is left intact; DistilModelClient delegates every attribute it doesn't wrap straight to the real client, and DistilLLM re-types the LLM you pass as a transparent subclass of its own class, so it still satisfies the isinstance checks LlamaIndex performs on an llm= argument.

LangChain / LangGraph as a package — langchain-distil

The two hooks above are also published as a standalone package, listed in LangChain’s own community middleware integrations. It is a thin wrapper over exactly those hooks — same certified compression path, nothing re-implemented — and it pulls distil-llm in as a dependency.

$ pip install langchain-distil

from langchain_distil import compress_messages, pre_model_hook, as_runnable

msgs  = compress_messages(msgs)                                   # a message list
graph = create_react_agent(model, tools, pre_model_hook=pre_model_hook())  # LangGraph state
chain = as_runnable() | llm                                       # or a chain step

as_runnable() imports langchain-core lazily, so importing the package never requires it. See LangChain × Distil or LangGraph × Distil for the full page.


ASGI middleware — when you host the endpoint

Every hook above sits in a client that calls out to a provider. If instead your own backend is the thing building the provider request — a FastAPI/Starlette/Litestar app that forwards to Anthropic/OpenAI/Gemini itself — wrap it once with DistilMiddleware, pure ASGI, no Starlette import:

from distil.integrations.asgi import DistilMiddleware

app = DistilMiddleware(app)  # wraps any ASGI 3 app; compresses matching POST bodies

It reuses the exact path detection and reversible compression distil proxy uses, so a handle minted here expands anywhere. See ASGI Middleware × Distil for the full page.


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.9+ — 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