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

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

    Stream 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 chunks

    Stream 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 chunks

    Accumulate streamed fragments while emitting progress.

    let text = '';
    for await (const d of streamText('Hi')) {
      text += d;
    }
    console.log('Output:', text);
    Async iterator abort

    Terminate 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 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