Chat & Clients.

Two ways to talk to the pool: the built-in terminal REPL, and a plain OpenAI-compatible /v1 that any existing chat client can point at instead.

Relay is alpha and unaccredited
Releases are pre-release versions on the 0.1.0-alpha.N → -beta.N → -rc.N → 0.1.0 ladder. The wire protocol between hub and node, the on-disk state layout and the CLI surface can all change without a deprecation period; /v1 tracks the OpenAI shape and is the most stable thing here. Nothing is accredited or covered by a compatibility promise.

relay chat

bash
relay chat                                          # picks the model, or asks if there's more than one
relay chat --model qwen3-8b                         # skip the picker
relay chat --url https://hub:8383                   # talk to a remote pool instead of the local daemon
relay chat --url https://hub:8383 --ca-fingerprint <fingerprint>  # pin the hub's CA explicitly

--url’s https:// pool is TLS by default (RELAY_V1_TLS=auto), signed by the hub’s own CA — relay chat resolves a fingerprint to pin automatically in most cases (this machine’s own daemon, or a node credential enrolled with that hub); otherwise pass --ca-fingerprint or set RELAY_CA_FINGERPRINT to the fingerprint relay status prints on the hub. See Security’s “Migrating clients”.

Pure Go, no CGO — works the same in a Linux terminal, macOS Terminal.app, and Windows Terminal.

text
relay chat — model qwen3-8b
Type /help for commands, /exit or Ctrl-D to quit, Ctrl-C to cancel a reply in progress.
you › explain the pool's placement order
assistant › Relay places a model on the cheapest strategy that fits: ...
2.1 tok/s
  • Fenced code blocks render distinctly from prose.
  • Ctrl-C cancels the reply in progress without leaving the REPL; a second Ctrl-C (or Ctrl-D / /exit) quits.
  • A line ending in \ continues onto the next line, for pasting multi-line input.
  • If the model has to load for the first time, relay chat prints “loading model across the pool…” instead of sitting silent — cold loads on a pool are measured in minutes, not seconds, so a hang would be indistinguishable from progress otherwise.
  • The conversation (including any /system prompt) lives in memory for the session only; nothing is written to disk.
  • Respects RELAY_API_KEY — set it in the environment before running relay chat if the pool requires one.

Slash commands

CommandEffect
/model NAMESwitch models for the rest of the session
/modelsList every model registered on the pool; * marks the current one
/system TEXT/system with no text clears it. Sets the system prompt otherwise.
/file PATHAttach a file's contents (fenced, up to 200 KiB) to your next message
/clearClear the conversation history
/statusShow pool status — full placement/node/model tables if you're talking to the local daemon, just the model list if you're talking to a remote --url
/helpShow this list
/exit, /quitQuit (same as Ctrl-D)

/status is the one worth knowing about: run it mid-session any time you want to see whether a node paused, whether a replica moved, or how much VRAM is free before loading something bigger.

Pointing another client at the pool

Nothing on the client side needs to know a pool exists. /v1/chat/completions, /v1/completions, /v1/embeddings, and /v1/models all speak the plain OpenAI dialect, gated by an optional bearer token (Authorization: Bearer $RELAY_API_KEY) if the hub set one. Point any of these at https://<hub>:8383/v1 — /v1 is TLS by default (RELAY_V1_TLS=auto), signed by the hub’s own CA, so a client also needs to trust or pin that CA (<RELAY_STATE_DIR>/tls/ca.pem, or the fingerprint relay status prints); RELAY_TLS_DOMAIN on the hub gets you a publicly-trusted certificate instead, needing no client-side trust setup at all, and RELAY_V1_TLS=off is the escape hatch back to plain HTTP while a client migrates (loopback bind only — with an API key present, the default, the hub refuses plaintext /v1 on a network address). See Security’s “Migrating clients” for the full detail.

ClientHow
Open WebUISettings → Connections → OpenAI API → Base URL https://hub:8383/v1, API key unused or your RELAY_API_KEY
Continue.devIn config.json, add a model with "apiBase": "https://hub:8383/v1" and "provider": "openai"
LibreChat, AnythingLLM, Cline, Aider, ZedAny "custom OpenAI base URL" field — same value
OpenAI SDK (Python)OpenAI(base_url="https://hub:8383/v1", api_key="unused", http_client=httpx.Client(verify="<RELAY_STATE_DIR>/tls/ca.pem"))
OpenAI SDK (JS)new OpenAI({ baseURL: "https://hub:8383/v1", apiKey: "unused", httpAgent: new https.Agent({ ca: fs.readFileSync("<RELAY_STATE_DIR>/tls/ca.pem") }) })
curl / anything that speaks HTTPPOST https://hub:8383/v1/chat/completions --cacert <RELAY_STATE_DIR>/tls/ca.pem with a JSON body
relay chat / relay bench--ca-fingerprint <fingerprint> or RELAY_CA_FINGERPRINT=<fingerprint> pin the CA by fingerprint instead of a file — see above
bash
curl --cacert <RELAY_STATE_DIR>/tls/ca.pem https://hub:8383/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer '"$RELAY_API_KEY" \
  -d '{"model":"qwen3-8b","messages":[{"role":"user","content":"hello"}]}'
python
from openai import OpenAI
import httpx

client = OpenAI(
    base_url="https://hub:8383/v1",
    api_key="unused",
    http_client=httpx.Client(verify="<RELAY_STATE_DIR>/tls/ca.pem"),
)
resp = client.chat.completions.create(
    model="qwen3-8b",
    messages=[{"role": "user", "content": "hello"}],
    stream=True,
)
for chunk in resp:
    print(chunk.choices[0].delta.content or "", end="")

Ollama-native clients work too, on their own surface

A client that speaks Ollama’s wire dialect rather than OpenAI’s does not need an OpenAI-compatible mode: /api/tags, /api/chat and /api/generate are mounted on the same listener as /v1, behind the same API key. Point the client at the pool’s base URL and it works unchanged. It is a translation layer rather than a byte-for-byte proxy — Ollama’s format is newline-delimited JSON rather than server-sent events — so see the Ollama surface on API for which fields survive the trip.

What does not work

  • LM Studio’s own client protocol — not OpenAI-shaped.
  • Anything that loads weights in-process (HF transformers, torch, llama.cpp bindings used directly) — there’s no remote-GPU shim; the pool is a server you talk HTTP to, not a CUDA driver replacement.

A request that gets a 503

If the pool genuinely can’t place the requested model right now — nothing fits it as a replica and it can’t be sharded across what’s left either — you get an explicit error naming the shortfall, not a hang or a silent CPU fallback:

json
{
  "error": {
    "message": "\"qwen3-14b\" needs about 12800 MB VRAM: the host has 6144 MB free, ...",
    "type": "model_unavailable",
    "code": "model_unavailable"
  }
}

See Troubleshooting for exactly when this fires and how to read the numbers in the message.