Skip to main content

pytest plugin to mock and replay OpenAI and Anthropic API calls — record once, replay forever, no API key needed

Project description

llm-mock

pytest plugin to mock OpenAI and Anthropic API calls — record real responses once, replay them in tests forever. No API key needed in CI, no cost per run, no flaky non-determinism.

# Record once against the real API (run locally with your API key)
with llm_mock(mode="record", fixture="tests/fixtures/summarize"):
    result = my_pipeline("Summarize this document...")

# Replay in tests — no API key, no cost, deterministic
@pytest.mark.llm_replay(fixture="summarize")
def test_summarize():
    result = my_pipeline("Summarize this document...")
    assert "key points" in result

Works with the Anthropic SDK (claude-* models) and the OpenAI SDK (gpt-* models) out of the box — no changes to your application code required.


Why mock LLM calls in tests?

  • Cost. A CI pipeline hitting real LLM APIs can cost dollars per run at scale.
  • Flakiness. LLM outputs are non-deterministic — even temperature=0 varies across model versions.
  • Speed. Replayed fixtures return instantly; no network round-trip.
  • Offline. Tests run without credentials in CI, on a plane, in a container.

llm-mock intercepts at the HTTP transport layer (via httpx/respx) — your production code is never touched. Fixtures are plain JSON files you commit to git, diff in PRs, and refresh on demand.


Installation

pip install llm-mock

Or install from source:

git clone https://github.com/autopost/llm-mock.git
cd llm-mock
pip install -e .

Runtime dependencies: httpx, respx, pydantic


How to use

Your production code — untouched

# my_app/pipeline.py
import anthropic

client = anthropic.Anthropic()

def summarize(text: str) -> str:
    message = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=100,
        messages=[{"role": "user", "content": f"Summarize: {text}"}],
    )
    return message.content[0].text

pipeline.py has zero knowledge of llm-mock. No imports, no changes needed.

Step 1 — Record (run once, locally)

Create a small script or a dedicated test that runs with mode="record". You need a real API key for this step.

# record_fixtures.py
from llm_mock import llm_mock
from my_app.pipeline import summarize

with llm_mock(mode="record", fixture="tests/fixtures/summarize"):
    result = summarize("Long article about climate change...")
    print(result)  # real response from the API
ANTHROPIC_API_KEY=sk-... python record_fixtures.py

This creates tests/fixtures/summarize.json. Commit this file to git.

Step 2 — Replay (in tests, forever)

Use the pytest decorator — no with block needed inside the test:

# tests/test_pipeline.py
import pytest
from my_app.pipeline import summarize

@pytest.mark.llm_replay(fixture="summarize")
def test_summarize():
    result = summarize("Long article about climate change...")
    assert "climate" in result
pytest  # no API key needed, runs offline, instant

The decorator auto-discovers the fixture path relative to the test file — fixture="summarize" looks for tests/fixtures/summarize.json when the test lives in tests/.

llm-mock intercepts the httpx call the Anthropic SDK makes internally and returns the saved response — your test code calls summarize() exactly as it would in production.

Alternative: use the context manager directly if you need more control:

from llm_mock import llm_mock

def test_summarize():
    with llm_mock(mode="replay", fixture="tests/fixtures/summarize"):
        result = summarize("Long article about climate change...")
        assert "climate" in result

Step 3 — Re-record when things change

If you change the prompt, update the model, or want to refresh fixtures:

ANTHROPIC_API_KEY=sk-... python record_fixtures.py  # overwrites old fixture
git add tests/fixtures/summarize.json
git commit -m "refresh summarize fixture"

Quick start (direct API usage)

A complete working example from scratch.

1. Install

pip install llm-mock

2. Save your API key

echo 'export ANTHROPIC_API_KEY=sk-ant-api03-...' > .env
echo '.env' >> .gitignore

3. Create a record script

Create try_record.py:

import anthropic
from llm_mock import llm_mock

client = anthropic.Anthropic()

with llm_mock(mode="record", fixture="fixtures/hello"):
    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=64,
        messages=[{"role": "user", "content": "Say hello in one sentence."}],
    )
    print("Response:", message.content[0].text)
    print("Fixture saved to fixtures/hello.json")

4. Run it

source .env && .venv/bin/python try_record.py

You should see the real response printed and fixtures/hello.json created.

5. Verify the fixture

llm-mock list tests/fixtures/hello

6. Replay without an API key

Create try_replay.py:

import anthropic
from llm_mock import llm_mock

client = anthropic.Anthropic(api_key="fake-key")  # key is irrelevant in replay

with llm_mock(mode="replay", fixture="fixtures/hello"):
    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=64,
        messages=[{"role": "user", "content": "Say hello in one sentence."}],
    )
    print("Replayed:", message.content[0].text)
.venv/bin/python try_replay.py

The exact same response is returned instantly — no network call made.

7. Write a test with the pytest decorator

# tests/test_hello.py
import anthropic
import pytest

client = anthropic.Anthropic(api_key="fake-key")

@pytest.mark.llm_replay(fixture="hello")
def test_hello():
    message = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=64,
        messages=[{"role": "user", "content": "Say hello in one sentence."}],
    )
    assert message.content[0].text  # replayed from fixtures/hello.json
.venv/bin/pytest tests/test_hello.py -v

CLI

Inspect and manage fixture files from the terminal.

Note: activate your virtual environment first so llm-mock is on your PATH:

source .venv/bin/activate

Or run it directly with .venv/bin/llm-mock <command>.

llm-mock list <fixture>

Show all recorded interactions in a fixture file:

$ llm-mock list tests/fixtures/summarize

Fixture : tests/fixtures/summarize.json
Provider: anthropic
Interactions: 2

  1. a3f2c1d4e5b6…  claude-sonnet-4-6        2026-04-23T10:00:00
       "Summarize this document about climate change..."
  2. b4g3d2e5f6c7…  claude-haiku-4-5-20251001  2026-04-24T11:00:00
       "What is the capital of France?"

llm-mock clear <fixture>

Delete an entire fixture file:

llm-mock clear tests/fixtures/summarize

Delete a single interaction by hash:

llm-mock clear tests/fixtures/summarize --hash a3f2c1d4e5b6

How it works

Record mode:
  Your code → Anthropic/OpenAI SDK → httpx
    → llm-mock intercepts → forwards to real API
    → saves response to fixture JSON
    → returns response to your code

Replay mode:
  Your code → Anthropic/OpenAI SDK → httpx
    → llm-mock intercepts → looks up fixture by SHA256(model + messages + temperature)
    → returns saved response — no network call made

Request matching uses SHA256 of (model, messages, temperature). Same request always hits the same fixture entry. Different temperature or different message content → different fixture entry.


API reference

llm_mock(mode, fixture, provider="all")

Context manager that activates record, replay, or auto mode.

Parameter Type Description
mode "record" | "replay" | "auto" record hits the real API and saves; replay returns from fixture; auto replays if fixture exists, records if not
fixture str Path to the fixture file. .json extension added automatically if omitted
provider "anthropic" | "openai" | "all" Which provider(s) to intercept. Default: "all"

auto mode is the recommended default for most projects — it self-heals when new requests appear without manual mode switches:

@pytest.mark.llm_replay(fixture="summarize", mode="auto")
def test_summarize():
    ...
from llm_mock import llm_mock

with llm_mock(mode="replay", fixture="tests/fixtures/my_test", provider="anthropic"):
    ...

Environment variables

Variable Effect
LLM_MOCK_DISABLED=1 Disables all interception — LLM calls go to the real API as normal

Useful for refreshing all fixtures in one shot without touching test code:

LLM_MOCK_DISABLED=1 ANTHROPIC_API_KEY=sk-... pytest

Or in a weekly CI job that validates against the live model.

Exceptions

Exception When raised
FixtureNotFoundError Replay mode: fixture file missing, or no matching hash in file
FixtureParseError Fixture file exists but contains invalid JSON
from llm_mock import llm_mock, FixtureNotFoundError

try:
    with llm_mock(mode="replay", fixture="tests/fixtures/missing"):
        client.messages.create(...)
except FixtureNotFoundError as e:
    print(e)  # includes hint to run in record mode first

Fixture file format

Fixture files are plain JSON — readable, diffable, committable.

{
  "version": "1.0",
  "provider": "anthropic",
  "interactions": [
    {
      "hash": "a3f2c1...",
      "request": {
        "model": "claude-sonnet-4-6",
        "messages": [{"role": "user", "content": "Say hello."}],
        "max_tokens": 64
      },
      "response": {
        "id": "msg_01XYZ",
        "type": "message",
        "role": "assistant",
        "content": [{"type": "text", "text": "Hello! How can I help you today?"}],
        "model": "claude-sonnet-4-6",
        "stop_reason": "end_turn",
        "usage": {"input_tokens": 10, "output_tokens": 9}
      },
      "recorded_at": "2026-04-23T10:00:00+00:00"
    }
  ]
}

Multiple interactions (from different requests) are stored in the same file. Re-recording an existing hash overwrites only that entry.


Supported providers

Provider Intercepted endpoint Status
Anthropic api.anthropic.com/v1/messages Supported
OpenAI api.openai.com/v1/chat/completions Supported
Streaming (stream=True) v1.1

Comparison

Tool Record mode Native SDK support In-process
llm-mock yes yes (Anthropic + OpenAI) yes
llm_recorder yes no (LiteLLM only) yes
AIMock no yes no (HTTP server)
vcr-langchain yes no (LangChain only) yes

Development

git clone https://github.com/yourname/llm-mock
cd llm-mock
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest

Roadmap

  • v0.2auto mode, LLM_MOCK_DISABLED env var ✓
  • v1.1 — streaming support
  • v2 — shared fixtures for teams, semantic matching, web dashboard

Related

pytest mock openai · pytest mock anthropic · mock LLM calls python · record replay LLM · vcr cassette openai · fake openai response pytest · test without API key · offline LLM testing · deterministic LLM tests

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

llm_mock-0.2.0.tar.gz (18.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

llm_mock-0.2.0-py3-none-any.whl (12.9 kB view details)

Uploaded Python 3

File details

Details for the file llm_mock-0.2.0.tar.gz.

File metadata

  • Download URL: llm_mock-0.2.0.tar.gz
  • Upload date:
  • Size: 18.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for llm_mock-0.2.0.tar.gz
Algorithm Hash digest
SHA256 99249265126805076ad6ecd59cf29b1a80011f26a3db6568fb923abca62a5ce2
MD5 663e081f3a4a005283a9747295e4edb8
BLAKE2b-256 4259b6934e8a645c574f617234f19f5ea77afc67e49c1c34fe743ebda1fbe827

See more details on using hashes here.

File details

Details for the file llm_mock-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: llm_mock-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 12.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.5

File hashes

Hashes for llm_mock-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ce99b89e4472e519386021481a806e36bdd215654bb4e66cffa2b989b69cb607
MD5 f738899f86880f935e6fafc131c1cc85
BLAKE2b-256 5015894b1fe85f419403e05471c9a18a9a520ec614ec80599f19b838ee1af261

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page