Streaming & Protocol
AI responses stream over WebSocket using a typed message protocol. The client hooks handle parsing and state management automatically, but understanding the protocol is useful for custom integrations.
#Client Messages
Client → Server
| Type | Purpose | Key Fields |
|---|---|---|
message | Send a message with optional file attachments | content, attachments? |
cancel | Stop a streaming response | conversationId |
branch | Fork conversation from a specific message | messageId, content |
TS
// Client → Server messages (sent over WebSocket)
type AIMessageRequest = {
type: 'message';
content: string;
conversationId?: string;
attachments?: AIAttachment[];
};
type AICancelRequest = {
type: 'cancel';
conversationId: string;
};
type AIBranchRequest = {
type: 'branch';
messageId: string;
content: string;
conversationId: string;
};#Server Messages
Server → Client
chunkStreamed text content fragment
thinkingExtended reasoning token stream
tool_statusTool execution running or complete with result
imageGenerated image data (may be partial during streaming)
completeResponse finished with model name, duration, and token usage
errorError message from the provider or plugin
TS
// Server → Client messages (received over WebSocket)
type AIChunkMessage = {
type: 'chunk';
content: string;
messageId: string;
conversationId: string;
};
type AIThinkingMessage = {
type: 'thinking';
content: string;
messageId: string;
conversationId: string;
};
type AIToolStatusMessage = {
type: 'tool_status';
name: string;
status: 'running' | 'complete';
input?: unknown;
result?: string;
messageId: string;
conversationId: string;
};
type AIImageMessage = {
type: 'image';
data: string;
format: string;
isPartial: boolean;
revisedPrompt?: string;
imageId?: string;
messageId: string;
conversationId: string;
};
type AICompleteMessage = {
type: 'complete';
durationMs?: number;
messageId: string;
model?: string;
conversationId: string;
usage?: AIUsage;
};
type AIErrorMessage = {
type: 'error';
message: string;
messageId?: string;
conversationId?: string;
};#AIMessage Type
On the client, streamed data accumulates into AIMessage objects. This is the shape returned by all framework hooks in the messages array.
TS
type AIMessage = {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
conversationId: string;
parentId?: string;
attachments?: AIAttachment[];
thinking?: string;
toolCalls?: AIToolCall[];
images?: AIImageData[];
isStreaming?: boolean;
model?: string;
usage?: AIUsage;
durationMs?: number;
timestamp: number;
};
type AIUsage = {
inputTokens: number;
outputTokens: number;
};
type AIAttachment = {
data: string; // base64-encoded
media_type:
| 'image/png'
| 'image/jpeg'
| 'image/gif'
| 'image/webp'
| 'application/pdf';
name?: string;
};#Extended Thinking
Reasoning models can stream their thought process alongside the final response. Enable it with a token budget. Thinking content arrives as thinking messages and is available on AIMessage.thinking.
| Model | Provider |
|---|---|
| Claude Opus 4.6 | Anthropic |
| Claude Sonnet 4.6 | Anthropic |
| o3 | OpenAI |
| o4-mini | OpenAI |
| DeepSeek Reasoner | DeepSeek |
Enable statically for all requests:
TS
new Elysia().use(
aiChat({
provider: getProvider,
thinking: { budgetTokens: 10000 },
})
);Or dynamically per model:
TS
new Elysia().use(
aiChat({
provider: getProvider,
thinking: (providerName, model) => {
const THINKING_MODELS = new Set([
'claude-opus-4-6',
'claude-sonnet-4-6',
'o3',
'o4-mini',
'deepseek-reasoner',
]);
return THINKING_MODELS.has(model)
? { budgetTokens: 8000 }
: undefined;
},
})
);#Connection Management
The client hooks manage the WebSocket connection lifecycle automatically. You get these behaviors out of the box:
Auto-Reconnect
Exponential backoff on disconnect
Keep-Alive
Ping interval every 30s by default
Max Retries
Up to 60 reconnect attempts
Type Validation
All messages validated with type guards
Cleanup
Graceful teardown on component unmount
Backpressure
1MB threshold for stream buffering
TS
type AIConnectionOptions = {
protocols?: string[];
reconnect?: boolean;
pingInterval?: number;
maxReconnectAttempts?: number;
};