Docs menuGateway

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

  1. 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 install
    Expected output
    Cloning into 'runback-community'..., then npm finishes with added … packages and no errors.
  2. Start recording

    In the runback-community folder, start the gateway in record mode. It forwards each request to OpenAI, returns the reply to your agent, and adds it to cassette.json before 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-header and RUNBACK_GATEWAY_UPSTREAM_KEY are 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-key and the bare key, with no Bearer.

    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.json
    Expected output

    Give 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.com

  3. Point 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.py
    Expected output
    Your agent's usual output. The gateway's terminal prints nothing per call.
  4. 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 output
    saved 2 interactions · digest 7c364bbcda40… → ./cassette.json
  5. Replay 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 output

    runback-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.

  1. HTTPgpt-4o-mini → get_weather200 · 17 tokens · called a tool
    {
      "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"
    }
  2. HTTPgpt-4o-mini200 · 28 tokens · finished
    {
      "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 seeWhat it meansFix
--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 tokenThe 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 :::8888Something 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 divergenceIn 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

Last updated 2 Nov 2026.