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.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.
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
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 thetarget, 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
connect_via_relay: trueis the only switch. There is noprovider: "relay". It stays at the root oftarget, not insidemetadata.target.endpointis 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_keyis"". The bridge authenticates to your internal LLM, not Enkrypt. Credentials for the internal LLM go inmetadata.relay.target_headers, which the bridge forwards verbatim. Never put them here.metadata.relay.target_endpointis the URL the bridge actually POSTs to inside your network.metadata.relay.model_nameis the identifier your internal LLM expects. It need not matchtarget.model_name, which is only the label the run is reported under.metadata.relay.target_headersis optional. Omit the key entirely rather than sending an empty object.
run_id and status: "queued".
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
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
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
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 fromwebsocket_url:
Getting the URL
The run tells you the path. It is on the submission response, and onGET /rt/runs/{run_id} if you no longer have that:
- 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
403at 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
endpointis your LLM’s URL as the bridge sees it. It fills bothtarget.endpoint(the label) andmetadata.relay.target_endpoint(what the bridge POSTs to).providerdefaults toopenai_compatible. It has no runtime effect underconnect_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. Usetarget_headersinstead.
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
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 inlinetarget 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 a400, not a 422, and it is raised when a /rt/model/*
endpoint uses the model rather than when you save it.
400
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:
Related
- Configuration Reference — every
target.metadata.relayfield - Payload Examples — the relay payload alongside every other Red Team payload shape
- Relay Bridge Connect — the WSS socket and its headers
- Relay Bridge Status — the status endpoint
- Stream Run Logs — the live log socket, and how its URL is built
- Python SDK reference —
RelayBridge, the bridge translation hooks, and the rest of the bridge environment contract - Testing Models Across Providers — target configuration for publicly reachable models
- Quickstart — the full run cycle

