TypeScript AI Streaming
A concise reference for streaming token deltas in real time using modern TypeScript asynchronous iterators and Web Streams.
TL;DR
- Iterate over stream chunks using
for await...ofloops. - Extract incremental text deltas using the
deltaproperty. - Pipe server responses using Web standard
ReadableStreampipelines.
Streaming with Provider SDKs
OpenAI stream completionRequest streaming completion and loop over arriving chunks.
const s = await openai.chat.completions.create({
model: 'gpt-4o', stream: true,
messages: [{ role: 'user', content: 'Hi' }],
});
for await (const c of s) {
const d = c.choices[0]?.delta?.content ?? '';
process.stdout.write(d);
}Anthropic message streamStream assistant messages from Claude with event handlers.
const s = anthropic.messages.stream({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 500,
messages: [{ role: 'user', content: 'Hi' }],
});
s.on('text', t => process.stdout.write(t));Gemini streaming chunksStream responses from Google Gemini models incrementally.
const s = await ai.models.generateContentStream({
model: 'gemini-2.0-flash', contents: 'Hi',
});
for await (const c of s) {
process.stdout.write(c.text ?? '');
}AsyncIterable Protocol
async function*Create reusable generator yielding clean string deltas.
async function* streamText(prompt: string) {
const s = await openai.chat.completions.create({
model: 'gpt-4o-mini', stream: true,
messages: [{ role: 'user', content: prompt }],
});
for await (const c of s) {
yield c.choices[0]?.delta?.content ?? '';
}
}Buffering stream chunksAccumulate streamed fragments while emitting progress.
let text = '';
for await (const d of streamText('Hi')) {
text += d;
}
console.log('Output:', text);Async iterator abortTerminate iteration early using break to clean up.
for await (const c of stream) {
const r = c.choices[0]?.finish_reason;
if (r === 'stop') break;
}Web Streams and Server Responses
new ReadableStream()Convert an AsyncIterable into a standard Web stream.
function toWebStream(iter: AsyncIterable<string>) {
const enc = new TextEncoder();
return new ReadableStream({
async start(controller) {
for await (const chunk of iter) {
controller.enqueue(enc.encode(chunk));
}
controller.close();
},
});
}HTTP streaming responseReturn streaming body with appropriate SSE headers.
export function GET(): Response {
const stream = toWebStream(textStream('Ping'));
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
});
}Stream backpressure controlPause pulling when downstream consumer buffers fill.
const writer = stream.writable.getWriter();
await writer.ready;
await writer.write(encoder.encode('hi'));Stream State and Metadata
stream_options usageRequest token counts inside final stream chunk.
const s = await openai.chat.completions.create({
model: 'gpt-4o', stream: true,
messages: [{ role: 'user', content: 'Hi' }],
stream_options: { include_usage: true },
});
for await (const c of s) {
if (c.usage) {
console.log('Tokens:', c.usage.total_tokens);
}
}Client abort handlerCancel active model stream upon user cancellation.
const abort = new AbortController();
req.signal.addEventListener('abort', () => {
abort.abort();
});Server-Sent Events formatFormat chunks with standard data prefixes for browsers.
function formatSSE(data: string): string {
return `data: ${JSON.stringify({ text: data })}\n\n`;
}Tips
- Use the
for await...ofloop syntax on SDK streaming responses to handle token deltas as soon as they arrive from the network. - Transform provider event streams into standard
ReadableStreaminstances to deliver low-latency responses directly to frontend clients.
Warnings
- Remember that token stream chunks may split multi-byte characters or markdown syntax tokens across separate
deltapacket boundaries. - Never forget to handle stream errors inside a
try...catchblock to close hanging HTTP connections and prevent memory leaks.
In Practice
A streaming API handler that reads token deltas and yields them through a standard Web ReadableStream.
- Request an asynchronous completion stream from the model client.
- Construct a Web ReadableStream to wrap the token iterator.
- Encode each incoming string fragment into UTF-8 Uint8Array bytes.
- Return the resulting stream in a standard HTTP Response object.
import { OpenAI } from 'openai';
export async function handleStream(prompt: string) {
const openai = new OpenAI();
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
const encoder = new TextEncoder();
return new ReadableStream({
async start(controller) {
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) controller.enqueue(encoder.encode(delta));
}
controller.close();
},
});
}FAQ
Set stream: true when calling openai.chat.completions.create. Iterate through the returned AsyncIterable with for await...of and read chunk.choices[0]?.delta?.content.
An AsyncIterable is a JavaScript protocol for sequential pulling with for await...of. A ReadableStream is a Web API designed for byte pipelining, browser transfers, and backpressure control.
With OpenAI, pass stream_options: { include_usage: true } to receive a final chunk containing the usage object. For Anthropic Claude, listen for the message_delta event which reports final output token counts.