restitch
v0.1.0
Published
See your agent runs as runs, not as forty disconnected HTTP requests.
Maintainers
Readme
Restitch
A local-first reverse proxy that reconstructs LLM agent runs from raw HTTP traffic.
See your agent runs as runs, not as forty disconnected HTTP requests. No SDK, no headers, no code changes.
npx restitch start --daemon
eval "$(restitch env)"
claude "explain this repo"Then open http://localhost:7357.
The problem
When you give a coding agent a single instruction, it doesn't make one API call. It makes forty. It thinks, runs a command, reads the result, thinks again, edits a file, checks its work. Each of those is a separate, independent HTTP request, and none of them knows about the others.
Every proxy-based tool in this space forwards those bytes correctly. None of them group the requests into runs unless the client volunteers a session id, and coding agents — Claude Code, Cursor, Aider, opencode — do not volunteer one. So on the proxy path they all degrade to a flat list of disconnected requests.
Restitch works it out from the traffic itself.
How the grouping works
Turn N+1 of an agent loop resends turn N's entire message list. That prefix relationship is the signal.
After each response, Restitch indexes a fingerprint of the conversation state the next request should open with. On each new request it walks backwards from the full message list, trimming up to six trailing messages, and looks for a match. That is six hashes per request rather than the O(n²) of hashing every prefix.
The fingerprint deliberately ignores everything a client legitimately changes
between turns — cache_control markers move as the client slides its cache
breakpoints, block ids differ, image bytes are never hashed, thinking blocks
aren't resent verbatim. Each of those breaks the chain if you leave it in.
If you own the client, skip the heuristic entirely:
x-restitch-session: my-run-idRestitch strips the header before forwarding, so the provider never sees it.
What you get
▚ session aSA5vHC_qNBl new conversation
│
├─ turn 1 claude-opus-5
│ ├─ ⚙ Bash toolu_grep
│ └─ tool_use 4in/640out cache=68.0kr/2.8kw
│
├─ turn 2 claude-opus-5
│ ├─ ✓ Bash returned 64ms round trip
│ ├─ ⚙ Read toolu_read
│ └─ tool_use 4in/640out cache=68.0kr/2.8kw
│
├─ turn 3 claude-opus-5
│ ├─ ✓ Read returned 63ms round trip
│ └─ end_turn 4in/640out cache=68.0kr/2.8kwPlus a dashboard at http://localhost:7357 with a session list, a span
timeline, a latency waterfall, a token breakdown, and a context chain view
that renders the conversation exactly as it was sent — which is how you answer
"why is this call costing 50k tokens" in about two seconds.
Install
Needs Node 22.13 or newer (node:sqlite is built in from there).
npm install -g restitch # or just use npxProviders
| Client | Set this |
|---|---|
| Anthropic | ANTHROPIC_BASE_URL=http://localhost:7357/anthropic |
| OpenAI | OPENAI_BASE_URL=http://localhost:7357/openai/v1 |
| Anything OpenAI-compatible | restitch provider add <name> <base-url> |
restitch env prints the export lines for whatever you have configured, which
is what eval "$(restitch env)" above is doing.
Groq, Together, Fireworks, OpenRouter, Azure, Ollama and vLLM all speak the
OpenAI wire format, so they work through provider add with no new code:
restitch provider add groq https://api.groq.com/openai/v1
# base URL: http://localhost:7357/custom/groqCommands
restitch start [--daemon] run the proxy and dashboard
restitch stop stop a backgrounded proxy
restitch status running? seen traffic?
restitch env [--provider name] print export lines
restitch session list list recorded sessions
restitch session show <id> one session, tools nested under turns
restitch session analyze <id> explain what went wrong
restitch session export <id> --format json | har
restitch session close <id>
restitch session delete <id> --yes
restitch provider add|list|remove
restitch config list|get|set|path
restitch pricing which models have a price
restitch statsSession ids accept a unique prefix, so restitch session show aSA5 works.
Cost
restitch pricing shows which of your models are priced. Anthropic and OpenAI
ship with the table; custom providers deliberately do not, because each
sets its own rates and a guessed price is worse than none. Add yours to
~/.restitch/config.json:
{
"pricingOverrides": {
"llama-3.1-8b-instant": { "inputPerMToken": 0.05, "outputPerMToken": 0.08 }
}
}Existing runs are re-priced on the next start. An unpriced model reports cost
as unknown, never $0.00.
Root cause analysis
restitch session analyze <id>Names the specific turn or tool call that broke the chain. Needs
ANTHROPIC_API_KEY or OPENAI_API_KEY, plus the matching SDK:
npm install @anthropic-ai/sdk # or: npm install openaiRestitch does not install either for you — together they are about 19 MB, and only this one command needs them.
Your prompts are never sent. Only a structural summary goes to the analysis model: turns, stop reasons, latencies, token counts, and tool names with truncated inputs and results. Check for yourself before spending anything:
restitch session analyze <id> --summary # prints exactly what would be sent, calls nothingThe call goes direct to the provider, not through the proxy, so analysis never shows up in its own trace.
Privacy
- Credentials are forwarded and never stored.
authorizationandx-api-keyare redacted at the boundary, before anything reaches storage. - Nothing leaves your machine except your own API traffic, which was going to the provider anyway, and root cause analysis if you explicitly run it.
- The proxy binds 127.0.0.1. A process holding in-flight API credentials
should not be reachable from the network.
--hostoverrides it and prints a warning. HostandOriginare validated on every path, including the WebSocket upgrade. Binding loopback alone does not stop a web page you visit from reaching a local service — an attacker can re-resolve their own domain to 127.0.0.1, at which point the browser treats it as same-origin and CORS protects nothing. Requests arriving for any host other than localhost are refused, so that does not work here.restitch config set storePayloads falsekeeps metadata — model, tokens, latency, tool names, stop reasons — and drops message bodies.
Known limitations
Real, and documented rather than hidden. This category has enough overclaiming in it already.
| Scenario | What happens | What to do |
|---|---|---|
| Your client compacts or truncates history | A new session starts mid-run | Use the x-restitch-session header |
| Two identical conversations run in parallel | They may chain into one session | Use the header |
| More than 6 messages appended per turn | The chain breaks | restitch config set maxTrim 10 |
| Tools routed through a vendor backend (Copilot subscription, Cursor Pro, ChatGPT web) | Not capturable at all | Nothing. These never talk to the API directly. |
| OpenAI streaming without stream_options | Token counts unknown | Nothing — Restitch will not inject the parameter, and reports unknown rather than zero |
| A model with no price | Cost reports as unknown | Add it to pricingOverrides |
The vendor-backend row is the important one. Restitch is a reverse proxy, not a MITM forward proxy — no certificate to install, no pinning to break — and the cost of that choice is that tools routing through a vendor's own backend cannot be seen at all.
What it will not do
- Modify your request. Not even to add
stream_optionsfor OpenAI token counts, which would be genuinely useful. Tokens are reported as unknown instead. - Delay or corrupt your response. Bytes are forwarded first and parsed second, on a detached path. The test suite sabotages the parser deliberately to prove a broken one cannot reach your stream.
- Report a number it isn't sure about. Unknown cost is null, unknown tokens
say unknown, a truncated stream says
TRUNCATED, and a tool that never returned says so.
Configuration
~/.restitch/config.json. Precedence is flag, then environment variable, then
file, then default.
| Key | Default |
|---|---|
| port | 7357 |
| host | 127.0.0.1 |
| dbPath | ~/.restitch/restitch.db |
| storePayloads | true |
| storeSystemPrompts | true |
| retentionDays | 30 |
| sessionIdleMinutes | 15 |
| maxTrim | 6 |
| analysisProvider | auto |
| analysisModel | provider default |
Development
npm install
npm run build # server + dashboard
npm test
npm run typecheck # strict, with noUncheckedIndexedAccessTests run offline with no API key. Fixtures under tests/fixtures/ include
both real captures from api.anthropic.com (credentials scrubbed) and
hand-authored ones that are deliberately adversarial about chunk boundaries.
Prior art
The category is occupied and this README will not pretend otherwise.
| Project | Capture | Session grouping | Tool calls on proxy path | Cost |
|---|---|---|---|---|
| agenttap | Proxy + OTLP | Needs x-session-id | No | No |
| llm-visuals | Proxy | None | Partial | Yes |
| agentlens | SDK / OTel / MCP | Free, from instrumentation | Yes | Yes |
| Langfuse, LangSmith, Helicone, Braintrust | SDK / OTel / proxy | Free, from instrumentation | Yes | Yes |
If you can add an SDK to your code, several of those are more mature and you should probably use one. Restitch is for the case where you cannot: a closed-source agent, a tool you didn't write, a binary you can only point at a different base URL.
License
MIT
