Skip to main content

standin

A stand-in for the real LLM in your tests. Record your LLM API calls once, then replay them forever: fast, free, deterministic, and fully offline. One line, any provider.

standin: record LLM calls once, replay them instantly and offline

import standin

with standin.use_cassette("tests/cassettes/summary.json"):
    reply = client.chat.completions.create(model="gpt-4o", messages=[...])
# First run: hits the real API and records it.
# Every run after: replayed from disk. No network, no cost, same answer.

Your LLM tests are slow, flaky, and cost money because they hit real APIs. standin makes them deterministic and offline by recording the real HTTP calls once and replaying them after, with the things LLM devs actually need: streaming, tool-calls, secret redaction, and body-aware matching.


Why not just VCR.py?

VCR.py is great, but it's a general HTTP tool. standin is built for LLMs:

  • Provider-agnostic, zero wiring. It hooks httpx under the hood, so it works with OpenAI, Anthropic, Gemini, Mistral, Cohere, litellm, LangChain, LlamaIndex — anything that sends over httpx. No per-SDK adapters.
  • Streaming just works. Server-sent event (SSE) responses are recorded and replayed intact.
  • Safe to commit. API keys in headers and secret-looking tokens in bodies are redacted automatically, so cassettes can live in a public repo.
  • Body-aware matching. Requests match on normalized JSON, so key ordering and formatting noise don't break replays. Repeated identical calls (agent loops) replay in order.
  • One-line pytest fixture, with sane auto-named cassettes.
  • Clean, typed, extensible core (see ARCHITECTURE.md) — swap the matcher, redactor, or storage backend.

Install

pip install standin

Python 3.9+ and httpx (already a dependency of the major LLM SDKs).

Quickstart

With pytest (recommended)

import pytest

@pytest.mark.standin          # cassette auto-named tests/cassettes/test_summarize.json
def test_summarize(standin):
    out = summarize("war and peace")     # your code that calls an LLM
    assert "Napoleon" in out

First run records against the real API; every run after replays from the cassette. Commit the cassette and teammates (and CI) run the test with no keys and no network.

Anywhere (context manager)

import standin
from openai import OpenAI

client = OpenAI()
with standin.use_cassette("tests/cassettes/haiku.json"):
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": "haiku about testing"}],
    )

Modes

Mode Behavior
once (default) Replay if the cassette exists, otherwise record it.
none Replay only. Errors on any unrecorded call — use this in CI.
all Always re-record, ignoring existing interactions.
new_episodes Replay what's recorded, record anything new (great for agent loops).

In CI, force replay-only for the whole run so a stray live call fails loudly:

STANDIN_MODE=none pytest

What a cassette looks like

Plain, reviewable JSON, secrets already stripped:

{
  "version": 1,
  "recorded_with": "standin",
  "interactions": [
    {
      "request": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "headers": { "authorization": "[REDACTED]" },
        "body": { "json": { "model": "gpt-4o-mini", "messages": [ ] } }
      },
      "response": { "status_code": 200, "body": { "json": { "choices": [ ] } } }
    }
  ]
}

Extending it

Everything is a small protocol you can replace (see ARCHITECTURE.md):

standin.use_cassette(path, matcher=MyMatcher(), redactor=MyRedactor(), store=MyStore())
  • Matcher — decide when a live request equals a recorded one. Ships with DefaultMatcher (exact) and FuzzyMatcher (body may drift up to a similarity threshold, so a reworded prompt still replays):

    from standin import use_cassette, FuzzyMatcher, DefaultRedactor
    with use_cassette(path, matcher=FuzzyMatcher(DefaultRedactor(), threshold=0.9)):
        ...
    
  • Redactor — control what gets scrubbed before writing.

  • CassetteStore — change the on-disk format.

Command line

standin list  tests/cassettes/summary.json     # one line per interaction
standin show  tests/cassettes/summary.json 0   # full request/response
standin stats tests/cassettes/summary.json     # counts by method/status
standin scrub tests/cassettes/summary.json     # re-run secret redaction in place

Roadmap

  • Embedding-based semantic matching (a Matcher you drop in; FuzzyMatcher already covers string-similarity drift today).
  • requests / aiohttp interceptors (the engine is already transport-neutral).

Contributing

See CONTRIBUTING.md. Run the suite with pytest, lint with ruff, type-check with mypy.

License

MIT. See LICENSE.

Download files

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

Source Distribution

standin-0.2.0.tar.gz (25.3 kB view details)

Uploaded Source

Built Distribution

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

standin-0.2.0-py3-none-any.whl (20.0 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: standin-0.2.0.tar.gz
  • Upload date:
  • Size: 25.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.3

File hashes

Hashes for standin-0.2.0.tar.gz
Algorithm Hash digest
SHA256 4436fbc11fcef774d44a9eca348240fd9cc0affb694c7c3a7ddbdb8cc0212c50
MD5 6ab06cc4fb3e3fda9b2380c3ebcc6268
BLAKE2b-256 0890e23e9880263b5e4fbb94e5d2bc3f029b313323efef12ae0de424ce6df284

See more details on using hashes here.

File details

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

File metadata

  • Download URL: standin-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 20.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.3

File hashes

Hashes for standin-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 148f38a7b316676a5635ec15eb140e478aa61db772d53fe63b3d5a4e19c37cd4
MD5 c00427e996d6de5e6f2e365027a741c9
BLAKE2b-256 914217706c6346eb0498bfa08a93d5e14c074361bb6dc9adbf541c01827bd85c

See more details on using hashes here.

Release history Release notifications | RSS feed

0.7.1

2 files

0.7.0

2 files

This release

0.2.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page