Installation
Install the SDK via your preferred package manager:
npm install @modelriver/client# oryarn add @modelriver/client# orpnpm add @modelriver/clientOr use the CDN for quick prototyping:
1<script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>How It Works
- Your backend calls the ModelRiver async API (e.g.
/v1/ai/async) to start a background AI request. - ModelRiver returns an async response with
channel_id, a short‑lived one‑timews_token, and WebSocket connection details. - Your frontend uses this SDK to connect via WebSocket using
channel_id+ws_tokenand receive streaming responses. - 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+).
- For longer gaps and ModelRiver restarts, pass a
getTokencallback: your backend checks the user owns the request, then calls ModelRiver'sPOST /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.
npm install @modelriver/clientChoose your framework
| Integration | Description | Guide |
|---|---|---|
| React | useModelRiver hook for functional components | React Guide → |
| Vue | useModelRiver composable for Composition API | Vue Guide → |
| Angular | Observable-based ModelRiverService | Angular Guide → |
| Svelte | Real-time reactive stores for AI streaming | Svelte Guide → |
| Vanilla JS | Lightweight, zero-dependency browser SDK | Vanilla 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
- Initiation: Your backend calls the ModelRiver async API.
- Delivery: ModelRiver returns a
ws_tokenandchannel_id. - Connection: Your frontend uses the SDK to connect with the token.
- 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):
| Event | When |
|---|---|
connecting, connected, disconnected | Socket lifecycle |
reconnecting | The connection dropped; receives (attempt, delayMs) before the next attempt (1.8.0+) |
channel_joined, channel_error | The request's channel was joined, or the join failed |
step | A workflow step changed (see above) |
stream_start, stream_delta, stream_done, stream_error | Token streaming for requests sent with "stream": true (1.6.0+, see Streaming Tokens) |
response | The final result (or an error result) |
error | Request 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+):
1client.on('stream_delta', (chunk, text) => {2 // chunk.text is the new piece; text is everything streamed so far3 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 follows10});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
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 getToken8 debug?: boolean; // Enable console logging9 persist?: boolean; // Enable localStorage persistence10 storageKeyPrefix?: string; // Key prefix for storage11}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(withpersist, the default), andclient.reconnect()uses it on load. The framework adapters callreconnect()on mount. On unmount with a request still pending, the React, Vue and Angular adapters callclient.detach()(1.8.0+), which closes the connection but keeps the stored request for the next mount; Svelte'sdisconnect()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.
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 is7 },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:
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 it5 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:
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.id6 onSignal: (signal) => {7 if (signal.name === 'agent.joined') showAgent(signal.data);8 },9 onDeleted: () => closeChat(),10 onLive: () => reloadMessages(), // joined, first time and after each reconnection11 onStatus: (status) => setBadge(status), // connecting | live | reconnecting | closed12});13 14// When the chat closes15subscription.close();| Option | Description |
|---|---|
getSubscription | Returns { ws_token, websocket_url, websocket_channel } from your backend (camelCase works too), or null when there's nothing to follow |
onMessage | A 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 |
onDeleted | The session was deleted; the subscription closes |
onLive | Joined. Events sent while disconnected aren't replayed, so reload the conversation here |
onStatus | connecting, live, reconnecting or closed |
onError | A 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 |
retryDelaysMs | Custom 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
- Review the API documentation for backend setup.
- Explore Workflows for model orchestration.
- Check Backend pipeline for webhook patterns.