Record and replay LLM API responses for fast, free, deterministic tests
Project description
llmvcr
Record and replay LLM API responses for fast, free, deterministic tests.
The Problem
Testing code that calls an LLM API is painful:
| Problem | Impact |
|---|---|
| ๐ข Slow | Each test waits 2โ10 seconds for a real API response |
| ๐ธ Expensive | Every test run costs real money |
| ๐ฒ Flaky | Same prompt, different response โ random test failures |
| ๐ก Needs internet | Tests break offline or when the API is down |
| ๐ Secrets in CI | You must inject API keys into every environment |
llmvcr solves all five by recording real LLM responses once and replaying them instantly โ for free, offline, with 100% identical results every time.
How It Works
Just like VCR.py records and replays HTTP requests, llmvcr records and replays LLM API responses. But unlike VCR.py, it works at the SDK layer โ not the raw HTTP layer โ so it handles streaming, never touches your API key, and produces clean human-readable YAML cassettes.
First run โ hits real API โ saves response to cassette YAML
Every run after โ reads from cassette โ instant, free, offline
Supported Providers
| Provider | SDK | Models | Install |
|---|---|---|---|
| OpenAI | openai |
GPT-4, GPT-4o, GPT-3.5 | pip install llmvcr[openai] |
| Anthropic | anthropic |
Claude 3.5, Claude 3 | pip install llmvcr[anthropic] |
| Google Gemini | google-genai |
Gemini 2.0, 1.5 | pip install llmvcr[gemini] |
| Llama (local) | ollama |
Llama 3.1, 3.2, Mistral, Qwen | pip install llmvcr[ollama] |
| Llama (cloud) | groq |
Llama 3.3 70B, 3.1 8B | pip install llmvcr[groq] |
Installation
# Core + one provider
pip install llmvcr[openai]
pip install llmvcr[anthropic]
pip install llmvcr[gemini]
pip install llmvcr[ollama]
pip install llmvcr[groq]
# Everything at once
pip install llmvcr[all]
Ollama note: Also requires the Ollama app running locally. Download from ollama.com, then
ollama pull llama3.1.
Quick Start
1. The @cassette decorator (recommended for tests)
Add one decorator to your test. First run records, every run after replays.
import llmvcr
import openai
@llmvcr.cassette("cassettes/summarize.yaml")
def test_summarize():
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Summarize AI in one sentence."}]
)
assert "intelligence" in response.choices[0].message.content
# First run: hits OpenAI API โ saves to cassettes/summarize.yaml
# Every run: reads from cassette โ instant, free, offline
2. Context managers (for scripts and notebooks)
import llmvcr
import openai
# Record
with llmvcr.record("cassettes/demo.yaml"):
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello!"}]
)
# Replay (never hits the API)
with llmvcr.playback("cassettes/demo.yaml"):
response = openai.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello!"}]
)
print(response.choices[0].message.content) # instant
3. Explicit modes
# auto (default) โ record if cassette missing, replay if it exists
@llmvcr.cassette("cassettes/test.yaml")
# record โ always re-record, overwrite cassette
@llmvcr.cassette("cassettes/test.yaml", mode="record")
# playback โ always replay, raise error if cassette missing
@llmvcr.cassette("cassettes/test.yaml", mode="playback")
Provider Examples
OpenAI
import llmvcr
import openai
@llmvcr.cassette("cassettes/openai_test.yaml", provider="openai")
def test_openai():
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "What is Python?"}]
)
assert "programming" in response.choices[0].message.content
Anthropic
import llmvcr
import anthropic
@llmvcr.cassette("cassettes/claude_test.yaml", provider="anthropic")
def test_anthropic():
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=100,
messages=[{"role": "user", "content": "What is Python?"}]
)
assert "programming" in response.content[0].text
Google Gemini
import llmvcr
from google import genai
from google.genai import types
@llmvcr.cassette("cassettes/gemini_test.yaml", provider="gemini")
def test_gemini():
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.0-flash",
contents="What is Python?",
config=types.GenerateContentConfig(
system_instruction="You are a concise assistant."
)
)
assert "programming" in response.text
Llama via Ollama (local)
import llmvcr
import ollama
@llmvcr.cassette("cassettes/llama_local_test.yaml", provider="ollama")
def test_ollama():
response = ollama.chat(
model="llama3.1",
messages=[{"role": "user", "content": "What is Python?"}]
)
assert "programming" in response.message.content
# also works: response["message"]["content"]
Llama via Groq (cloud)
import llmvcr
from groq import Groq
@llmvcr.cassette("cassettes/llama_groq_test.yaml", provider="groq")
def test_groq():
client = Groq()
response = client.chat.completions.create(
model="llama-3.3-70b-versatile",
messages=[{"role": "user", "content": "What is Python?"}]
)
assert "programming" in response.choices[0].message.content
What a Cassette Looks Like
After recording, a clean human-readable YAML file is saved โ no binary blobs, no API keys, safe to commit to git:
llmvcr_version: '0.1'
provider: openai
recorded_at: '2025-03-03T10:24:00Z'
interactions:
- request:
model: gpt-4
messages:
- role: user
content: Summarize AI in one sentence.
response:
id: chatcmpl-abc123
choices:
- message:
role: assistant
content: Artificial intelligence is the simulation of human-like reasoning
and decision-making by computer systems.
usage:
prompt_tokens: 14
completion_tokens: 22
total_tokens: 36
You can review it, edit it, and diff it in pull requests just like any other file.
CLI
# Inspect a cassette โ see stored interactions, token counts, models
llmvcr info cassettes/demo.yaml
# Record a script to a cassette
llmvcr record --output cassettes/demo.yaml python my_script.py
# Replay a script using a cassette (no API call made)
llmvcr playback --cassette cassettes/demo.yaml python my_script.py
Example llmvcr info output:
Cassette: cassettes/demo.yaml
Provider: openai
Recorded at: 2025-03-03T10:24:00Z
Version: 0.1
Interactions: 2
[1] model=gpt-4
prompt: 'Summarize AI in one sentence.'
response: 'Artificial intelligence is the simulation of...'
tokens: 36
[2] model=gpt-4
prompt: 'What is Python?'
response: 'Python is a high-level programming language...'
tokens: 38
Why Not VCR.py or pytest-recording?
Those tools were built for generic HTTP APIs. LLMs break them in several ways:
| Issue | VCR.py / pytest-recording | llmvcr |
|---|---|---|
| Streaming responses | โ Crashes with chunked read errors | โ Records and replays natively |
| API key in cassette | โ Saved in plaintext by default | โ Never sees credentials |
Adding temperature=0.7 |
โ Breaks cassette match | โ Ignored, match still works |
| Gzip-encoded responses | โ Saves as unreadable binary | โ Always clean YAML |
| Zero-config setup | โ Requires conftest.py boilerplate | โ Works out of the box |
The root cause: VCR.py intercepts raw HTTP bytes. llmvcr intercepts at the SDK layer, where responses are already decoded Python objects โ no gzip, no auth headers, no socket state.
Limitations
- Changing prompts, message structure, provider, or model may require re-recording a cassette.
- Request matching is based on the serialized request content used by the provider integration.
- Provider SDK changes may require updates in future
llmvcrreleases. - Local-provider workflows like Ollama require the model/runtime to be available when recording.
Running the Tests
Install dev dependencies
# Clone the repo
git clone https://github.com/shahid-43/llmvcr
cd llmvcr
# Install in editable mode with dev dependencies
pip install -e ".[all]"
pip install pytest
Run all tests
pytest
Run with verbose output (see each test name)
pytest -v
Run a specific test file
pytest tests/test_matching.py
pytest tests/test_storage.py
pytest tests/test_recorder.py
pytest tests/test_cassettes.py
pytest tests/test_new_providers.py
Run a specific test by name
pytest -v -k "test_playback_ignores_temperature"
Run with coverage report
pip install pytest-cov
pytest --cov=llmvcr --cov-report=term-missing
Expected output
============================= test session starts ==============================
collected 81 items
tests/test_cassettes.py .................... [ 25%]
tests/test_matching.py ......... [ 36%]
tests/test_new_providers.py ................................... [ 79%]
tests/test_recorder.py ......... [ 90%]
tests/test_storage.py ....... [100%]
============================== 81 passed in 0.70s ==============================
All 81 tests run without any API keys โ they use pre-recorded cassettes in
tests/cassettes/.
Project Structure
llmvcr/
โโโ pyproject.toml # Package metadata and build config
โโโ requirements.txt # All dependencies listed
โโโ README.md
โโโ LICENSE
โ
โโโ src/
โ โโโ llmvcr/
โ โโโ __init__.py # Public API: cassette, record, playback
โ โโโ cassette.py # @llmvcr.cassette() decorator
โ โโโ context.py # with llmvcr.record() / playback()
โ โโโ recorder.py # Core engine: record/playback state
โ โโโ matching.py # Semantic request matching
โ โโโ storage.py # Read/write cassette YAML files
โ โโโ errors.py # Custom exceptions
โ โโโ _patch.py # Provider routing
โ โโโ cli.py # CLI commands
โ โ
โ โโโ providers/
โ โโโ openai_provider.py
โ โโโ anthropic_provider.py
โ โโโ gemini_provider.py
โ โโโ ollama_provider.py
โ โโโ groq_provider.py
โ
โโโ tests/
โโโ cassettes/
โ โโโ sample_openai.yaml
โ โโโ sample_anthropic.yaml
โ โโโ sample_gemini.yaml
โ โโโ sample_ollama.yaml
โ โโโ sample_groq.yaml
โโโ test_matching.py
โโโ test_storage.py
โโโ test_recorder.py
โโโ test_cassettes.py
โโโ test_new_providers.py
Error Reference
| Error | Cause | Fix |
|---|---|---|
CassetteNotFoundError |
Used mode="playback" but no cassette file exists |
Run once with mode="record" first |
NoMatchFoundError |
Request doesn't match any interaction in the cassette | Re-record the cassette or add the interaction |
ProviderNotSupportedError |
Unknown provider name passed | Use one of: openai, anthropic, gemini, ollama, groq |
Contributing
- Fork the repo
- Create a branch:
git checkout -b my-feature - Make your changes and add tests
- Run the test suite:
pytest - Open a pull request
When adding a new provider, follow the pattern in src/llmvcr/providers/openai_provider.py โ implement serialize_request, serialize_response, deserialize_response, and make_patch, then register the provider in _patch.py.
License
This project uses the MIT LICENSE.
That means other developers can use, modify, publish, and even use your package commercially, as long as they keep the license notice and copyright text.
Contributing
- Fork the repo
- Create a branch
- Add tests for changes
- Run
pytest -v - Open a pull request
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 llmvcr-0.1.1.tar.gz.
File metadata
- Download URL: llmvcr-0.1.1.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05294b203ca445255b1a2e0059925a6138e9558037046df9078a65f96f43fb01
|
|
| MD5 |
3a67ad521d5646da9bf27d7adedd8749
|
|
| BLAKE2b-256 |
151ea5731171da77cfde0aeb4e090542aaa36900a309c57209344b637c1d9b05
|
File details
Details for the file llmvcr-0.1.1-py3-none-any.whl.
File metadata
- Download URL: llmvcr-0.1.1-py3-none-any.whl
- Upload date:
- Size: 23.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e45a5b005cc3d81dad9cd66dac6195ef92aed2d02339038dd6679e5ec237f0a
|
|
| MD5 |
7c3e3d0aeb20853312c4a75a30510a46
|
|
| BLAKE2b-256 |
c6a0be6fb834c2ae437d5617eebf0f20c5818c00773deb3472c9a966d185c81e
|