Skip to main content
Some models cannot be reached from the internet at all — a vLLM server on a private subnet, an internal inference gateway, a model running on a laptop. The Relay bridge red-teams those without opening a hole in your firewall. You run a small bridge process inside your own network. It holds one outbound WSS connection to Enkrypt on port 443. Enkrypt pushes OpenAI-shaped chat.completions requests down that socket, the bridge calls your internal LLM over your own intranet, and the response returns the same way. Nothing in Enkrypt’s cloud ever dials your model, and your firewall never has to accept an inbound connection.
target.metadata.relay, used throughout this page, is being rolled out. If a run returns 422 naming target.metadata.relay, your environment is on the previous release: send the relay block as target.relay instead, which stays supported and is described under Inline target. Everything else on this page is unchanged.
Substitute whichever host your Enkrypt contact gives you for the examples below.

Prerequisites

  • An Enkrypt AI API key from app.enkryptai.com
  • A machine inside your network that can reach the LLM and can make outbound HTTPS connections on port 443
  • The bridge itself, which ships in the Python SDK
Shell

Step 1 — Run the bridge

pip install enkryptai-sdk installs an enkryptai-relay console script. Run it on the machine inside your network. It needs an API key and a bridge id, and nothing else.
On success the bridge logs:
There is no user id to configure. Authentication is the API key alone. The gateway resolves the owning account from it and stamps that identity on both the bridge socket and the runs you submit — that is how a run finds the right bridge, and why two customers can both name a bridge my-laptop without colliding.

Step 2 — Confirm the bridge is connected

JSON
The answer is scoped to your own account, so connected: false covers “not running”, “never existed” and “registered by somebody else” alike rather than leaking which it was. This is also the only reachability check that works for a relay target — see Checking reachability below.

Step 3 — Point a red team run at the bridge

Routing through the bridge is a property of the target, not a separate endpoint. There are two ways to express it.

Inline target

POST /rt/redteam, with the relay block in the request body:
POST /rt/redteam
The relay-specific parts:
  • connect_via_relay: true is the only switch. There is no provider: "relay". It stays at the root of target, not inside metadata.
  • target.endpoint is kept for labelling and audit only. Nothing in Enkrypt’s cloud ever dials it, so it may be an address that only resolves inside your network.
  • target.api_key is "". The bridge authenticates to your internal LLM, not Enkrypt. Credentials for the internal LLM go in metadata.relay.target_headers, which the bridge forwards verbatim. Never put them here.
  • metadata.relay.target_endpoint is the URL the bridge actually POSTs to inside your network.
  • metadata.relay.model_name is the identifier your internal LLM expects. It need not match target.model_name, which is only the label the run is reported under.
  • metadata.relay.target_headers is optional. Omit the key entirely rather than sending an empty object.
The response is a run_id and status: "queued".
target.metadata accepts relay and nothing else. Any other key — metadata.tenant_id, metadata.aws_region, provider-specific metadata in general — is a 422. The /rt APIs cannot express provider-specific metadata at all, and accepting it silently would mean the run attacks a differently-configured target and reports success on it. Unknown keys inside relay are a 422 too.
target.relay — the bare spelling, without the metadata wrapper — is an older form and is still accepted, so existing integrations keep working. Prefer metadata.relay: it is the spelling used everywhere else in the chain, including what a saved model stores. Full field reference: target.metadata.relay.

Saved model

Store the relay configuration once and reuse it. POST /models/add-model:
POST /models/add-model
Then run it with POST /rt/model/redteam and no target in the body, naming the saved model in headers. The gateway resolves the stored model into the target for you and returns 202 with a run_id.
cURL
Give endpoint all four keysscheme, host, port and base_path. All four are required. Some providers have built-in defaults that paper over an omission; openai_compatible has none, so leaving any one of them out fails the save.
connect_via_relay and metadata.relay are a pair, and metadata.relay cannot share the metadata object with other provider metadata. A model that sets the flag without a relay block, a relay block without the flag, or a relay block alongside provider metadata is rejected with a 400 naming the model when a /rt/model/* endpoint tries to use it. That is deliberate: a half-configured relay would otherwise be dialled directly, at an address that only resolves inside your network.

Step 4 — Track the run and read the results

These are the same endpoints as any other run — nothing about reading a relay run is special.
cURL
See the Quickstart for the run lifecycle’s status values, what the summary and record shapes contain, and how to read an ASR.
Do not follow the submission response’s status_url, results_url and websocket_url fields literally. They are relative and are not guaranteed to carry the /rt/ prefix this API is published under, so requesting one as given can fail. Build the URL from run_id and the paths above instead.websocket_url is the Server-Sent Events progress stream, /rt/runs/{run_id}/stream, despite the name. The live log socket is a different field, logs_url — see Step 5.

Step 5 — Watch the logs while the run is still going

A relay run has a second side you cannot see from the outside: every attempt is a call out to your network. The log stream is where that becomes visible while it is happening, rather than after the fact in the records — which is why it is worth wiring up for a relay run even if you would skip it for a hosted one. This is a WebSocket, and it is a different stream from websocket_url:

Getting the URL

The run tells you the path. It is on the submission response, and on GET /rt/runs/{run_id} if you no longer have that:
That value is relative. Join it onto the log socket’s base and append your API key:
The key goes in the query string, not the apikey header, because a browser cannot set headers on a WebSocket handshake and this is the same route the dashboard uses. The assembled URL therefore contains your API key — do not paste it into a ticket, a log line or a shared notebook.
Two things about that path are worth knowing, because getting either wrong produces a socket that opens normally and then simply stays silent rather than returning an error:
  • the run id segment must be the prefixed one (rt-…), not the bare UUID;
  • the first segment is the run’s owner, and it must match the account your API key belongs to. Another account’s run is a 403 at the handshake. A run started with an organization key is owned by the organization, not by you personally.

Reading the stream

curl cannot do this; use a WebSocket client.
websocat
Python SDK
stream_logs resolves logs_url for you, so the run id is all you pass. It blocks and ends on its own when the run does. astream_logs is the same stream for asyncio (async for line in rt.astream_logs(run.run_id)), and logs_url() returns the assembled URL if you would rather hold the socket yourself. Lines already produced are replayed before the stream tails, so connecting part-way through still gives you the run from the beginning — you do not have to race the job to start watching. The server closes the socket once the run reaches a terminal state. Retention is finite, so a run that finished long ago replays from its stored app.log instead.
Connections are rate limited per IP, not per key, and the limit is shared with the dashboard. Opening a socket per attempt in a loop will trip it; open one per run and keep it.

Step 6 — The SDK equivalent

RTModelConfig.via_relay() builds the whole relay target, emitting metadata.relay, so the three values you actually own are the three you pass:
Python SDK
  • endpoint is your LLM’s URL as the bridge sees it. It fills both target.endpoint (the label) and metadata.relay.target_endpoint (what the bridge POSTs to).
  • provider defaults to openai_compatible. It has no runtime effect under connect_via_relay — the relay wire is OpenAI-shaped end to end — but it survives as metadata.
  • remote_model_name= overrides only the name sent to your internal LLM, for when it differs from the label you want the run reported under.
  • Passing a non-empty api_key= raises, on purpose: a key there would be shipped to Enkrypt and then ignored, because the bridge is what authenticates to your LLM. Use target_headers instead.
Pass the target to run_redteam exactly as you would a hosted one; routing through the bridge is a property of the target, so no client method changes. RTModelConfig.hosted() is the symmetric constructor for a publicly reachable endpoint, which makes moving a run between a hosted and an internal model a one-line edit.

If the bridge drops mid-run

The run pauses; it does not fail. Calls made in that window are retried, the bridge reconnects on backoff, and no test case is lost. You can restart the bridge process, reboot the host, or lose the network for a while and pick the run up where it left off.

Checking reachability

POST /rt/model-health and POST /rt/model/model-health do not support relay targets. Both probes dial target.endpoint directly. On a relay target that field is only a placeholder for an address inside your own network, so the probe always reports unhealthy however healthy the bridge is.Use the bridge status endpoint from Step 2 instead: GET /redteam/relay/v1/relay/bridges/{bridge_id}/status.
Submitting the run itself is unaffected: the gateway knows not to health-probe a relay target on the way in.

Troubleshooting

Exact 422 strings for an inline target

An inline target is validated by the Red Team service. Every failure comes back as 422 Unprocessable Entity with a detail array; these are the strings verbatim, so you can match on them.
422
The Value error, prefix is real — it is part of the msg string the API returns, not documentation punctuation. The unprefixed sentence is repeated in ctx.error, which is the easier field to match on exactly.The two extra_forbidden rows are the same message for two different mistakes: a key other than relay under metadata (row 3), and an unrecognised key inside the relay block — timeout_s, offline_grace_s, a relay endpoint or token — which do not exist as settings at all (row 4). The loc array is what tells them apart, and its last element names your key.

Exact 400 strings for a saved model

A saved model is projected into a target by the gateway before the service sees it, so a relay misconfiguration there is a 400, not a 422, and it is raised when a /rt/model/* endpoint uses the model rather than when you save it.
400
Every message opens with the subject — Saved model '<your model name>', or The submitted target_model_configuration if you sent a legacy target block inline — and then reads: