TypeScript AI Streaming

A concise reference for streaming token deltas in real time using modern TypeScript asynchronous iterators and Web Streams.

TL;DR

  1. Iterate over stream chunks using for await...of loops.
  2. Extract incremental text deltas using the delta property.
  3. Pipe server responses using Web standard ReadableStream pipelines.

Streaming with Provider SDKs

    OpenAI stream completion

    Loop over deltas with for await.

    const s = await openai.chat.completions.create({
      model: 'gpt-4o', stream: true,
      messages: [{ role: 'user', content: 'Hi' }],
    });
    for await (const c of s) {
      process.stdout.write(c.choices[0]?.delta?.content ?? '');
    }
    Anthropic message stream

    Claude streams via 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 chunks

    Gemini uses generateContentStream.

    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*

    A reusable generator yielding 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 chunks

    Collect the full response.

    let text = '';
    for await (const d of streamText('Hi')) text += d;
    Async iterator abort

    break ends the loop early.

    for await (const c of stream) {
      if (c.choices[0]?.finish_reason === '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 response

    Return 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 control

    Pause 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 usage

    Request 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 handler

    Cancel active model stream upon user cancellation.

    const abort = new AbortController();
    req.signal.addEventListener('abort', () => {
      abort.abort();
    });
    Server-Sent Events format

    Format chunks with standard data prefixes for browsers.

    function formatSSE(data: string): string {
      return `data: ${JSON.stringify({ text: data })}\n\n`;
    }

Tips

  1. Use the for await...of loop syntax on SDK streaming responses to handle token deltas as soon as they arrive from the network.
  2. Transform provider event streams into standard ReadableStream instances to deliver low-latency responses directly to frontend clients.

Warnings

  1. Remember that token stream chunks may split multi-byte characters or markdown syntax tokens across separate delta packet boundaries.
  2. Never forget to handle stream errors inside a try...catch block to close hanging HTTP connections and prevent memory leaks.

In Practice

FAQ