Real-time AI in your frontend

The ModelRiver client SDK handles WebSocket connections, reconnection, and progress tracking so you can focus on building great user experiences.

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. /api/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, channel joins, and automatic reconnection for transient network issues (while the page is open).
  5. For page refresh recovery, use the persistence + reconnect helpers (persist, hasPendingRequest, reconnect, reconnectWithBackend) together with your backend /api/v1/ai/reconnect endpoint.
Frontend Your Backend ModelRiver
1. Request AI
>
2. POST /api/v1/ai/async
>
3. channel_id,
ws_token,
websocket_url
<
4. Return token
<
5. SDK connects via WebSocket
>
6. AI response streamed
<

Quick Start

1. Get a token from your backend

Your backend makes the async request and returns the token:

JAVASCRIPT
1// Frontend: call your backend
2const response = await fetch('/api/ai/chat', {
3 method: 'POST',
4 headers: { 'Content-Type': 'application/json' },
5 body: JSON.stringify({ message: 'Hello AI' }),
6});
7 
8const { ws_token } = await response.json();

2. Connect with the SDK

JAVASCRIPT
1import { ModelRiverClient } from '@modelriver/client';
2 
3const client = new ModelRiverClient({
4 baseUrl: 'wss://api.modelriver.com/socket',
5});
6 
7// Listen for responses
8client.on('response', (data) => {
9 console.log('AI Response:', data.data);
10});
11 
12client.on('error', (error) => {
13 console.error('Error:', error);
14});
15 
16// Connect with the token
17client.connect({ wsToken: ws_token });

Framework Adapters

The SDK includes first-class adapters for popular frameworks.

React

TSX
1import { useModelRiver } from '@modelriver/client/react';
2 
3function ChatComponent() {
4 const {
5 connect,
6 response,
7 error,
8 isConnected,
9 steps
10 } = useModelRiver({
11 baseUrl: 'wss://api.modelriver.com/socket',
12 });
13 
14 const handleSend = async () => {
15 const { ws_token } = await yourAPI.createRequest(message);
16 connect({ wsToken: ws_token });
17 };
18 
19 return (
20 <div>
21 <button onClick={handleSend} disabled={isConnected}>
22 Send
23 </button>
24
25 {/* Progress steps */}
26 {steps.map((step) => (
27 <div key={step.id} className={step.status}>
28 {step.name}
29 </div>
30 ))}
31
32 {/* Response */}
33 {response && <pre>{JSON.stringify(response.data, null, 2)}</pre>}
34
35 {/* Error */}
36 {error && <p className="error">{error}</p>}
37 </div>
38 );
39}

Vue

VUE
1<script setup>
2import { useModelRiver } from '@modelriver/client/vue';
3 
4const { connect, response, error, isConnected, steps } = useModelRiver({
5 baseUrl: 'wss://api.modelriver.com/socket',
6});
7 
8async function handleSend() {
9 const { ws_token } = await yourAPI.createRequest(message);
10 connect({ wsToken: ws_token });
11}
12</script>
13 
14<template>
15 <button @click="handleSend" :disabled="isConnected">Send</button>
16
17 <div v-for="step in steps" :key="step.id" :class="step.status">
18 {{ step.name }}
19 </div>
20
21 <pre v-if="response">{{ response.data }}</pre>
22 <p v-if="error" class="error">{{ error }}</p>
23</template>

Angular

TYPESCRIPT
1import { Component } from '@angular/core';
2import { ModelRiverService } from '@modelriver/client/angular';
3 
4@Component({
5 selector: 'app-chat',
6 providers: [ModelRiverService],
7 template: `
8 <button (click)="send()" [disabled]="modelRiver.isConnected">Send</button>
9
10 <div *ngFor="let step of modelRiver.steps$ | async" [class]="step.status">
11 {{ step.name }}
12 </div>
13
14 <pre *ngIf="modelRiver.response$ | async as res">{{ res.data | json }}</pre>
15 <p *ngIf="modelRiver.error$ | async as err" class="error">{{ err }}</p>
16 `,
17})
18export class ChatComponent {
19 constructor(public modelRiver: ModelRiverService) {
20 this.modelRiver.init({ baseUrl: 'wss://api.modelriver.com/socket' });
21 }
22 
23 async send() {
24 const { ws_token } = await this.api.createRequest(message);
25 this.modelRiver.connect({ wsToken: ws_token });
26 }
27}

Svelte

SVELTE
1<script>
2 import { createModelRiver } from '@modelriver/client/svelte';
3 
4 const { response, error, isConnected, steps, connect } = createModelRiver({
5 baseUrl: 'wss://api.modelriver.com/socket',
6 });
7 
8 async function send() {
9 const { ws_token } = await api.createRequest(message);
10 connect({ wsToken: ws_token });
11 }
12</script>
13 
14<button on:click={send} disabled={$isConnected}>Send</button>
15 
16{#each $steps as step}
17 <div class={step.status}>{step.name}</div>
18{/each}
19 
20{#if $response}
21 <pre>{JSON.stringify($response.data, null, 2)}</pre>
22{/if}
23 
24{#if $error}
25 <p class="error">{$error}</p>
26{/if}

Configuration Options

TYPESCRIPT
1interface ModelRiverClientOptions {
2 // WebSocket URL (default: 'wss://api.modelriver.com/socket')
3 baseUrl?: string;
4 
5 // Optional HTTP base URL for backend reconnect helper.
6 // When set, the SDK can call your backend's /api/v1/ai/reconnect
7 // to obtain a fresh ws_token for an existing async request after
8 // a page refresh (see "WebSocket Reconnect & Page Refresh Recovery").
9 apiBaseUrl?: string;
10
11 // Enable debug logging (default: false)
12 debug?: boolean;
13
14 // Enable localStorage persistence for page refresh recovery (default: true)
15 persist?: boolean;
16
17 // Storage key prefix (default: 'modelriver_')
18 storageKeyPrefix?: string;
19
20 // Heartbeat interval in ms (default: 30000)
21 heartbeatInterval?: number;
22
23 // Request timeout in ms (default: 300000)
24 requestTimeout?: number;
25}

Events

EventPayloadDescription
connecting-Connection attempt started
connected-Successfully connected to WebSocket
disconnectedreason?: stringDisconnected from WebSocket
responseAIResponseAI response received
errorError or stringError occurred
stepWorkflowStepWorkflow step status updated
channel_joined-Successfully joined the channel
channel_errorreason: stringFailed to join channel

Response Format

Async API Response

When you call /api/ai/async, you receive:

TYPESCRIPT
1interface AsyncResponse {
2 message: string; // "success"
3 status: 'pending'; // Always "pending" for async
4 channel_id: string; // Unique channel ID
5 websocket_url: string; // WebSocket URL to connect to
6 websocket_channel: string; // Full channel name (e.g., "ai_response:uuid")
7 instructions?: {
8 websocket?: string;
9 webhook?: string;
10 };
11 test_mode?: boolean; // Present in test mode
12}

WebSocket Response

When the AI completes, you receive an AIResponse via WebSocket:

TYPESCRIPT
1interface AIResponse {
2 status: string; // 'success' or 'error'
3 channel_id?: string;
4 content?: string; // AI response text
5 model?: string; // Model used (e.g., 'gpt-4')
6 data?: unknown; // Structured output data
7 meta?: {
8 workflow?: string;
9 status?: string;
10 duration_ms?: number;
11 usage?: {
12 prompt_tokens?: number;
13 completion_tokens?: number;
14 total_tokens?: number;
15 };
16 };
17 error?: {
18 message: string;
19 details?: unknown;
20 };
21}

WebSocket Reconnect & Page Refresh Recovery

Async requests are processed in the background and identified by a channel_id.
The WebSocket ws_token returned from /api/v1/ai/async is:

  • short‑lived (≈5 minutes)
  • single‑use (consumed on first successful WebSocket authentication)

This means you cannot safely reuse the original ws_token after a page refresh.
Instead, you should:

  1. Persist the active request (channel_id, etc.) in localStorage (the SDK does this when persist: true)
  2. On page load, ask your backend for a fresh ws_token for that channel_id via /api/v1/ai/reconnect
  3. Use the new token to reconnect the WebSocket

Backend: /api/v1/ai/reconnect

On your backend, expose a simple endpoint that:

  1. Verifies the channel_id belongs to the current user/project in your own database
  2. Calls ModelRiver’s reconnect API with your project API key
  3. Returns only the safe connection fields (channel_id, ws_token, websocket_url, websocket_channel) to the frontend

In pseudocode:

TypeScript
1// Backend (Node / any server framework)
2async function reconnectAsync(channel_id: string) {
3 // 1) Validate channel_id belongs to the current user/project in your DB
4 const job = await findPendingAsyncJobInYourDB(channel_id);
5 if (!job) throw new Error('No pending async request found');
6 
7 // 2) Call ModelRiver's reconnect endpoint with your API key
8 const res = await fetch('https://api.modelriver.com/v1/ai/reconnect', {
9 method: 'POST',
10 headers: {
11 'Content-Type': 'application/json',
12 'Authorization': `Bearer ${process.env.MODELRIVER_API_KEY}`, // never send this to the browser
13 },
14 body: JSON.stringify({ channel_id }),
15 });
16 
17 if (!res.ok) throw new Error('ModelRiver reconnect failed');
18 
19 // 3) Return only what the frontend needs to reconnect
20 const {
21 channel_id: new_channel_id,
22 ws_token,
23 websocket_url,
24 websocket_channel,
25 } = await res.json();
26 
27 return { channel_id: new_channel_id, ws_token, websocket_url, websocket_channel };
28}

Security: This function lives only on your backend.
The browser never sees MODELRIVER_API_KEY or calls ModelRiver HTTP APIs directly.

Frontend: reconnecting with the SDK (TypeScript)

Configure the client with both baseUrl (WebSocket) and apiBaseUrl (HTTP base for your backend):

TypeScript
1import { ModelRiverClient } from '@modelriver/client';
2 
3const client = new ModelRiverClient({
4 baseUrl: 'wss://your-app.com/socket',
5 apiBaseUrl: 'https://your-app.com', // your backend base URL
6 persist: true,
7});
8 
9// On initial request:
10// 1) Your frontend calls your backend: POST /api/v1/ai/async (backend → ModelRiver)
11// 2) Backend returns { channel_id, ws_token, websocket_url, websocket_channel }
12// 3) Frontend connects:
13client.connect({
14 channelId: asyncResponse.channel_id,
15 wsToken: asyncResponse.ws_token,
16 websocketUrl: asyncResponse.websocket_url,
17 websocketChannel: asyncResponse.websocket_channel,
18});
19 
20// On page load, attempt to resume any pending request:
21if (client.getState().hasPendingRequest) {
22 // This helper:
23 // - Reads the stored channel_id from localStorage
24 // - Calls your backend's /api/v1/ai/reconnect (using apiBaseUrl)
25 // - Connects with the fresh ws_token + websocket_url/channel
26 client.reconnectWithBackend().catch((err) => {
27 console.error('Reconnect failed', err);
28 });
29}

For CDN usage, the same pattern applies: your backend should call /api/v1/ai/async and /api/v1/ai/reconnect, and the browser should talk only to your backend, never sending the ModelRiver API key directly.

Workflow Steps

The SDK tracks progress through four steps:

StepDescription
queueRequest is being queued
processAI is processing the request
receiveWaiting for response delivery
completeResponse received successfully

Each step has a status: pending, loading, success, or error.

Persistence

By default, the SDK persists active requests to localStorage. If the user refreshes the page mid-request, the SDK automatically reconnects and resumes receiving the response.

Disable persistence if you don't need it:

JAVASCRIPT
1const client = new ModelRiverClient({
2 persist: false,
3});

Security

The ws_token is a short-lived JWT that:

  • Expires after 5 minutes
  • Contains project_id, channel_id, and connection details
  • Is consumed on first use (one-time token)

Important: Always obtain tokens from your backend. Never expose your ModelRiver API key in frontend code.

Browser Support

  • Chrome 60+
  • Firefox 55+
  • Safari 12+
  • Edge 79+

CDN Usage

For projects without a build step:

HTML
1<!DOCTYPE html>
2<html>
3<head>
4 <script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>
5</head>
6<body>
7 <button id="send">Send</button>
8 <pre id="response"></pre>
9 
10 <script>
11 const client = new ModelRiver.ModelRiverClient({
12 baseUrl: 'wss://api.modelriver.com/socket',
13 });
14 
15 client.on('response', (data) => {
16 document.getElementById('response').textContent =
17 JSON.stringify(data, null, 2);
18 });
19 
20 document.getElementById('send').addEventListener('click', async () => {
21 const res = await fetch('/api/ai/request', { method: 'POST' });
22 const { ws_token } = await res.json();
23 client.connect({ wsToken: ws_token });
24 });
25 </script>
26</body>
27</html>

Next Steps