Skip to main content

Record real LLM calls once, then replay them in your tests forever — offline, no API key, no cost, and the same result every run.

Project description

cendor-cassette

Record an agent run once; replay it forever — deterministic, offline, and free. Unlike vcrpy (HTTP-only), it captures the whole run: every LLM call and tool call, in order.

Agent tests with no API key — a recorded run replays in microseconds per call, offline.

PyPI license · pip install cendor-cassette

Using an AI coding assistant? npx @cendor/init (TS) / uvx cendor-init (Python) wires it up — or point it at cendor.ai/docs/for-ai-assistants.

from cendor.core import instrument
from cendor import cassette

client = instrument(OpenAI())          # the same instrumented seam used in production

@cassette.use("triage_happy_path.json")   # record first run, replay after (auto mode)
def test_triage():
    result = my_agent.run("My card was charged twice")
    assert "refund" in result.tools_called
    assert cassette.semantic_match(result.answer, "offers a refund")

Highlights

  • Whole-run capture — every LLM and tool call, in order (not just HTTP, like vcrpy).
  • Four modesauto (record then replay) · record · replay (fail on an unrecorded call) · rerecord (run live, report drift() without overwriting the committed cassette).
  • Decorator or context manager@cassette.use("run.json") / with cassette.using(...) (handy in pytest fixtures).
  • Meaning-based assertionssemantic_match(actual, expected) (offline lexical default; opt into a free offline local-embedding scorer, a BYO-provider embedder, or an LLM judge). semantic_drift() filters rerecord noise down to real regressions.
  • Pluggable matching + redaction — a normalizer ignores volatile fields; secrets/PII redacted on write, but matching hashes the un-redacted request so redaction never collapses two distinct calls (redact=True|False|callable).
  • Parallel-safe — recording is scoped to the active using()/use() context (a ContextVar), so concurrent blocks never capture each other's calls; cassettes are written atomically. Under pytest-xdist, give each worker its own cassette path (e.g. suffix with PYTEST_XDIST_WORKER) so workers don't race on one file.
  • Faithful replay — dict-response providers (Ollama/Bedrock) replay as dicts and SDK-object providers as attribute objects; stream=True and stream=False calls match their own recordings (cassette format v2; committed v1 cassettes still replay).
  • promote() turns a production JSONL trace into a replayable regression test (LLM and tool calls).

Semantic matching (opt-in)

semantic_match defaults to lexical_score — offline, deterministic, zero-dependency. For meaning-aware (negation-sensitive) checks, pass a scorer into the existing hook. cassette binds no model and adds no dependency unless you ask for one. Four tiers, hermetic-and-free → meaning-aware-but-costly:

  1. Lexical (default) — lexical_score. Hermetic, deterministic, free, zero-dep.
  2. Local embeddings (recommended) — local_embedding_scorer(), free/offline/deterministic via model2vec static embeddings (numpy-only, no torch, ~8–30 MB). Behind pip install 'cendor-cassette[embeddings]'.
  3. BYO provider embeddingsembedding_scorer(embed_fn) wraps any provider (OpenAI text-embedding-3-small/large, Google gemini-embedding, Cohere embed-v3; Anthropic has no embeddings API → use Voyage). Non-hermetic: a cloud embedder calls the network at score time. openai_embedding_scorer(client, model="text-embedding-3-small") is a thin convenience over an already-built OpenAI-shaped client.
  4. LLM-judge — a scorer that calls your own instrumented client (a documented recipe, never a shipped dependency). Non-hermetic, non-deterministic, costs money.
from cendor import cassette

score = cassette.local_embedding_scorer()                 # free, offline, deterministic
assert cassette.semantic_match(result.answer, "offers a refund", scorer=score)
assert not cassette.semantic_match("we will not offer a refund", "offers a refund", scorer=score)

drift() stays byte-exact; at temperature > 0 it flags every run. semantic_drift(threshold=0.8, scorer=None) re-scores each divergence's recorded-vs-live text and keeps only those below the threshold (real regressions, with a score), so cosmetic rewording is ignored. The alternative for byte-stable drift: record/replay at temperature=0.

Wrap-around, test-time only — records via core's bus, replays via a core interceptor; no second patch, no network.

See docs/cassette.md · CHANGELOG. Part of the Cendor stack — github.com/cendorhq/cendor-libs. Powered by PowerAI Labs. Apache-2.0; provided "as is", without warranty — use at your own risk (LICENSE §7–8).

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

cendor_cassette-1.1.0.tar.gz (26.6 kB view details)

Uploaded Source

Built Distribution

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

cendor_cassette-1.1.0-py3-none-any.whl (20.9 kB view details)

Uploaded Python 3

File details

Details for the file cendor_cassette-1.1.0.tar.gz.

File metadata

  • Download URL: cendor_cassette-1.1.0.tar.gz
  • Upload date:
  • Size: 26.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for cendor_cassette-1.1.0.tar.gz
Algorithm Hash digest
SHA256 3727d62795c68dec8f7b8485803ad4305a9e6e6d326ed2e643f7f9fc2ea779ae
MD5 36d668f4fdcb2e7406945e3e54f236d5
BLAKE2b-256 5b3253f93d2342a49546c8f010d2bc691b7f31a4868e61175ad4a36df15bf418

See more details on using hashes here.

Provenance

The following attestation bundles were made for cendor_cassette-1.1.0.tar.gz:

Publisher: release.yml on cendorhq/cendor-libs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file cendor_cassette-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: cendor_cassette-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 20.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for cendor_cassette-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 19272c35f3e2e632678ee76c90dd7e8b0a8293e7373501fbf48bdc617ffa2e2a
MD5 a16634d93c0168f3bbf06132abf3f832
BLAKE2b-256 d44d7dcc372f7fba117a162d32707f2418470c92c6b78ad2a0276c184f69627b

See more details on using hashes here.

Provenance

The following attestation bundles were made for cendor_cassette-1.1.0-py3-none-any.whl:

Publisher: release.yml on cendorhq/cendor-libs

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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