Skip to main content

Documentation Index

Fetch the complete documentation index at: https://s2.dev/docs/llms.txt

Use this file to discover all available pages before exploring further.

S2 integrates with the Anthropic Messages API through the @s2-dev/resumable-stream/anthropic entrypoint. The integration includes two helpers:
  • The createResumableChat server helper stores raw Anthropic stream events in S2 streams and replays them as Anthropic-shaped SSE.
  • The subscribe browser helper is a small async generator that reads the replay endpoint.
export S2_ACCESS_TOKEN="..."
export S2_BASIN="my-basin"
export ANTHROPIC_API_KEY="..."

Server Setup

lib/s2.ts
import { createResumableChat } from '@s2-dev/resumable-stream/anthropic';

export const chat = createResumableChat({
  accessToken: process.env.S2_ACCESS_TOKEN!,
  basin: process.env.S2_BASIN!,
  mode: 'session',
});
mode: 'session' can be useful when you want your chat app to have one S2 stream to hold a long-lived log for a chat. Use single-use if each response needs its own stream.

Start A Turn

app/api/chat/route.ts
import Anthropic from '@anthropic-ai/sdk';
import type { MessageParam } from '@anthropic-ai/sdk/resources/messages';
import { chat } from '@/lib/s2';

const anthropic = new Anthropic({
  apiKey: process.env.ANTHROPIC_API_KEY!,
});

export async function POST(req: Request) {
  const { id, messages } = (await req.json()) as {
    id: string;
    messages: MessageParam[];
  };

  const source = anthropic.messages.stream({
    model: process.env.ANTHROPIC_MODEL ?? 'claude-haiku-4-5-20251001',
    max_tokens: 1024,
    messages,
  });

  return chat.makeResumable(`chat-${id}`, source, {
    delivery: 'replay',
    waitUntil: (p) => p.catch(console.error),
  });
}
delivery: 'replay' makes the POST return 202. The client reads the actual events from the replay route.

Replay Route

app/api/chat/stream/route.ts
import { chat } from '@/lib/s2';

function parseFromSeqNum(value: string | null): number | undefined {
  if (value === null) return undefined;
  const parsed = Number.parseInt(value, 10);
  return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : undefined;
}

export async function GET(req: Request) {
  const url = new URL(req.url);
  const id = url.searchParams.get('id');
  if (!id) return new Response('Missing id query parameter', { status: 400 });

  return chat.replay(`chat-${id}`, {
    fromSeqNum: parseFromSeqNum(url.searchParams.get('from')),
    live: url.searchParams.get('live') === '1',
  });
}
Use live: true in session mode when the browser should stay connected at the tail and receive future turns.

Browser Subscription

import { subscribe } from '@s2-dev/resumable-stream/anthropic/client';

await fetch('/api/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: chatId, messages }),
});

for await (const event of subscribe({
  url: (cursor) => {
    const params = new URLSearchParams({ id: chatId, live: '1' });
    if (cursor !== undefined) params.set('from', String(cursor));
    return `/api/chat/stream?${params}`;
  },
})) {
  switch (event.type) {
    case 'message_start':
      break;
    case 'content_block_delta':
      break;
    case 'message_stop':
      break;
  }
}
subscribe yields Anthropic RawMessageStreamEvent values, tracks the replay cursor from SSE id: values, and reconnects from that cursor if the response drops. It can also yield the adapter’s error envelope:
{ type: 'error', error: { type: string, message: string } }

Completed History

The Anthropic helper does not infer user messages and does not build a transcript for you. You can pair the replay stream with a separate history store:
  1. Append the user message to history before starting the model.
  2. Fold Anthropic stream events into a completed assistant message.
  3. Append the assistant message when message_stop arrives.
  4. On page load, render history immediately, then subscribe to replay for any active turn.

Options

createResumableChat accepts:
optiondefaultdescription
mode"single-use""single-use" uses one stream per generation. "shared" reuses one active-generation stream. "session" appends generations to one durable stream.
endpointsS2 defaultsOptional endpoint overrides, commonly used with S2 Lite.
batchSize10Maximum number of events per append batch.
lingerDuration50Maximum batching delay in milliseconds.
leaseDurationMs5000shared mode takeover window for stale active generations.
onErrorgeneric messageMaps upstream errors to an Anthropic-shaped error event.
replay accepts:
optiondescription
fromSeqNumS2 cursor to resume from. The client tracks this from SSE id: values.
liveSession mode only. Keeps the SSE open at the tail for future records.
subscribe accepts:
optiondescription
urlReplay URL. String URLs get ?from=<cursor> appended on reconnect; function URLs receive the cursor.
signalOptional abort signal.
fetchOptional fetch implementation override.
headersStatic or lazy headers sent on every request.
credentialsFetch credentials mode. Defaults to same-origin.
reconnectBackoffMsMillisecond backoff schedule. Pass [] to disable reconnect.

Example

A complete Bun server and browser client is available here: examples/anthropic-resumable-chat.