VCR for LLM APIs — record and replay OpenAI/Anthropic/Gemini calls including streaming.
Project description
Reel
VCR for LLM APIs. Record real OpenAI / Anthropic / Gemini calls once, replay them deterministically forever — including streaming, tool calls, and timing. No SDK changes. No real network in CI. No surprise spend.
The 30-second pitch
# Start once
uv run reel auto -c tape.jsonl
# Point any LLM SDK at the proxy
export OPENAI_BASE_URL=http://127.0.0.1:7878/v1
# Run your code — first run records, every run after replays in ~3ms with $0 spend
python my_app.py
That's the entire workflow. The cassette is plain JSONL: diff it in PRs, grep it, share it. Secrets and PII are scrubbed at capture time, so it's safe to commit.
your code ─► 127.0.0.1:7878 ─► reel proxy ─► api.openai.com
│
├── writes JSONL on first call
└── replays on next calls
Why people pick it up
| Pain | What Reel does about it |
|---|---|
pytest burns $$ on every CI run |
Replay mode — zero network, no API key needed |
| Prompt iteration is slow + expensive | Captured responses replay in ~3ms; iterate the prompt locally for free |
| Production bug, no way to repro locally | Hand a colleague the JSONL — they replay your prod traffic byte-for-byte |
| Three providers, three mock libraries | One proxy serves OpenAI, Anthropic, and Gemini, including SSE streaming |
Quickstart
# From PyPI once v0.1.0 is published:
pip install reel-vcr # distribution name is reel-vcr;
# binary + import path stay `reel`
# Or from source today:
git clone https://github.com/tathagat22/reel && cd reel && uv sync
uv run reel auto -c demo.jsonl &
export OPENAI_BASE_URL=http://127.0.0.1:7878/v1
python -c "from openai import OpenAI; print(OpenAI().chat.completions.create(model='gpt-5', messages=[{'role':'user','content':'Hi'}]).choices[0].message.content)"
Note: the bare
reelname on PyPI was already taken by an unrelated async-subprocess library, so the distribution name isreel-vcr. The CLI binary, GitHub repo, and Python import path (import reel) all stay asreel.
Drop into an existing pytest suite
# tests/test_chatbot.py
def test_summarize(reel_cassette): # ← that's the entire integration
from openai import OpenAI
OpenAI().chat.completions.create(...)
The pytest plugin auto-registers; no conftest.py edits needed. First run records to tests/cassettes/test_chatbot/test_summarize.jsonl; subsequent runs replay it.
pytest # first time: records (costs real money)
pytest # every time after: replays (free, ~3ms)
pytest --reel-mode replay # CI mode — fails loud on uncaptured calls
Full guide: docs/guides/pytest.md.
Commands
| Command | What it does |
|---|---|
reel auto -c <path> |
Replay if cached, else record (the dev-loop default) |
reel record -c <path> |
Always forward + capture (initial capture, refresh) |
reel replay -c <path> |
Cassette only; 404 on miss (CI mode) |
reel ui -c <path> |
Local web UI to browse and search cassettes |
reel inspect -c <path> |
Rich-table view of entries, with composable filters |
reel cost -c <path> |
$$ aggregate — what you spent (or would have) |
reel diff -l A -r B |
Show drift between two cassettes |
reel stats -c <path> |
Counts, error rate, token totals, TTFT distribution |
reel redact -c <path> |
Post-hoc scrub secrets / PII |
reel doctor |
Health check: ports, upstreams, write perms |
reel version |
Print the installed version |
What's in the box
- OpenAI / Anthropic / Gemini — single proxy, routed by path or explicit
/<provider>/…URL prefix - SSE streaming with timing fidelity — chunks captured per-ms; replay reproduces TTFT and inter-chunk gaps;
--timing realtime | fast | slow=N - Four match modes —
exact,normalized(default),ignore-fields(great for per-callrequest_id),fuzzy(embedding similarity, optionalreel[fuzzy]) - Capture-time redaction — OpenAI / Anthropic / Google / GitHub / AWS / Slack key shapes + Bearer tokens, always. PII (email + phone) on by default; opt out with
REEL_REDACT_PII=0 - pytest plugin — auto-discovered via
pytest11entry point;reel_cassettefixture /@cassettedecorator /@pytest.mark.cassette - Analytics CLI — inspect / cost / diff / stats with composable filters and pricing tables for the major models
- Structured logs —
--log-format jsonforjq-pipeable per-request observability - JSONL cassettes — git-friendly, append-only, ~5 KB per buffered call
- Pre-commit hook to refuse any cassette that contains a detectable secret
- 344 tests including multi-provider E2E + a pytester-driven plugin suite
Architecture
src/reel/
├── proxy/ # HTTP + SSE core (forwarder, modes, stream, logs)
├── adapters/ # openai · anthropic · gemini (one ProviderAdapter interface)
├── cassette/ # schema · writer · reader · matcher · body codec · store
├── redact/ # secret + PII scrubbing
├── analytics/ # filters · cost · diff · stats (pure over CassetteEntry)
├── inspector/ # `reel ui` — Starlette + HTMX + Pico.css
├── cli/ # typer commands wired into `reel` entry point
└── sdk/ # @cassette decorator + pytest plugin
Deeper dive: docs/architecture.md. Roadmap: docs/SPRINT_SHEET.md.
Status
Sprints 1-6 of 6 are shipped. Currently pre-alpha — usable today from source, PyPI publish + v0.1.0 tag is the last open item.
Development
uv sync
make check # ruff + pyright (strict) + pytest — must pass before every commit
uv run reel auto -c ./scratch/test.jsonl
CI runs lint + format + typecheck + tests on Python 3.11 / 3.12 / 3.13 — see .github/workflows/ci.yml. Docs deploy to GitHub Pages — .github/workflows/docs.yml.
Contributing
PRs welcome — see CONTRIBUTING.md. The pre-commit hook will refuse any cassette with a detectable secret, so capture won't silently leak.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reel_vcr-0.1.0.tar.gz.
File metadata
- Download URL: reel_vcr-0.1.0.tar.gz
- Upload date:
- Size: 278.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ca057d43c16159a9a8a95c9ac1c570a5336ce7d9f151c139744bd178d9eb540
|
|
| MD5 |
da44b64419c07704ef429d2cb62bea81
|
|
| BLAKE2b-256 |
8e24ac78346be60fc32486305f2f1edacb7fd0999fbd2e086352bf208c8c918a
|
Provenance
The following attestation bundles were made for reel_vcr-0.1.0.tar.gz:
Publisher:
release.yml on tathagat22/reel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reel_vcr-0.1.0.tar.gz -
Subject digest:
5ca057d43c16159a9a8a95c9ac1c570a5336ce7d9f151c139744bd178d9eb540 - Sigstore transparency entry: 1546972371
- Sigstore integration time:
-
Permalink:
tathagat22/reel@1da3dec41377ed4822fbf9d51e54c4555fcfd982 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tathagat22
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1da3dec41377ed4822fbf9d51e54c4555fcfd982 -
Trigger Event:
push
-
Statement type:
File details
Details for the file reel_vcr-0.1.0-py3-none-any.whl.
File metadata
- Download URL: reel_vcr-0.1.0-py3-none-any.whl
- Upload date:
- Size: 96.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d592931448ab44f1c06f09ec076435e23a52c6a889da5b42d79ea2392a0d631
|
|
| MD5 |
b8d037bac7713c691b03b884da4faef6
|
|
| BLAKE2b-256 |
80e471f33502e1d7dcbe59c1e232e5ff11ab32a61501718c19c20812e0f802c9
|
Provenance
The following attestation bundles were made for reel_vcr-0.1.0-py3-none-any.whl:
Publisher:
release.yml on tathagat22/reel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reel_vcr-0.1.0-py3-none-any.whl -
Subject digest:
2d592931448ab44f1c06f09ec076435e23a52c6a889da5b42d79ea2392a0d631 - Sigstore transparency entry: 1546972395
- Sigstore integration time:
-
Permalink:
tathagat22/reel@1da3dec41377ed4822fbf9d51e54c4555fcfd982 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/tathagat22
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1da3dec41377ed4822fbf9d51e54c4555fcfd982 -
Trigger Event:
push
-
Statement type: