Metadata-Version: 2.4
Name: kopya
Version: 0.2.0
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Rust
Classifier: Topic :: Software Development :: Testing
License-File: LICENSE
Summary: Record your app's real API calls once, replay them offline forever.
Keywords: mock,proxy,testing,record-replay,sse,llm,vcr
Home-Page: https://github.com/thinkgrid-labs/kopya
License-Expression: Apache-2.0
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/thinkgrid-labs/kopya
Project-URL: Issues, https://github.com/thinkgrid-labs/kopya/issues

# kopya

**Record your app's real API calls once. Replay them offline, forever.**

`kopya` (Filipino for *"copy"*) is a single static binary that sits between your app and
the internet. Run your test suite once against the real API, and every call after that is
served from a fixture file — no network, no API bill, no flakes.

```console
$ kopya record -- pytest tests/
...
kopya recorded 47 calls → .kopya/fixtures.json
  Secrets were redacted. Review the file before committing it.

$ kopya replay -- pytest tests/
kopya loaded 47 recorded calls from .kopya/fixtures.json
...
kopya replayed 47 calls · 0 live
  Saved ~128,400 tokens and 94.2s.
```

Your application code does not change. No mock library, no dependency injection, no
monkeypatching.

## Why

Tests that hit OpenAI, Anthropic, Stripe or any other third-party API are slow,
nondeterministic, and cost real money. The existing answers all have a catch: language
-bound mock libraries only cover one runtime, and the proxy-based tools need Python or a
JVM in your CI image.

kopya is one ~7MB binary with no runtime. It boots instantly in a container and works the
same for Python, Node, Go, Rust, Ruby, Java or `curl`.

## Install

Whichever ecosystem you already have. All three install the same prebuilt binary;
nothing is compiled or downloaded at install time. (The npm package is a small
Node shim that execs the binary, so `npx` invocations pay Node's startup once.)

```sh
npx @kopya/cli --help     # Node
uvx kopya --help          # Python
cargo install kopya       # Rust
```

(The npm package is scoped `@kopya/cli`; the installed command is still `kopya`.)

Or grab a binary from the [releases page](https://github.com/thinkgrid-labs/kopya/releases).
Prebuilt for macOS (Intel and Apple Silicon), Linux (x64 and arm64, statically linked
against musl) and Windows x64.

## How it works

kopya runs an HTTP/HTTPS proxy on `127.0.0.1` and launches your command with the proxy and
CA environment variables already set:

| Variable | Why |
| --- | --- |
| `HTTP_PROXY` / `HTTPS_PROXY` | Routes traffic through kopya |
| `NODE_USE_ENV_PROXY=1` | Node's built-in `fetch` ignores `HTTP_PROXY` without it |
| `NODE_EXTRA_CA_CERTS` | Node uses its own CA bundle, not the system keychain |
| `REQUESTS_CA_BUNDLE` | Python `requests` / `httpx` use `certifi` |
| `SSL_CERT_FILE`, `CURL_CA_BUNDLE` | OpenSSL and curl |

This matters: **trusting a root CA in the system keychain is not enough** for Node, Python
or Java. Setting these per-process is what makes "zero code changes" actually true. If you
point an already-running process at kopya instead, run `kopya ca trust` and export the
variables it prints.

## Commands

```sh
kopya record -- <command>     # run <command>, save its outbound calls
kopya replay -- <command>     # run <command> against the saved calls
kopya ca path                 # print the root CA path (creates it on first run)
kopya ca trust                # install the root CA into the system trust store
kopya ca untrust              # remove it
```

Useful flags:

- `--out` / `--in` — fixture file path (default `.kopya/fixtures.json`)
- `--port` — proxy port (default `8888`; `0` picks a free one)
- `--append` — keep calls already in the fixture that this run did not re-record
- `--no-proxy` — hosts to reach directly (default `localhost,127.0.0.1,::1`)
- `--allow-live` — let unmatched requests reach the network instead of failing
- `--realtime` — reproduce the original timing of recorded streams
- `--no-redact` — keep credentials in the fixture. Don't.

Loopback is excluded by default, so a local test server, a testcontainer or a
WebDriver on `127.0.0.1` is not swept into the fixture. Pass `--no-proxy ""` to
record those too.

Running `kopya record` or `kopya replay` with no `--` command starts the proxy in the
foreground so you can point an existing process at it.

## Streaming responses

`text/event-stream` responses — every streaming LLM API — are recorded frame by
frame, with the offset each frame arrived at, and replayed with those boundaries
intact. A recording is passed through to your app live as it happens, so the run
you record behaves exactly like an unproxied one.

By default replay emits the frames as fast as the client will take them, because
the point of replay is to make a slow test fast:

```
recorded   7 frames over 1015ms
replayed   7 frames over 7ms          # 145x faster, same frames
replayed   7 frames over 1012ms       # with --realtime
```

Use `--realtime` when the thing under test is the streaming behaviour itself —
time-to-first-token, incremental rendering, or a client-side timeout.

Token usage is parsed out of the stream — OpenAI's `usage.total_tokens`,
Anthropic's split `input_tokens`/`output_tokens`, and Gemini's
`usageMetadata` — so the summary can report what a replayed run would have cost.

A stream that never reached its end, because the client hung up or the command
exited mid-flight, is recorded with `"truncated": true` and called out in the
summary. It will replay short, so re-record it if a test depends on the end.

Only `text/event-stream` is streamed. Other chunked responses — NDJSON, long
polls, large downloads — are buffered whole before being passed on.

## Strict by default

In replay, a request with no recorded match returns `502` **and fails the run**, even if
your test swallowed the error. A green replay run therefore proves the suite is genuinely
hermetic. Pass `--allow-live` when you want the softer behaviour.

## Fixtures are meant to be committed

The fixture file is the artifact your team shares, so it is designed to survive code
review:

- Headers are stored in sorted maps and bodies as plain text, so diffs stay readable
- Credentials are redacted before anything touches disk — `Authorization`, `Cookie`,
  `x-api-key` and friends, plus bare `sk-`/`ghp_`/`AKIA`-style tokens found in bodies,
  form fields, and query strings like Google's `?key=`
- Binary payloads are dropped rather than base64-inflating the file

Redaction only ever touches the stored copy. Match keys are always derived from the
request as it arrived on the wire, so redacting a request body cannot stop it matching
on replay.

Repeat calls to the same endpoint are kept in order and replayed in order, so polling
and pagination work. Once a sequence is used up its last response repeats.

Always read a fixture before committing it the first time. Redaction is conservative by
design, not a guarantee.

## Status

Early. `0.2` covers HTTP/1.1 record and replay, including server-sent event streams,
with redaction, response decompression and strict matching.

Fixtures are format version 2. Version 1 files do not load — `kopya` will say so and
ask you to re-record.

**Roadmap:** HTTP/2, a GitHub Action, Homebrew, request-count assertions, and latency +
error injection.

Not planned for `0.1`: gRPC, WebSocket, a GUI, declarative YAML rule files.

## License

Apache License 2.0 — © 2026 ThinkGrid Labs

