llm-vcr
Record and replay LLM HTTP traffic for deterministic, key-free pytest runs.
Status: v0.6 — SSE streaming replay, sequential tool-call cassettes, OpenAI + Anthropic semantic matching, model-date normalize, and
llm-vcr diff.
60-second try
pip install pytest-llm-vcr
llm-vcr health
docker compose run --rm test # cassette replay, no API key
Why this vs alternatives
| Approach | Strength | Gap |
|---|---|---|
| llm-vcr | LLM-aware matching, redaction, SSE + sequential cassettes | httpx-focused today |
| VCR.py / pytest-recording | Mature generic HTTP cassettes | No model-date normalize or LLM-shaped diffs |
| Hand-written mocks | Fast, no network | Drift from live provider payloads |
| Live API in CI | Highest fidelity | Flaky, keyed, and expensive |
Problem
Testing code that calls LLMs is slow, flaky, and expensive. Hand-written mocks drift from reality. Generic HTTP cassettes (VCR.py) don't understand LLM request shapes or redact API keys well.
Key features (v0.6)
- pytest plugin —
@llm_vcrdecorator,llm_vcr_clientfixture,--llm-vcr-record - httpx transport — sync + async, including
client.stream(...) - YAML cassettes —
streaming: true+chunksfor SSE - Matching — exact (default) or
matcher="semantic"(volatile keys, model aliases, messages by role+content, tools by name); Anthropic Messages API viaapi.anthropic.comURL detection llm-vcr diff— show normalized differences between JSON bodies or cassette interactions- Automatic redaction — strips api_key,
x-api-key, token, authorization fields
Architecture
@pytest test
└── @llm_vcr decorator
└── VCRTransport (httpx)
├── replay mode → read cassette YAML
└── record mode → live HTTP + save cassette
| Component | Technology | Why |
|---|---|---|
| HTTP | httpx | Modern, sync+async, transport hooks |
| Cassettes | YAML | Readable diffs in PRs |
| Tests | pytest entry point | Zero-config discovery |
Installation
pip install pytest-llm-vcr
pip install -e ".[dev]" # from source
Local development
pip install -e ".[dev]"
pytest tests/ -v
llm-vcr health
llm-vcr diff left.json right.json
Docker
docker compose run --rm test
docker compose run --rm health
Configuration
| Variable | Default | Description |
|---|---|---|
LLM_VCR_CASSETTE_DIR |
tests/cassettes |
Cassette storage directory |
LLM_VCR_RECORD |
false |
Force record mode |
Usage
Replay (CI — no API key needed)
import httpx
from llm_vcr.plugin import llm_vcr
@llm_vcr("my_test")
def test_chat(client: httpx.Client) -> None:
resp = client.post(
"https://api.openai.com/v1/chat/completions",
json={"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Say hello"}]},
)
assert resp.json()["choices"][0]["message"]["content"]
Record new cassettes
LLM_VCR_RECORD=true pytest tests/ --llm-vcr-record
Diff normalized request bodies
llm-vcr diff left.json right.json
llm-vcr diff cassette_a.yaml cassette_b.yaml --index 0
Model strings with a trailing -YYYY-MM-DD normalize equal so dated aliases do not show as diffs.
Anthropic Messages API
Point httpx at https://api.anthropic.com/v1/messages the same way as OpenAI — record once, replay in CI without a key. Query strings (e.g. beta flags) are stripped for matching. Use matcher="semantic" to ignore metadata and tool_use / tool_result ids:
@llm_vcr("anthropic_chat", matcher="semantic")
def test_anthropic(client: httpx.Client) -> None:
resp = client.post(
"https://api.anthropic.com/v1/messages",
json={
"model": "claude-3-5-sonnet",
"max_tokens": 64,
"messages": [{"role": "user", "content": "Say hello"}],
},
headers={"x-api-key": "sk-ant-...", "anthropic-version": "2023-06-01"},
)
assert resp.json()["content"][0]["text"]
x-api-key is redacted in cassette bodies when present.
Running tests
pytest tests/ -v
Roadmap
- SSE streaming chunk replay
- Tool-call multi-step loops (
@llm_vcr(..., sequential=True)) - Async httpx transport (fixture + replay)
- Model-date normalize +
llm-vcr diff - OpenAI + Anthropic semantic request matching (
matcher="semantic")
Known limitations (v0.6)
- Sequential matching is opt-in (
sequential=True); default matching is still hash-based - Matching: exact hash (default) or semantic (volatile keys, model aliases, message/tool shape; Anthropic when host is
api.anthropic.com) - Record mode for streaming stores chunks, not per-event timestamps
License
MIT
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 pytest_llm_vcr-0.6.0.tar.gz.
File metadata
- Download URL: pytest_llm_vcr-0.6.0.tar.gz
- Upload date:
- Size: 26.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c00b7bf776aa8b9f48db123e63322e0a12bae24636b998d68b990ab6c45c49a6
|
|
| MD5 |
514b8e2758fa835c467ddc21648e408d
|
|
| BLAKE2b-256 |
739c116575a86aff90876a9fb4564be9f746665810036c08ffcddcacd8b1fce7
|
File details
Details for the file pytest_llm_vcr-0.6.0-py3-none-any.whl.
File metadata
- Download URL: pytest_llm_vcr-0.6.0-py3-none-any.whl
- Upload date:
- Size: 15.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43129e5ff9f19a34e93b1e59b0d70ce21c35f642c28e79e8d41888a026dbc820
|
|
| MD5 |
8293bf2115f8a7ac18fddbbd102b41b9
|
|
| BLAKE2b-256 |
3e3b0361a77fefe36af01ca33d81d7d929a4652d7f730567d7e14b4ca95d31c3
|