Step 5 of the quickstart says “point it to your model, boom done.” This page is the boom-done part: a sample relay you run on your own server, so your provider key stays in your environment — never on a browser page, never on NetShow. That relay is built and validated — the proof conversation ran through it verbatim — and this static preview shows the exact wiring, with the live hookup to follow. The wire monitor at the bottom shows every request this page makes.
Page side: one fetch('/turn') + the documented five-line feeder — the face
thinks while the model thinks, speaks while it streams, settles when it's done. Server side: the
sample relay forwards the turn to the configured model endpoint and streams the reply back as plain text.
This box is seeded from personality_desc inside the sealed file — the persona
your customer typed in the studio travels with the avatar. On this preview the page sends it per turn;
in production, set SYSTEM_PROMPT in the relay's environment instead and the relay
pins it server-side, ignoring anything a visitor's browser sends.
⬇ text-turn-relay.mjs — one file, zero npm installs, Node 18+. It has one job: take a turn from your page, forward it to your model endpoint, stream the reply back. Read it — it's short on purpose.
Your key comes from your shell, on your machine. It is never written into a file and the page never sees it. No key at all for local models.
MODEL_URL=https://api.openai.com/v1/chat/completions \ MODEL_NAME=gpt-4o-mini \ MODEL_KEY=$OPENAI_API_KEY \ node text-turn-relay.mjs
MODEL_KIND=anthropic \ MODEL_URL=https://api.anthropic.com/v1/messages \ MODEL_NAME=claude-sonnet-5 \ MODEL_KEY=$ANTHROPIC_API_KEY \ node text-turn-relay.mjs
MODEL_URL=http://127.0.0.1:11434/v1/chat/completions \ MODEL_NAME=llama3.2 \ node text-turn-relay.mjs
/turn on the same origin as your page — then talkThe avatar element and the relay both live by the same-origin law. Proxy the route through the web server that already serves your page (this preview is wired for exactly that; its live proxy hookup follows):
location = /turn {
proxy_pass http://127.0.0.1:8787/turn;
proxy_buffering off;
proxy_read_timeout 180s;
}
And the page side is the quickstart's five-line feeder plus one fetch:
import { attachTextAgent } from '/alive-site/alive-adapter-text.js'; const feeder = attachTextAgent(document.querySelector('netshow-alive').session); feeder.userSaid(question); feeder.beginTurn(); const res = await fetch('/turn', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ user: question, history }) }); const reader = res.body.getReader(); const dec = new TextDecoder(); for (;;) { const { done, value } = await reader.read(); if (done) break; feeder.delta(dec.decode(value, { stream: true })); } feeder.endTurn(); // boom — done
| request | POST /turn · JSON · { user, system?, history? } —
user ≤ 4000 chars; history ≤ 40 turns of {role:'user'|'assistant', content};
system is ignored when the relay pins SYSTEM_PROMPT server-side |
|---|---|
| reply | 200 text/plain, streamed as the model streams — feed each chunk
straight into feeder.delta(). Header X-Brain-Model names the configured model |
| errors | terse JSON: 400 bad turn shape · 429 relay busy ·
502 model endpoint failed (details stay in the relay's server log — never in the browser) |
| keys | live in the relay's environment on your server, used in exactly one place (the upstream auth header), never logged, never on this wire |