Official ModelRiver client SDK for real-time AI response streaming via WebSockets
npm install @modelriver/clientOfficial ModelRiver client SDK for real-time AI response streaming via WebSockets.
- WebSocket streaming - Receive AI responses in real-time via Phoenix Channels
- Auto-reconnection - Automatically reconnects on connection loss
- Persistence + reconnect - Survives page refreshes with localStorage + backend reconnect
- Framework adapters - First-class support for React, Vue, Angular, and Svelte
- CDN ready - Use via script tag without a build step
- TypeScript - Full type definitions included
- Lightweight - ~15KB minified (including Phoenix.js)
``bash`
npm install @modelriver/clientor
yarn add @modelriver/clientor
pnpm add @modelriver/client
`html`
Your backend calls the ModelRiver /api/v1/ai/async endpoint and receives connection details:
`javascript
// Your backend endpoint proxies to ModelRiver
const response = await fetch('/api/ai/request', {
method: 'POST',
body: JSON.stringify({ message: 'Hello AI' }),
});
// Response from /api/v1/ai/async:
// {
// "message": "success",
// "status": "pending",
// "channel_id": "a1b2c3d4-...",
// "ws_token": "one-time-websocket-token",
// "websocket_url": "wss://api.modelriver.com/socket",
// "websocket_channel": "ai_response:PROJECT_ID:a1b2c3d4-..."
// }
const { channel_id, ws_token, websocket_url, websocket_channel } = await response.json();
`
`javascript
import { ModelRiverClient } from '@modelriver/client';
const client = new ModelRiverClient({
baseUrl: 'wss://api.modelriver.com/socket',
});
client.on('response', (data) => {
console.log('AI Response:', data);
});
client.on('error', (error) => {
console.error('Error:', error);
});
client.connect({
channelId: channel_id,
wsToken: ws_token,
websocketUrl: websocket_url,
websocketChannel: websocket_channel,
});
`
`tsx
import { useModelRiver } from '@modelriver/client/react';
function ChatComponent() {
const {
connect,
disconnect,
response,
error,
isConnected,
steps
} = useModelRiver({
baseUrl: 'wss://api.modelriver.com/socket',
persist: true,
});
const handleSend = async () => {
const {
channel_id,
ws_token,
websocket_url,
websocket_channel,
} = await yourBackendAPI.createRequest(message); // calls /api/v1/ai/async
connect({
channelId: channel_id,
wsToken: ws_token,
websocketUrl: websocket_url,
websocketChannel: websocket_channel,
});
};
return (
{JSON.stringify(response.data, null, 2)}{error}
}$3
`vue
{{ step.name }}
{{ response.data }}
{{ error }}
`$3
`typescript
import { Component, OnDestroy } from '@angular/core';
import { ModelRiverService } from '@modelriver/client/angular';@Component({
selector: 'app-chat',
providers: [ModelRiverService],
template:
{{ res.data | json }}
{{ err }}
,
})
export class ChatComponent implements OnDestroy {
constructor(public modelRiver: ModelRiverService) {
this.modelRiver.init({
baseUrl: 'wss://api.modelriver.com/socket'
});
} async send() {
const {
channel_id,
ws_token,
websocket_url,
websocket_channel,
} = await this.backendService.createRequest(message); // calls /api/v1/ai/async
this.modelRiver.connect({
channelId: channel_id,
wsToken: ws_token,
websocketUrl: websocket_url,
websocketChannel: websocket_channel,
});
}
ngOnDestroy() {
this.modelRiver.destroy();
}
}
`$3
`svelte
{#each $steps as step}
{step.name}
{/each}{#if $response}
{JSON.stringify($response.data, null, 2)}
{/if}{#if $error}
{$error}
{/if}
`$3
`html
`API Reference
$3
#### Constructor Options
`typescript
interface ModelRiverClientOptions {
baseUrl?: string; // WebSocket URL (default: 'wss://api.modelriver.com/socket')
apiBaseUrl?: string; // Optional HTTP base URL for backend reconnect (/api/v1/ai/reconnect)
debug?: boolean; // Enable debug logging (default: false)
persist?: boolean; // Enable localStorage persistence (default: true)
storageKeyPrefix?: string; // Storage key prefix (default: 'modelriver_')
heartbeatInterval?: number; // Heartbeat interval in ms (default: 30000)
requestTimeout?: number; // Request timeout in ms (default: 300000)
}
`#### Methods
| Method | Description |
|--------|-------------|
|
connect({ channelId, websocketUrl?, websocketChannel? }) | Connect to WebSocket with channel ID |
| disconnect() | Disconnect from WebSocket |
| reset() | Reset state and clear stored data |
| reconnect() | Reconnect using stored channel ID |
| reconnectWithBackend() | Call your backend /api/v1/ai/reconnect to get a fresh ws_token and reconnect |
| getState() | Get current client state |
| hasPendingRequest() | Check if there's a pending request |
| on(event, callback) | Add event listener (returns unsubscribe function) |
| off(event, callback) | Remove event listener |
| destroy() | Clean up all resources |#### Events
| Event | Payload | Description |
|-------|---------|-------------|
|
connecting | - | Connection attempt started |
| connected | - | Successfully connected |
| disconnected | reason?: string | Disconnected from WebSocket |
| response | AIResponse | AI response received |
| error | Error or string | Error occurred |
| step | WorkflowStep | Workflow step updated |
| channel_joined | - | Successfully joined channel |
| channel_error | reason: string | Channel join failed |$3
`typescript
// Response from /api/ai/async endpoint
interface AsyncResponse {
message: string; // "success"
status: 'pending'; // Always "pending" for async
channel_id: string; // Unique channel ID
ws_token: string; // One-time WebSocket token for authentication
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
}// AI response received via WebSocket
interface AIResponse {
status: string; // "success", "error", "ai_generated", or "completed"
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;
};
// Event-driven workflow fields
ai_response?: {
data?: unknown;
meta?: {
workflow?: string;
status?: string;
duration_ms?: number;
usage?: {
prompt_tokens?: number;
completion_tokens?: number;
total_tokens?: number;
};
};
};
event_name?: string;
task_id?: string;
callback_metadata?: Record;
customer_data?: Record;
}
interface WorkflowStep {
id: string;
name: string;
status: 'pending' | 'loading' | 'success' | 'error';
duration?: number;
errorMessage?: string;
}
`How It Works
1. Your backend calls ModelRiver's
/api/v1/ai/async endpoint
2. ModelRiver returns channel_id, ws_token, websocket_url, and websocket_channel
3. Your backend returns these fields to the frontend (never the API key)
4. Your frontend uses this SDK to connect via WebSocket using channel_id + ws_token
5. AI responses are delivered in real-time to your frontend
6. The SDK handles heartbeats, channel joins, and automatic reconnection for transient network issues.
7. 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. Create request │
│ │─────────────────────>│
│ │ │
│ │ 3. Return channel_id│
│ │<─────────────────────│
│ 4. Return channel_id│ │
│<─────────────────────│ │
│ │ │
│ 5. Connect WebSocket (SDK) │
│─────────────────────────────────────────────>│
│ │ │
│ 6. Stream AI response │
│<─────────────────────────────────────────────│
│ │ │
`Security
The
/api/v1/ai/async response contains:
- channel_id - Unique identifier for this request
- ws_token - Short-lived, one-time WebSocket token (per user + project)
- websocket_url - WebSocket endpoint URL
- websocket_channel - Channel name to joinThe client SDK uses
channel_id and ws_token to connect to the WebSocket.
The ws_token is:- Short-lived (≈5 minutes)
- Single-use (consumed on first successful WebSocket authentication)
For page refresh recovery:
- The SDK persists the active request (by default) to
localStorage
- On reload, you can:
- either call client.reconnect() to reuse the stored ws_token (if still valid)
- or call client.reconnectWithBackend() to have your backend issue a fresh ws_token via /api/v1/ai/reconnectImportant: Always obtain
channel_id and ws_token from your backend.
Never expose your ModelRiver API key in frontend code. Your backend should be the only component that talks to ModelRiver's HTTP API (/api/v1/ai/async, /api/v1/ai/reconnect`, etc.).- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
MIT
- Client SDK Documentation
- API Reference
- Getting Started
- Dashboard
- GitHub Issues