Docs

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.

Current section

Client SDK

Updated

This week

Build time

Minutes, not hours

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

  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:

// 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

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:

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:

  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:

// 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 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):

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:

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:

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