Docs Integrations
Gateway
Record any agent's model calls, in any language and with no code change, to a local file you replay as a CI check. About fifteen minutes.
Before you begin
- Node.js 22 or later, with git and npm. Check with
node --version. - An OpenAI API key, exported as
OPENAI_API_KEY. - An agent whose OpenAI client reads
OPENAI_BASE_URL, as the official OpenAI SDKs do. - No Runback account or key: the gateway never contacts Runback.
Steps
Get the gateway
The gateway is not published to npm. It runs from a copy of the Community edition's source.
terminalShell git clone https://github.com/letsRunback/runback-community cd runback-community && npm installExpected outputCloning into 'runback-community'..., then npm finishes withadded … packagesand no errors.Start recording
In the
runback-communityfolder, start the gateway in record mode. It forwards each request to OpenAI, returns the reply to your agent, and adds it tocassette.jsonbefore answering. It holds your OpenAI key itself and gives your agent a token instead, which OpenAI would reject.The key stays inside the gateway only when both
--upstream-auth-headerandRUNBACK_GATEWAY_UPSTREAM_KEYare set. With the key alone, the gateway ignores it and forwards whatever your agent sends, so that credential also works against OpenAI directly. For Anthropic, use--upstream-auth-header x-api-keyand the bare key, with noBearer.terminalShell RUNBACK_GATEWAY_TOKEN="$(openssl rand -hex 32)" export RUNBACK_GATEWAY_TOKEN export RUNBACK_GATEWAY_UPSTREAM_KEY="Bearer $OPENAI_API_KEY" echo "Give your agent this token: $RUNBACK_GATEWAY_TOKEN" node --import tsx packages/gateway/bin/gateway.ts --mode record --port 8888 \ --upstream https://api.openai.com \ --upstream-auth-header authorization \ --cassette ./cassette.jsonExpected outputGive your agent this token: 7ac7d251…, then the rest of a 64-character token, new each time the gateway starts.runback-gateway record :8888 → https://api.openai.comPoint your agent at it
In a second terminal, set the two variables your OpenAI client reads and run your agent. Use the token the gateway printed.
terminalShell export OPENAI_BASE_URL="http://localhost:8888/v1" export OPENAI_API_KEY="token-the-gateway-printed" # the gateway token, worthless against the provider python agent.pyExpected outputYour agent's usual output. The gateway's terminal prints nothing per call.Stop recording
Press Ctrl+C in the gateway's terminal. It writes the recording and prints how many calls it holds and the start of its digest.
Expected outputsaved 2 interactions · digest 7c364bbcda40… → ./cassette.jsonReplay it as a CI check
Replay serves the recorded replies and makes no call to OpenAI. If your agent sends a request the recording does not hold, it gets a 409 and the gateway exits 1, which fails the job.
ci.shShell node --import tsx packages/gateway/bin/gateway.ts --mode replay --port 8888 --cassette ./cassette.json & gateway=$! sleep 2 OPENAI_BASE_URL="http://localhost:8888/v1" OPENAI_API_KEY="unused-in-replay" python agent.py kill -TERM "$gateway" wait "$gateway" # 0: the agent reproduced the recording. 1: it diverged.Expected outputrunback-gateway replay :8888 — serving 2 recorded responses, NO upstream✅ no divergences — agent reproduced the baseline
Check it worked
The recording is a file, not a run in the app. Open cassette.json: entry_count is the number of model calls your agent made, and the replay printed no divergences. A request matches only when its method, path and body are the same as in the recording, so changing one character of a prompt is a divergence. Query parameters may come in any order.
What the gateway records from this
The gateway command in record mode, holding the provider key, in front of a scripted Chat Completions model. 2 recorded exchanges, captured 19 Oct 2026.
HTTPgpt-4o-mini → get_weather
{ "seq": 0, "kind": "http", "key": "c92f60c805892b0d0d0b007351bb5863c253a09e4bc4dd31a7a33b9872d136ac", "output": { "status": 200, "headers": { "content-type": "application/json" }, "bodyText": "{\"id\":\"chatcmpl_2\",\"object\":\"chat.completion\",\"created\":1760000000,\"model\":\"gpt-4o-mini\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":null,\"tool_calls\":[{\"id\":\"call_1\",\"type\":\"function\",\"function\":{\"name\":\"get_weather\",\"arguments\":\"{\\\"city\\\":\\\"Paris\\\"}\"}}]},\"finish_reason\":\"tool_calls\",\"logprobs\":null}],\"usage\":{\"prompt_tokens\":12,\"completion_tokens\":5,\"total_tokens\":17}}" }, "hash": "d7cdebc6206d51f46a9ff4198793e65da2b1c068ef6094383243f95b0149d306" }HTTPgpt-4o-mini
{ "seq": 1, "kind": "http", "key": "25f96768c5adede082376b4eb0009c2b14050ec859e1403cb8039291c90db381", "output": { "status": 200, "headers": { "content-type": "application/json" }, "bodyText": "{\"id\":\"chatcmpl_3\",\"object\":\"chat.completion\",\"created\":1760000000,\"model\":\"gpt-4o-mini\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"It is sunny in Paris.\"},\"finish_reason\":\"stop\",\"logprobs\":null}],\"usage\":{\"prompt_tokens\":20,\"completion_tokens\":8,\"total_tokens\":28}}" }, "hash": "ced15c10959e5a87ea364d14b0a30f7834feea8a4751cd1917c580139d667693" }
A recording file the gateway really wrote on its own machine. It is not sent to Runback, so it is not a run in any workspace. Each request is kept only as a fingerprint in its entry's key. Runback's test suite re-records it on every change.
Each entry keeps the response's status, headers and full body. The request is kept only as a fingerprint of its method, path and body, in key. Each entry's hash covers the one before it, and digest is the last, so an edited recording no longer matches. Neither your OpenAI key nor the gateway token is written to the file.
Troubleshooting
| You see | What it means | Fix |
|---|---|---|
--upstream-auth-header was set but RUNBACK_GATEWAY_UPSTREAM_KEY is not — the real credential would still come from the caller. Set both, or neither. | The flag was given without the key, so the gateway stopped (exit code 2). | Export RUNBACK_GATEWAY_UPSTREAM_KEY in the same terminal, as Start recording does. |
Your agent gets a 401 whose message is missing or invalid gateway token | The agent sent its OpenAI key, or an old token, instead of this gateway's token. | Set OPENAI_API_KEY to the token the gateway printed. It changes each time the gateway starts. |
Warning: RUNBACK_GATEWAY_TOKEN is not set — this gateway accepts requests from anyone who can reach it. | No token, so anyone who can reach the port can use your OpenAI account through it. | Fine on your own machine for a test. Otherwise set it, as Start recording does. |
record needs --upstream <url> and --cassette <file> | One of the two was left off. | Pass both, as in Start recording. |
[gateway] uncaught exception — captured interactions were already persisted incrementally: Error: listen EADDRINUSE: address already in use :::8888 | Something is already using port 8888. The gateway also rewrote the file named by --cassette as an empty recording. | Stop the other process or pass another --port, and record to a new file name so an existing recording is not emptied. |
❌ 1 divergence(s) — agent behaviour changed, and the agent got a 409 marked divergence | In replay, the agent sent a request the recording does not hold. | If the change is intended, record again. If not, the check caught a change in behaviour. |
What it does not do
- It does not redact. The recording holds OpenAI's replies as they arrived, so treat it as sensitive. Recording again to the same file replaces it.
- It reads each whole reply before passing it on, so a streamed reply reaches your agent at once, not token by token.
- Nothing reaches Runback, so a call made only through the gateway has no run, no policy check and no signed evidence.
Next steps
- Record runs into Runback from TypeScript or Python: TypeScript quickstart.
- No SDK for your language? OpenTelemetry sends runs to Runback with no code change.
- Running Runback on your own servers: Self‑host.