Documentation

Real-time AI Streaming SDK

Connect your frontend directly to AI streams. Handle WebSocket lifecycles, real-time token delivery, and reconnects after drops and reloads with our first-class SDKs.

Installation

Install the SDK via your preferred package manager:

Bash
npm install @modelriver/client
# or
yarn add @modelriver/client
# or
pnpm add @modelriver/client

Or use the CDN for quick prototyping:

HTML
1<script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>

How It Works

  1. Your backend calls the ModelRiver async API (e.g. /v1/ai/async) to start a background AI request.
  2. ModelRiver returns an async response with channel_id, a short‑lived one‑time ws_token, and WebSocket connection details.
  3. Your frontend uses this SDK to connect via WebSocket using channel_id + ws_token and receive streaming responses.
  4. The SDK handles heartbeats and channel joins, and reconnects on its own after network drops, the 1-hour connection cutoff and page reloads within about 5 minutes, using single-use resume tokens (1.8.0+).
  5. For longer gaps and ModelRiver restarts, pass a getToken callback: your backend checks the user owns the request, then calls ModelRiver's POST /v1/ai/reconnect (see Reconnecting).
Frontend Your Backend ModelRiver
│ │ │
│ 1. Request AI │ │
│───────────────────>│ │
│ │ 2. POST │
│ │ /v1/ai/async │
│ │───────────────────>│
│ │ │
│ │ 3. channel_id, │
│ │ ws_token, │
│ │ websocket_url │
│ │<───────────────────│
│ 4. Return token │ │
│<───────────────────│ │
│ │ │
│ 5. SDK connects via WebSocket │
│─────────────────────────────────────────>│
│ │ │
│ 6. AI response streamed │
│<─────────────────────────────────────────│

Quick Start

The fastest way to get started is to install the core library and use one of our first-class adapters.

Bash
npm install @modelriver/client

Choose your framework

IntegrationDescriptionGuide
ReactuseModelRiver hook for functional componentsReact Guide →
VueuseModelRiver composable for Composition APIVue Guide →
AngularObservable-based ModelRiverServiceAngular Guide →
SvelteReal-time reactive stores for AI streamingSvelte Guide →
Vanilla JSLightweight, zero-dependency browser SDKVanilla JS Guide →

Core Concepts

1. Unified Real-time Stream

The SDK manages the WebSocket connection to ModelRiver. It handles heartbeats and state synchronization, and reconnects after drops with single-use resume tokens (see Reconnecting), so your frontend reflects the latest AI status.

2. Async Lifecycle

  1. Initiation: Your backend calls the ModelRiver async API.
  2. Delivery: ModelRiver returns a ws_token and channel_id.
  3. Connection: Your frontend uses the SDK to connect with the token.
  4. Streaming: Response data and workflow steps stream in real-time.

3. Workflow Steps

The SDK tracks progress through four defined steps:

  • queue: Request is being queued.
  • process: AI is processing the prompt.
  • receive: AI has finished; data is being delivered.
  • complete: Request finished successfully.

4. Events

Subscribe with client.on(event, callback):

EventWhen
connecting, connected, disconnectedSocket lifecycle
reconnectingThe connection dropped; receives (attempt, delayMs) before the next attempt (1.8.0+)
channel_joined, channel_errorThe request's channel was joined, or the join failed
stepA workflow step changed (see above)
stream_start, stream_delta, stream_done, stream_errorToken streaming for requests sent with "stream": true (1.6.0+, see Streaming Tokens)
responseThe final result (or an error result)
errorRequest error, or a connection error once the client has stopped retrying

Streaming Tokens

When your backend sends the async request with "stream": true, the answer arrives token by token before the final response event (1.6.0+):

TYPESCRIPT
1client.on('stream_delta', (chunk, text) => {
2 // chunk.text is the new piece; text is everything streamed so far
3 renderAnswer(text);
4});
5 
6client.on('stream_done', (done) => renderAnswer(done.accumulated_text));
7 
8client.on('stream_error', (err) => {
9 console.error('Stream failed:', err.message); // a final `response` with an error status follows
10});

The text so far is also in client.getState().streamText. The framework adapters track the request's steps and final response; for token-by-token text, use the client's events as above. See Streaming over WebSocket for the payloads.

Configuration Options

TYPESCRIPT
1interface ModelRiverClientOptions {
2 baseUrl?: string; // WebSocket gateway (default: api.modelriver.com)
3 getToken?: (request: { channelId: string; websocketChannel?: string }) =>
4 Promise<ReconnectInfo | null>; // New token from your backend for longer gaps (1.8.0+)
5 retryDelaysMs?: number[]; // Waits between reconnect attempts (default 1s, 2s, 5s, 10s, then 30s)
6 tokenRefreshInterval?: number; // How often to rotate the resume token in ms (default 120000)
7 apiBaseUrl?: string; // Deprecated: use getToken
8 debug?: boolean; // Enable console logging
9 persist?: boolean; // Enable localStorage persistence
10 storageKeyPrefix?: string; // Key prefix for storage
11}

Reconnecting

Listening tokens are single-use: ModelRiver consumes a ws_token when a socket connects with it. From 1.8.0, ModelRiver hands the client its next token, a resume token, over the channel it already joined, and the client swaps it for a fresh one every 2 minutes. When the connection drops, the client reconnects with that token, with backoff.

Reconnects on its own (no backend route needed):

  • Network drops
  • ModelRiver's 1-hour connection cutoff
  • Page reloads within about 5 minutes. The resume token is kept in localStorage (with persist, the default), and client.reconnect() uses it on load. The framework adapters call reconnect() on mount. On unmount with a request still pending, the React, Vue and Angular adapters call client.detach() (1.8.0+), which closes the connection but keeps the stored request for the next mount; Svelte's disconnect() ends the request

Needs getToken:

  • Gaps longer than about 5 minutes (a closed tab, a laptop asleep): resume tokens expire 5 minutes after they're issued
  • ModelRiver deploys and restarts: tokens live in server memory, so a restart erases them

Without getToken, the client stops with an error once it has no usable token, instead of retrying. With getToken, it stops after 3 new tokens in a row fail to get it rejoined, or after an hour without a successful join.

stream_delta events sent while disconnected are lost; getState().streamText keeps what arrived before the drop. The final response is buffered on the server for 10 minutes and delivered when the client rejoins.

getToken

getToken asks your backend for a new token. Return null when the request is gone (ModelRiver answered 404) and the client stops; throw for anything temporary and the client retries with backoff.

TYPESCRIPT
1const client = new ModelRiverClient({
2 getToken: async ({ channelId }) => {
3 const res = await fetch(`/api/ai/requests/${channelId}/token`, { method: 'POST' });
4 if (res.status === 404) return null;
5 if (!res.ok) throw new Error(`Token route failed: ${res.status}`);
6 return res.json(); // ModelRiver's /v1/ai/reconnect response, as is
7 },
8});

ModelRiver only checks that the channel belongs to your project, not which of your users started it. Your route must check that the signed-in user owns the channel_id before it calls POST /v1/ai/reconnect with your API key:

JAVASCRIPT
1app.post('/api/ai/requests/:channelId/token', requireUser, async (req, res) => {
2 const { channelId } = req.params;
3 
4 // Only the user who started this request may listen to it
5 const owned = await db.aiRequests.findOne({ channelId, userId: req.user.id });
6 if (!owned) return res.status(404).end();
7 
8 const mr = await fetch('https://api.modelriver.com/v1/ai/reconnect', {
9 method: 'POST',
10 headers: {
11 Authorization: `Bearer ${process.env.MODELRIVER_API_KEY}`,
12 'Content-Type': 'application/json',
13 },
14 body: JSON.stringify({ channel_id: channelId }),
15 });
16 if (mr.status === 404) return res.status(404).end();
17 if (!mr.ok) return res.status(502).end();
18 res.json(await mr.json()); // { channel_id, project_id, ws_token, websocket_url, websocket_channel }
19});

Resume tokens are scoped like the first ws_token: single-use, valid for 5 minutes, and able to join only that request's channel. They can't start requests. Each refresh revokes the previous token, and the final response revokes the last one.

Live Sessions

subscribeToSession (1.7.0+) follows a session-memory conversation as it happens: AI turns, user or human-agent turns your backend appends, signals your backend sends, and deletion. Use it to show an agent's reply in a customer's chat, or a customer's new message in your team's dashboard, without polling.

Your backend gets a single-use token from POST /v1/sessions/:id/subscribe (with your API key, after checking the user may see that conversation) and returns the JSON as is. The SDK calls it again for every reconnection:

TYPESCRIPT
1import { subscribeToSession } from '@modelriver/client';
2 
3const subscription = subscribeToSession({
4 getSubscription: () => fetch(`/api/chats/${chatId}/live`, { method: 'POST' }).then((r) => r.json()),
5 onMessage: (message) => addMessage(message), // merge by message.id
6 onSignal: (signal) => {
7 if (signal.name === 'agent.joined') showAgent(signal.data);
8 },
9 onDeleted: () => closeChat(),
10 onLive: () => reloadMessages(), // joined, first time and after each reconnection
11 onStatus: (status) => setBadge(status), // connecting | live | reconnecting | closed
12});
13 
14// When the chat closes
15subscription.close();
OptionDescription
getSubscriptionReturns { ws_token, websocket_url, websocket_channel } from your backend (camelCase works too), or null when there's nothing to follow
onMessageA new turn: { id, session_id, role, author, author_name, content, created_at }, with author user, ai or agent. Ids match GET /v1/sessions/:id
onSignal{ session_id, name, data, sent_at }, sent by your backend with POST /v1/sessions/:id/signals. Not stored
onDeletedThe session was deleted; the subscription closes
onLiveJoined. Events sent while disconnected aren't replayed, so reload the conversation here
onStatusconnecting, live, reconnecting or closed
onErrorA connection or join failed. It retries after 1s, 2s, 5s, 10s, then every 30s, unless the session is gone or the token is for another session
retryDelaysMsCustom retry waits in ms; the last one repeats

Failed AI requests and test-mode runs aren't announced. Connections close after an hour and reconnect with a fresh token. See Live updates for the server side.

Next Steps