Relay docs

Using Relay from Nook

Nook is the Windows desktop companion (the island at the top of the screen that shows what Claude Code is doing, answers its permission prompts, and runs its own agent). Nook uses Relay as its only model backend. It speaks the OpenAI-compatible API, so all it needs is a base URL and a key; everything else — providers, credentials, budgets, telemetry — stays on the gateway.

This page is the Relay side of that integration: what to configure, what Nook sends, and which gateway features it leans on. Nook's own setup walkthrough lives in its repository (companion/docs/relay-setup-guide.md).

#Connect

Setting (Nook → Settings → Relay)Value
Base URLhttps://<your-relay>/v1 (locally http://localhost:5300/v1). With or without the /v1 suffix.
API keyA Relay key, sent as Authorization: Bearer app_live_…. Nook stores it in Windows Credential Manager.

Create the key in the panel under API Keys. Scopes it needs:

ScopeUsed for
chat.completionsChat, streaming, tool calling, agent runs, voice sessions.
models.readThe model picker (GET /v1/models).
traces.writeOnly if Nook's trace export is turned on — it then sends its own agent spans to POST /v1/traces/ingest.

Add a model allow-list to keep Nook to the models you intend, and a rate limit or a budget if it will run unattended. File uploads (/v1/files) and realtime sessions need no extra scope. Do not grant data.query or connectors.use unless you want Nook's users running dataset or connector agents — see Agents below for why those need a trustworthy user id.

Nook does not use the panel's Playground proxy (api/playground/*): that is a panel-cookie trust path for the browser. Nook calls /v1/* directly with its own key, like any other application.

#Attribute the calls

Nook sends these headers on every request, so its traffic is easy to find and group on the Traces and Sessions pages:

HeaderValue
X-Relay-Session-IdOne id per Nook conversation, so every turn lands in the same session.
X-Relay-Tagsnook (comma-separate more, e.g. nook,desktop).
X-Relay-User-IdThe signed-in user's Entra object id, when Nook knows it (see Who the user is).

Each response returns X-Relay-Request-Id, X-Relay-Trace-Id, X-Relay-Observation-Id and, on non-streamed calls, X-Relay-Cost-Usd; agent runs with a thread also return X-Relay-Thread-Id. Nook reads these back and shows the cost and trace id against the answer. See Observability for what the headers drive.

#Pick a model

GET /v1/models lists the enabled aliases the key may use, with their capabilities:

{ "id": "gpt-4o", "object": "model", "owned_by": "relay",
  "capabilities": { "tools": true, "vision": true, "realtime": false }, "context_window": 128000 }

Nook reads the flags rather than carrying a hard-coded model table:

  • it offers a model for agent work only when capabilities.tools is true;
  • it attaches screenshots only when capabilities.vision is true;
  • it treats a missing context_window as unknown and falls back to a conservative budget;
  • models with capabilities.realtime are offered for voice, not chat — they are served by /v1/realtime.

The flags are advisory: the gateway does not refuse a request because of them, so a wrong tick in Models → Edit shows up as a provider error, not a gateway one. Set them per model in the panel. See Models.

#Tools, streaming and images

Nook's agent loop uses standard OpenAI tool calling on POST /v1/chat/completions, and it works identically on OpenAI, Anthropic and Gemini models — Relay translates tools, tool_choice, assistant tool_calls and tool results for each family. With stream: true, tool calls arrive as delta.tool_calls fragments keyed by index; Nook concatenates function.arguments per index and stops at finish_reason: "tool_calls".

Screenshots go in as image_url content parts with a data:image/png;base64,… URL, at most Gateway:MaxImageBytes (5 MB decoded by default) each. Gemini models cannot fetch https:// images through the gateway — send them inline, which Nook always does.

The exact shapes, the per-provider translation table and the streamed-tool-call format are in Chat completions.

#Files

To ask about a document, Nook uploads it once with POST /v1/files and names the returned id in file_ids on each chat request that needs it. The gateway extracts the text at upload (spreadsheets, decks, Word, PDF, Markdown, CSV and plain text) and injects it for any model, so the attachment works on models with no file support of their own.

Nook sends the same X-Relay-User-Id on the upload and on the chat that names it. An upload made with a user id can only be read by requests carrying that same id; one made without is readable by the whole workspace. Attachments expire after 24 hours by default. See File attachments.

#Relay agents

Besides its own built-in agent, Nook can talk to the agents you compose in Relay. It discovers them with GET /v1/agents and runs them with POST /v1/agents/{id}/chat/completions, exactly as described in the Agents API.

What Nook does with the directory it gets back:

Field on GET /v1/agentsNook's behaviour
model_allowed: falseHidden — the key's allow-list excludes the agent's model, so a run would be refused.
connectors: trueHidden — connector agents read a user's own mailbox and files, which does not fit a desktop agent running as a service key.
requires_user_header: true (a dataset or private agent)Shown only when the user is signed in, because the run needs X-Relay-User-Id.
realtime: trueOffered as a voice agent as well as a chat one.

Two things about the run itself:

  • Threads. A follow-up in the same Nook conversation sends ?thread=<id>, so the agent keeps its memory server-side and Nook does not resend history. The response carries X-Relay-Thread-Id.
  • Streaming. Nook streams plain agent runs. A threaded run, a tool-loop run (MCP servers attached) or a dataset agent is answered in one piece; Nook sends stream: false for dataset agents, because the gateway returns 400 stream_not_supported for them rather than silently downgrading.

Nook appends its own short system message to every agent request so the model can emit cards (headline figures, charts, tables) in its answer. Relay places a caller's system message after the agent's own prompt, so the agent's instructions still come first.

#Voice

Nook's voice calls use Relay's realtime API. It mints a session with POST /v1/realtime/sessions for a model, or POST /v1/agents/{id}/realtime/sessions for an agent, then opens the WebSocket at the url the mint returned (/v1/realtime?session=…), prefixed with the gateway origin and the scheme swapped to ws/wss. The minted token is single-use and expires in 60 seconds, so Nook's key never appears in the socket URL.

Relay therefore needs at least one model marked Realtime in the catalogue for the microphone to appear in Nook. Pick the model, voice and transcription settings under Settings → Relay → Voice in Nook. During a call Nook still executes its own tools (the gateway forwards every function call to the client except its own search_knowledge_base), the user can interrupt by talking, and typed messages join the call. A session minted for an agent carries the same X-Relay-User-Id as a chat run, since a private agent is checked at the mint.

#Who the user is

Nook can sign in through the flowbyte identity app (OAuth PKCE, public client NOOK). When it has, it sends the user's Entra object id (oid) in X-Relay-User-Id — the id space Relay uses for agent sharing, dataset row-level security, connector connections and per-user file ownership. The user's email is never sent.

Until the identity app issues the oid in its tokens, Nook has nothing to put in the header, so dataset and private agents stay hidden in Nook and uploads are workspace-scoped rather than per-user. The header is caller-asserted either way: Relay trusts the key holder to name its end user, which is why data.query and connectors.use should be granted to Nook's key only once that id is genuinely the signed-in person's. See Agents for the id-space detail and Dataset agents for what the header controls.

#Checklist

  1. Create a key under API Keys with chat.completions and models.read (plus traces.write if you want Nook's own spans), a model allow-list and, if unattended, a rate limit.
  2. Make sure the models Nook should use carry the right Supports tool calling / Supports vision / Context window values, and mark one Realtime if you want voice.
  3. Paste the base URL and key into Nook (Settings → Relay → Test connection).
  4. Find Nook's traffic on Traces by the nook tag, and group a conversation on Sessions.