Changelog

Source: CHANGELOG.md at the repo root.

All notable changes to tapedeck are documented here. The format follows Keep a Changelog; this project adheres to semantic versioning once it reaches 1.0.0.

0.4.0 - 2026-08-11

Added

  • mode: 'compare' (drift detection). A fourth mode that calls the live model and loads the recorded cassette, then reports how the two diverged. A cassette is a static fixture, and a static fixture rots silently: the provider retunes the model and the recorded trajectory quietly stops being what the model does. Works for doGenerate and doStream, hash-addressed and named cassettes.
    • Three signals, all explainable: the tool-call trajectory (same names, same order, same inputs, compared as canonical JSON so key order is never drift), the unified finish reason, and text (exact / normalized / different). No similarity score to argue with.
    • Never writes. The cassette is left byte-identical and the caller receives the live result, so compare is a live run with a drift check stapled on. See compare never writes.
    • onCompare(result) fires once per compared call with the structured CassetteCompareResult. With no handler, the first diverging call throws the new CassetteDriftError, so drift can never pass silently; register a handler to own the policy.
    • tapedeck compare <script> [args...] runs a command with CASSETTE_MODE=compare and propagates its exit code: a CI drift gate.
    • New exports: compareCassetteResponses, summarizeResponse, formatCompareResult, CassetteDriftError, and the report types (CassetteCompareResult, ResponseSummary, ToolCallSummary, …).
    • Compare spans carry a tapedeck.compare_equal attribute.

Changed

  • Language-model spec v4 is now typed, not merely supported at runtime. cassetteMiddleware returned a LanguageModelV3Middleware, so ai@7 consumers (spec v4) had to write as unknown as LanguageModelMiddleware at wrapLanguageModel even though the runtime path was green in the weekly cron. The middleware is now typed structurally (TapedeckMiddleware): it describes the fields tapedeck actually reads and stays generic in the result type, so one object is assignable to ai@6's spec v3 middleware and to ai@7's spec v4 middleware with no cast on either side.
    • test/types.test-d.ts asserts assignability to both spec surfaces and to wrapLanguageModel from the installed major, in both legs of the CI matrix.
    • specificationVersion stays 'v3': v4 hosts accept any string, v3 hosts accept only 'v3'.
    • Runtime behaviour, cassette format, and hashes are untouched. No CASSETTE_VERSION bump: v1 and v2 cassettes replay byte-identically.

0.3.1 - 2026-08-10

Changed

  • ai peer range widened to >=6.0.0 <8. ai@7 keeps the language-model middleware surface tapedeck wraps: the weekly compat cron passed typecheck, the full suite, and the record/replay round trip against 7.0.37 and 7.0.58, but the <7 pin made the package uninstallable next to a fresh npm i ai. No runtime changes; cassette formats and hashes are untouched.
  • CI runs the suite against the latest of each supported ai major (6 and 7) on every push and PR, instead of only the lockfile's 6.x plus the weekly ai@latest cron.

0.3.0 — 2026-06-10

Added

  • Multi-interaction named cassettes. A named cassette (withCassette('checkout-flow.json', …) or cassetteName) now stores every model call the test makes, keyed by request hash — a multi-step agent records all its calls into one file and replays each one distinctly, in any order. New v2 file shape: { version: "tapedeck@0.3.0", recordedAt, interactions: [{ hash, request, response }] }. Each withCassette run is one recording session (re-recording starts the file fresh); a static cassetteName upserts by hash. Legacy v1 named cassettes keep their serve-as-is replay; hash-addressed files unchanged.
  • New exports: MultiCassette, CassetteInteraction, CassetteFile, isMultiCassette, MULTI_CASSETTE_VERSION, diffCassetteFiles / formatCassetteFileDiff. tapedeck diff and tapedeck ls understand both formats.

Fixed

  • withCassette had no effect on the published package. The two dist entry points are separate bundles, each with its own copy of the ambient-context module — withCassette published into one AsyncLocalStorage while the middleware read another, silently falling back to live mode. The context registry now lives on globalThis under Symbol.for('tapedeck.cassette-context'), and a post-build cross-bundle smoke test in CI guards the regression.

0.2.0 — 2026-06-10

Published as @nkwib/tapedeck from this release — the unscoped tapedeck name on npm belongs to an unrelated 2022 package. The CLI bin is still tapedeck.

Added

  • tapedeck CLI (npx tapedeck …): record / replay a script with CASSETTE_MODE set, ls cassettes, semantic diff (exit 1 on difference), and merge with conflict reporting (--force overwrites).
  • OTel span emission. cassetteMiddleware({ tracer }) accepts any OpenTelemetry-compatible tracer, typed structurally — still zero runtime dependencies. Spans carry mode, hash, cassette path, model, hit/miss, and chunk-count attributes; misses record the exception.
  • Pluggable storage. cassetteMiddleware({ store }) takes a CassetteStore; ships with fileCassetteStore() (default) and memoryCassetteStore() for tests and edge runtimes.
  • toFollowRoute() matcher in @nkwib/tapedeck/vitest — asserts a tool-call trajectory follows a toolroute router; structurally typed, no dependency either way.
  • Diff/merge as library functions: diffCassettes, formatCassetteDiff, mergeCassetteDirs, plus parseCassette / serializeCassette.

Changed

  • Edge-safe core. No static node:fs / node:path / node:crypto: hashing uses WebCrypto (identical digests) and the file store loads node:fs lazily. Only node:async_hooks remains (Cloudflare Workers nodejs_compat flag).
  • computeCassetteHash is now async (returns Promise<string>). Digests unchanged; existing cassettes stay valid.

0.1.0 — 2026-06-10

Initial public release. Treated as a pre-1.0 calling card; a 1.0.0 cut will follow once the API has been used in anger.

  • cassetteMiddleware({ mode, cassetteDir, redact, cassetteName }) — a Vercel AI SDK LanguageModelV3Middleware that intercepts both doGenerate and doStream. Modes: record | replay | live.
  • Streaming is first-class: record drains and captures ordered stream parts; replay re-serves them as a genuine ReadableStream via the SDK's own simulateReadableStream.
  • Hash-addressed cassettes keyed by a stable SHA-256 of { modelProvider, modelId, prompt, toolSchemas, maxOutputTokens, temperature, topP }. Tool schemas are normalized (descriptions stripped, keys sorted).
  • Secret redaction at record time. Default matchers: apiKey, authorization, x-api-key, bearer, token (case-insensitive). Configurable via redact: (string | RegExp)[]. A replayed cassette that still contains a value a matcher would strip throws CassetteSecretError.
  • withCassette(name, testFn, options?) from @nkwib/tapedeck/vitest — pins a test to a named cassette and forces replay for its duration via AsyncLocalStorage.
  • Error family — CassetteMissError, CassetteSecretError, CassetteCorruptError, CassetteModeError, all extending CassetteError.
  • COMPATIBILITY.md row stamped against ai@6.0.0. Zero runtime dependencies beyond the ai peer.