ai-sdk-reconnect
v0.3.0
Published
Cursor-aware reconnecting chat transport for Vercel AI SDK
Downloads
318
Maintainers
Readme
ai-sdk-reconnect
Automatic cursor-based reconnects for Vercel AI SDK chat streams.
ReconnectingChatTransport is a drop-in replacement for
DefaultChatTransport. It keeps the same logical UIMessageChunk stream alive
across temporary HTTP or network disconnects, without replaying chunks the
client has already processed.
Requirements
- AI SDK 7
- Node.js 22 or newer
- ESM imports; CommonJS
require()is not supported - A backend that keeps generation running after a client disconnects and can replay the active stream by cursor
Install
npm install ai-sdk-reconnect aiClient setup
import { useChat } from "@ai-sdk/react";
import { ReconnectingChatTransport } from "ai-sdk-reconnect";
const transport = new ReconnectingChatTransport({
api: "/api/chat",
credentials: "include",
});
function Chat({ chatId }: { chatId: string }) {
const chat = useChat({
id: chatId,
resume: true,
transport,
});
return <div>{chat.messages.length} messages</div>;
}The transport accepts the same HTTP options as DefaultChatTransport,
including api, body, credentials, fetch, headers, and
prepareSendMessagesRequest.
Message submissions use POST /api/chat. Reconnects use
GET /api/chat/:chatId/stream by default. To use another endpoint:
const transport = new ReconnectingChatTransport({
api: "/api/chat",
prepareReconnectToStreamRequest({ id }) {
return {
api: `/api/chat/${encodeURIComponent(id)}/stream`,
};
},
});The callback also receives cursor, source, request metadata, headers,
credentials, and body. source is "send" for a stream created by a message
submission and "resume" for a stream opened by resumeStream(). The transport
manages Last-Event-ID; callers do not need to set it.
Backend contract
The reconnect endpoint must provide a durable logical stream:
- Generation continues when an HTTP client disconnects.
- Every SSE event containing an AI SDK JSON chunk has an opaque
id. The ID remains stable when the event is replayed. The standard id-lessdata: [DONE]marker is allowed. - A request without
Last-Event-IDreplays the stream from its initialstartevent. - A request with
Last-Event-IDreturns only the events after that cursor, in their original order. - The endpoint returns
204when no active stream exists. - Terminal
finish,abort, orerrorevents are retained long enough for disconnected clients to read them. - Response caching and proxy buffering are disabled.
Example:
id: cursor-1
data: {"type":"start","messageId":"assistant-1"}
id: cursor-2
data: {"type":"text-start","id":"text-1"}
id: cursor-3
data: {"type":"text-delta","id":"text-1","delta":"Hello"}Page reloads
When using resume: true, do not return an unfinished assistant message in the
initial chat history. Keep its chunks in the durable stream and replay that
stream from start; persist the completed assistant message after finish.
The history response and stream lookup should refer to the same backend run or
snapshot. If a run finishes between those requests, replay it when the loaded
history did not include it, and return 204 when that completed message was
already included.
Reconnect status parts
prepareReconnectDataPart can expose reconnect state as a typed AI SDK data
part. When the callback is absent or returns undefined, no part is added.
import type { UIMessage } from "ai";
import {
ReconnectingChatTransport,
type ReconnectEvent,
} from "ai-sdk-reconnect";
type ChatMessage = UIMessage<
unknown,
{
reconnect: Pick<
ReconnectEvent,
"attempt" | "attemptId" | "maxAttempts" | "state"
>;
}
>;
const transport = new ReconnectingChatTransport<ChatMessage>({
prepareReconnectDataPart(event) {
return {
type: "data-reconnect",
id: "reconnect-status",
data: {
attempt: event.attempt,
attemptId: event.attemptId,
maxAttempts: event.maxAttempts,
state: event.state,
},
};
},
});ReconnectEvent contains:
state:"reconnecting","reconnected", or"failed"attempt: the one-based physical attempt numberattemptId: a unique ID shared by an attempt's reconnecting and terminal eventmaxAttempts: the current maximum, ornullfor unlimited retriessource:"send"or"resume"
Using a constant part id makes AI SDK update one status part. Using
attemptId as the part ID keeps one part per attempt. Reconnect parts are
regular, non-transient message parts and may be rendered, persisted, and sent
to the backend like other data parts.
An attempt emits "failed" before the next attempt starts or before its error
is surfaced. Cancellation stops the reconnect lifecycle without emitting a
terminal reconnect event, so also use the chat status when rendering state.
Retry options
const transport = new ReconnectingChatTransport({
reconnect: {
initialDelayMs: 500,
jitter: 0.2,
maxDelayMs: 10_000,
maxRetries: 8,
multiplier: 2,
visibilityReconnectAfterMs: 30_000,
},
});| Option | Default | Description |
| --- | ---: | --- |
| initialDelayMs | 1000 | Delay before the first retry |
| maxDelayMs | 30000 | Maximum retry delay |
| maxRetries | Infinity | Retry limit for consecutive failures |
| multiplier | 2 | Exponential backoff multiplier |
| jitter | 0.2 | Random delay variation from 0 to 1 |
| visibilityReconnectAfterMs | 30000 | Replace a stale connection after returning to the page; use false to disable |
The transport waits while the browser is offline without consuming retry
budget. It never resubmits the initial POST: when the outcome of that request
is unknown, recovery uses the reconnect GET endpoint.
An initial resumeStream() call made while offline waits until the browser is
online. chat.stop() cannot cancel this initial wait.
Cancellation
For streams started by sendMessage(), chat.stop() immediately stops the
local stream and reconnect loop. It does not stop the durable backend job.
Use a separate backend cancel endpoint to:
- Stop the targeted generation.
- Store an SSE
abortevent with an event ID. - Close the active stream.
For a stream opened through resumeStream(), chat.stop() does not stop the
local resumed stream. The server-side cancel and durable abort event are
required to return the resumed client to ready.
Send the current backend run or stream ID to the cancel endpoint. The server should ignore the request if the chat already points to a newer run.
Errors
The package exports:
ChatStreamHttpError: a non-successful HTTP responseMissingEventIdError: an AI SDK data event did not have a cursorReconnectRetriesExhaustedError: the configured retry limit was reached; itsattemptsproperty contains the number of reconnect requestsResumableStreamUnavailableError: the active stream disappeared before a terminal event
An explicit error response to the initial message POST is surfaced without
resubmitting the message.
