Guides
HTTP over the Relay Broker
Reach HTTP services on your own machines from anywhere with @thuum/relay-http. An OpenAI SDK agent on an edge device calls a model in your data center, through outbound connections only, with nothing stored on the way.
The Relay Broker carries any request and its response.
@thuum/relay-http uses it to carry HTTP: an app calls a URL on its own machine, and the request
reaches an HTTP server on another network, streamed both ways. Both ends only dial out to Thuum over
WSS on port 443: no open port, no fixed IP, no VPN, and Thuum stores nothing.
This guide connects AI agents on edge devices, which speak the OpenAI API, to models running on your own machines with Ollama and LM Studio.
Edge device Thuum Data center
┌───────────────────────┐ ┌──────────────┐ ┌──────────────────────────────┐
│ agent (OpenAI SDK) │ │ │ │ relay-http server (active) │──▶ Ollama
│ │ HTTP, localhost │ WSS │ Relay Broker │ WSS │ │──▶ LM Studio (next)
│ ▼ │──────▶│ │◀──────│ relay-http server (standby) │
│ relay-http client │ │ │ │ │
└───────────────────────┘ └──────────────┘ └──────────────────────────────┘The two modes#
One package, two modes. Both exchange their App Key for a token, connect to the Relay Broker, and reconnect with backoff.
Client mode runs on each device, next to the apps that make the calls:
- It listens on
127.0.0.1, so only the device's own apps reach it. - The first path segment is the topic:
POST /green-lantern/v1/chat/completionsbecomes a request on topicgreen-lanternwith target/v1/chat/completions. - It streams the app's request into a head and body parts, and the response back as it arrives: Server-Sent Events work unchanged.
Server mode runs in your data center, next to the machines that answer:
- It serves each topic of its file, with a server list per topic, tried in order: on a refused
connection, a connect timeout, or a
502,503or504, it tries the next machine, as long as the response hasn't started. - To retry, it keeps the request body in memory until the response starts: on your equipment, never in Thuum.
- A second instance with the same file is a standby: it takes over when the first one's connection drops.
The names of your topics, your machines and their order exist only in the server's file. Thuum learns the topics while the server is connected, and nothing else.
1. Keys and claims#
Create two App Keys, and give each a Relay Broker claim:
| App Key | Relay Broker claim |
|---|---|
| One per edge device | P (8) on the topics that device may use |
| One for the servers | S (2) on every topic they serve |
Neither needs a Domain Claim. A key copied from a device can send requests to its own topics and nothing else, and is revoked alone; only the servers' key can serve, so nobody else can take a topic over. With the client (or in the console's Relay Broker section):
import { PUBLISH, SUBSCRIBE } from '@thuum/client';
const agents = await acme.relayBroker.claims.create({
name: 'Edge agents',
topics: { 'green-lantern': PUBLISH }
});
await acme.relayBroker.claims.keys.link(agents.id, deviceSelector);
const models = await acme.relayBroker.claims.create({
name: 'Model servers',
topics: { 'green-lantern': SUBSCRIBE, 'iron-man': SUBSCRIBE }
});
await acme.relayBroker.claims.keys.link(models.id, serverSelector);2. Run the server#
In the data center, on any machine that reaches the model servers, write relay-http-server.yaml:
# relay-http-server.yaml
url: { { THUUM_API_URL } }
domain: acme
key: ${THUUM_KEY} # the servers' App Key
topics:
green-lantern:
- http://192.168.1.21:11434 # Ollama
- http://192.168.1.35:1234 # LM Studio, if Ollama fails
iron-man:
- http://192.168.1.35:1234
- http://192.168.1.21:11434| Key | Meaning |
|---|---|
url | The Thuum API. |
domain | Your Domain. |
key | The App Key, linked to a claim with S on every topic below. |
topics | Each topic to serve, with its server list: base URLs, tried in order. |
secret | Optional: encrypts every data end to end (below). |
thuum-relay-http server --config relay-http-server.yamlStart the same command with the same file on a second machine: it serves as a standby.
3. Run the client#
On each edge device, write relay-http-client.yaml:
# relay-http-client.yaml
url: { { THUUM_API_URL } }
domain: acme
key: ${THUUM_KEY} # this device's App Key
listen: 127.0.0.1:8787| Key | Meaning |
|---|---|
url | The Thuum API. |
domain | Your Domain. |
key | This device's App Key, linked to a claim with P on the topics it may use. |
listen | The local address to listen on. |
secret | Optional: the same as the server's (below). |
thuum-relay-http client --config relay-http-client.yaml4. Point the agent at it#
The agent's whole change is its base URL, with the topic as the first segment:
OPENAI_BASE_URL=http://127.0.0.1:8787/green-lantern/v1
OPENAI_API_KEY=unused # any value: the Thuum App Key lives in the client appThe code stays as it was:
import OpenAI from 'openai';
const openai = new OpenAI(); // reads OPENAI_BASE_URL and OPENAI_API_KEY
const stream = await openai.chat.completions.create({
model: 'llama3.1',
messages: [{ role: 'user', content: 'Summarize the last shift.' }],
stream: true
});
for await (const chunk of stream) process.stdout.write(chunk.choices[0]?.delta?.content ?? '');The request reaches the client app on 127.0.0.1:8787, crosses Thuum on topic green-lantern, and
the server app sends it to http://192.168.1.21:11434/v1/chat/completions, Ollama's
OpenAI-compatible API. The events stream back as Ollama writes them. Check the path with
curl http://127.0.0.1:8787/green-lantern/v1/models.
The HTTP convention#
What the two apps put in each part's data. Thuum never reads it; it is written down so you can
write your own client or server, in any language, with the
Relay Broker WebSocket:
| Part | data |
|---|---|
Request head | { "method": "POST", "target": "/v1/chat/completions", "headers": [["content-type", "application/json"]] } |
Response head | { "status": 200, "headers": [["content-type", "text/event-stream"]] } |
body (both ways) | The bytes, base64. |
- Headers are a list of
[name, value]pairs, so repeated headers keep their order. - The client app removes
Authorization,Host,Cookie, and the hop-by-hop headers (Connection,Keep-Alive,Transfer-Encoding,TE,Trailer,Upgrade,Proxy-*). - The server app adds nothing: the request reaches each machine as the app sent it.
- If the app disconnects, the client app sends
cancel, and the server app aborts the machine's request.
Errors#
The apps answer errors in OpenAI's error shape ({ "error": { "message": … } }), so agents handle
them as OpenAI's own: the SDKs retry 5xx, then raise.
| Situation | Made by | HTTP to the app |
|---|---|---|
No connection to Thuum, or nobody serves the topic (unavailable) | client app | 503 |
The device's key has no P on the topic (forbidden) | client app | 403 |
| No response within the client app's timeout | client app | 504 |
| Every machine in the server list failed | server app | 502 |
| A part of a request it never saw (after a server switch) | server app | 502 |
Failover#
- Between machines. The server app tries a topic's list in order, until the response starts. Once a machine has started answering, a failure ends that response.
- Between server apps. When the active server app's connection drops, Thuum makes the standby
active at once. Requests in flight on the old one fail (
504or502); the next ones go to the new one.
End-to-end encryption#
Add the same secret to the client's and the server's files, and every data, heads and body parts
alike, is encrypted with AES-256-GCM before it leaves the device or the data center. Thuum then sees
topics, request ids, sizes and timing, and nothing else. It never needs more: the
Relay Broker stores nothing either way.
Why it holds up#
- Long streams. Only WebSockets cross the internet, kept alive by pings; HTTP stays on the local network at both ends, so there are no proxy timeouts to tune for a response that takes minutes.
- No rate limits from Thuum. Requests and responses pass unchanged, streamed, with no queue and
no
429added on the way. - Small blast radius. A device's key only reaches its own topics; a lost device is one key to delete.