Installation
Install the SDK via your preferred package manager:
npm install @modelriver/client
# or
yarn add @modelriver/client
# or
pnpm add @modelriver/client
Or use the CDN for quick prototyping:
<script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>
How It Works
- Your backend calls the ModelRiver async API (e.g.
/api/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, channel joins, and automatic reconnection for transient network issues (while the page is open).
- For page refresh recovery, use the persistence + reconnect helpers (
persist,hasPendingRequest,reconnect,reconnectWithBackend) together with your backend/api/v1/ai/reconnectendpoint.
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:
// Frontend: call your backend
const response = await fetch('/api/ai/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: 'Hello AI' }),
});
const { ws_token } = await response.json();
2. Connect with the SDK
import { ModelRiverClient } from '@modelriver/client';
const client = new ModelRiverClient({
baseUrl: 'wss://api.modelriver.com/socket',
});
// Listen for responses
client.on('response', (data) => {
console.log('AI Response:', data.data);
});
client.on('error', (error) => {
console.error('Error:', error);
});
// Connect with the token
client.connect({ wsToken: ws_token });
Framework Adapters
The SDK includes first-class adapters for popular frameworks.
React
import { useModelRiver } from '@modelriver/client/react';
function ChatComponent() {
const {
connect,
response,
error,
isConnected,
steps
} = useModelRiver({
baseUrl: 'wss://api.modelriver.com/socket',
});
const handleSend = async () => {
const { ws_token } = await yourAPI.createRequest(message);
connect({ wsToken: ws_token });
};
return (
<div>
<button onClick={handleSend} disabled={isConnected}>
Send
</button>
{/* Progress steps */}
{steps.map((step) => (
<div key={step.id} className={step.status}>
{step.name}
</div>
))}
{/* Response */}
{response && <pre>{JSON.stringify(response.data, null, 2)}</pre>}
{/* Error */}
{error && <p className="error">{error}</p>}
</div>
);
}
Vue
<script setup>
import { useModelRiver } from '@modelriver/client/vue';
const { connect, response, error, isConnected, steps } = useModelRiver({
baseUrl: 'wss://api.modelriver.com/socket',
});
async function handleSend() {
const { ws_token } = await yourAPI.createRequest(message);
connect({ wsToken: ws_token });
}
</script>
<template>
<button @click="handleSend" :disabled="isConnected">Send</button>
<div v-for="step in steps" :key="step.id" :class="step.status">
{{ step.name }}
</div>
<pre v-if="response">{{ response.data }}</pre>
<p v-if="error" class="error">{{ error }}</p>
</template>
Angular
import { Component } from '@angular/core';
import { ModelRiverService } from '@modelriver/client/angular';
@Component({
selector: 'app-chat',
providers: [ModelRiverService],
template: `
<button (click)="send()" [disabled]="modelRiver.isConnected">Send</button>
<div *ngFor="let step of modelRiver.steps$ | async" [class]="step.status">
{{ step.name }}
</div>
<pre *ngIf="modelRiver.response$ | async as res">{{ res.data | json }}</pre>
<p *ngIf="modelRiver.error$ | async as err" class="error">{{ err }}</p>
`,
})
export class ChatComponent {
constructor(public modelRiver: ModelRiverService) {
this.modelRiver.init({ baseUrl: 'wss://api.modelriver.com/socket' });
}
async send() {
const { ws_token } = await this.api.createRequest(message);
this.modelRiver.connect({ wsToken: ws_token });
}
}
Svelte
<script>
import { createModelRiver } from '@modelriver/client/svelte';
const { response, error, isConnected, steps, connect } = createModelRiver({
baseUrl: 'wss://api.modelriver.com/socket',
});
async function send() {
const { ws_token } = await api.createRequest(message);
connect({ wsToken: ws_token });
}
</script>
<button on:click={send} disabled={$isConnected}>Send</button>
{#each $steps as step}
<div class={step.status}>{step.name}</div>
{/each}
{#if $response}
<pre>{JSON.stringify($response.data, null, 2)}</pre>
{/if}
{#if $error}
<p class="error">{$error}</p>
{/if}
Configuration Options
interface ModelRiverClientOptions {
// WebSocket URL (default: 'wss://api.modelriver.com/socket')
baseUrl?: string;
// Optional HTTP base URL for backend reconnect helper.
// When set, the SDK can call your backend's /api/v1/ai/reconnect
// to obtain a fresh ws_token for an existing async request after
// a page refresh (see "WebSocket Reconnect & Page Refresh Recovery").
apiBaseUrl?: string;
// Enable debug logging (default: false)
debug?: boolean;
// Enable localStorage persistence for page refresh recovery (default: true)
persist?: boolean;
// Storage key prefix (default: 'modelriver_')
storageKeyPrefix?: string;
// Heartbeat interval in ms (default: 30000)
heartbeatInterval?: number;
// Request timeout in ms (default: 300000)
requestTimeout?: number;
}
Events
| Event | Payload | Description |
|---|---|---|
| connecting | - | Connection attempt started |
| connected | - | Successfully connected to WebSocket |
| disconnected | reason?: string | Disconnected from WebSocket |
| response | AIResponse | AI response received |
| error | Error or string | Error occurred |
| step | WorkflowStep | Workflow step status updated |
| channel_joined | - | Successfully joined the channel |
| channel_error | reason: string | Failed to join channel |
Response Format
Async API Response
When you call /api/ai/async, you receive:
interface AsyncResponse {
message: string; // "success"
status: 'pending'; // Always "pending" for async
channel_id: string; // Unique channel ID
websocket_url: string; // WebSocket URL to connect to
websocket_channel: string; // Full channel name (e.g., "ai_response:uuid")
instructions?: {
websocket?: string;
webhook?: string;
};
test_mode?: boolean; // Present in test mode
}
WebSocket Response
When the AI completes, you receive an AIResponse via WebSocket:
interface AIResponse {
status: string; // 'success' or 'error'
channel_id?: string;
content?: string; // AI response text
model?: string; // Model used (e.g., 'gpt-4')
data?: unknown; // Structured output data
meta?: {
workflow?: string;
status?: string;
duration_ms?: number;
usage?: {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
};
};
error?: {
message: string;
details?: unknown;
};
}
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:
- Persist the active request (
channel_id, etc.) inlocalStorage(the SDK does this whenpersist: true) - On page load, ask your backend for a fresh
ws_tokenfor thatchannel_idvia/api/v1/ai/reconnect - Use the new token to reconnect the WebSocket
Backend: /api/v1/ai/reconnect
On your backend, expose a simple endpoint that:
- Verifies the
channel_idbelongs to the current user/project in your own database - Calls ModelRiver’s reconnect API with your project API key
- Returns only the safe connection fields (
channel_id,ws_token,websocket_url,websocket_channel) to the frontend
In pseudocode:
// Backend (Node / any server framework)
async function reconnectAsync(channel_id: string) {
// 1) Validate channel_id belongs to the current user/project in your DB
const job = await findPendingAsyncJobInYourDB(channel_id);
if (!job) throw new Error('No pending async request found');
// 2) Call ModelRiver's reconnect endpoint with your API key
const res = await fetch('https://api.modelriver.com/v1/ai/reconnect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.MODELRIVER_API_KEY}`, // never send this to the browser
},
body: JSON.stringify({ channel_id }),
});
if (!res.ok) throw new Error('ModelRiver reconnect failed');
// 3) Return only what the frontend needs to reconnect
const {
channel_id: new_channel_id,
ws_token,
websocket_url,
websocket_channel,
} = await res.json();
return { channel_id: new_channel_id, ws_token, websocket_url, websocket_channel };
}
Security: This function lives only on your backend.
The browser never seesMODELRIVER_API_KEYor 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):
import { ModelRiverClient } from '@modelriver/client';
const client = new ModelRiverClient({
baseUrl: 'wss://your-app.com/socket',
apiBaseUrl: 'https://your-app.com', // your backend base URL
persist: true,
});
// On initial request:
// 1) Your frontend calls your backend: POST /api/v1/ai/async (backend → ModelRiver)
// 2) Backend returns { channel_id, ws_token, websocket_url, websocket_channel }
// 3) Frontend connects:
client.connect({
channelId: asyncResponse.channel_id,
wsToken: asyncResponse.ws_token,
websocketUrl: asyncResponse.websocket_url,
websocketChannel: asyncResponse.websocket_channel,
});
// On page load, attempt to resume any pending request:
if (client.getState().hasPendingRequest) {
// This helper:
// - Reads the stored channel_id from localStorage
// - Calls your backend's /api/v1/ai/reconnect (using apiBaseUrl)
// - Connects with the fresh ws_token + websocket_url/channel
client.reconnectWithBackend().catch((err) => {
console.error('Reconnect failed', err);
});
}
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:
| Step | Description |
|---|---|
| queue | Request is being queued |
| process | AI is processing the request |
| receive | Waiting for response delivery |
| complete | Response 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:
const client = new ModelRiverClient({
persist: false,
});
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:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>
</head>
<body>
<button id="send">Send</button>
<pre id="response"></pre>
<script>
const client = new ModelRiver.ModelRiverClient({
baseUrl: 'wss://api.modelriver.com/socket',
});
client.on('response', (data) => {
document.getElementById('response').textContent =
JSON.stringify(data, null, 2);
});
document.getElementById('send').addEventListener('click', async () => {
const res = await fetch('/api/ai/request', { method: 'POST' });
const { ws_token } = await res.json();
client.connect({ wsToken: ws_token });
});
</script>
</body>
</html>
Next Steps
- Review the API documentation to understand backend integration
- Explore Workflows to configure AI models and fallbacks
- Check Troubleshooting for common issues