npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@tamerxz/llm-stream

v0.1.0

Published

Zero-dependency, provider-agnostic SSE parser for LLM streaming responses. Browser + Node.js. TypeScript-first. Under 3KB minified+gzipped.

Downloads

41

Readme

llm-stream

npm version bundle size license

Zero-dependency, provider-agnostic SSE parser for LLM streaming responses. Browser + Node.js. TypeScript-first. Under 3KB minified+gzipped.

llm-stream turns the raw SSE byte stream from any LLM provider into a unified, typed event stream — text deltas, tool calls, thinking blocks, finish reasons, usage — without bundling an HTTP client or framework.

Why

Every developer writes the same boilerplate to consume LLM streams:

| Step | Raw fetch | llm-stream | |---|---|---| | Open stream | ~5 lines | ✨ already done | | Decode UTF-8 | ~3 lines | ✨ | | Split on data: | ~10 lines | ✨ | | Parse JSON | ~3 lines | ✨ | | Extract deltas | ~15 lines | ✨ | | Handle tool calls | ~20 lines | ✨ | | Handle each provider differently | ×N | one API | | Total | ~60 lines/provider | 3 lines |

Existing options are either heavy (Vercel AI SDK ≈ 50KB+ with framework deps), generic (eventsource-parser doesn't know LLM semantics), or single-provider.

Installation

npm i @tamerxz/llm-stream

No dependencies. Works in Node.js 18+, Deno, Bun, Cloudflare Workers, and modern browsers.

Quickstart

OpenAI

import { parseStream } from "@tamerxz/llm-stream";

const response = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: JSON.stringify({
    model: "gpt-4o",
    stream: true,
    messages: [{ role: "user", content: "Hello!" }],
  }),
});

for await (const event of parseStream(response, { provider: "openai" })) {
  if (event.type === "text") process.stdout.write(event.delta);
  else if (event.type === "done") console.log("\n", event.usage);
}

Anthropic

import { parseStream } from "@tamerxz/llm-stream";

const response = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.ANTHROPIC_API_KEY!,
    "anthropic-version": "2023-06-01",
  },
  body: JSON.stringify({
    model: "claude-opus-4-7",
    max_tokens: 1024,
    stream: true,
    messages: [{ role: "user", content: "Hello!" }],
  }),
});

for await (const event of parseStream(response, { provider: "anthropic" })) {
  if (event.type === "text") process.stdout.write(event.delta);
  else if (event.type === "thinking") console.error("[thinking]", event.delta);
  else if (event.type === "tool_use_end") console.log("tool:", event.input);
  else if (event.type === "done") console.log("\n", event.reason, event.usage);
}

Auto-detect provider

for await (const event of parseStream(response, { provider: "auto" })) {
  // library inspects the first chunk and picks the right parser
}

Callback style

await parseStream(response, {
  provider: "openai",
  onText: ({ delta }) => process.stdout.write(delta),
  onToolUse: (event) => {
    if (event.type === "tool_use_end") console.log("tool:", event.input);
  },
  onDone: ({ reason, usage }) => console.log(reason, usage),
});

When any callback is provided, parseStream returns a thenable that resolves on stream completion. Mix both styles as you see fit.

Abort

const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);

for await (const event of parseStream(response, {
  provider: "openai",
  signal: controller.signal,
})) {
  // clean done event with reason: 'error' on abort
}

Event types

All events are members of a discriminated union with a literal type field.

type StreamEvent =
  | { type: "text"; delta: string; cumulative: string }
  | { type: "tool_use_start"; id: string; name: string }
  | { type: "tool_use_delta"; id: string; delta: string }
  | { type: "tool_use_end"; id: string; input: unknown }
  | { type: "thinking"; delta: string; cumulative: string }
  | { type: "citation"; text: string; source: string }
  | { type: "error"; error: Error; recoverable: boolean }
  | {
      type: "done";
      reason: "stop" | "length" | "tool_use" | "content_filter" | "error";
      usage?: { input_tokens: number; output_tokens: number };
    };

The cumulative field on text / thinking is computed by the library — you don't need to maintain your own buffer.

tool_use_end.input is the fully-parsed JSON for the tool call. The library accumulates tool_use_delta fragments per tool-call id and parses them when the call completes. If parsing fails, you get a recoverable error event and iteration continues.

Provider support

| Provider | Status (0.1.0) | Text | Tool use | Thinking | Citations | |-----------|----------------|------|----------|----------|-----------| | OpenAI | ✅ shipped | ✅ | ✅ | ✅ (o1) | — | | Anthropic | ✅ shipped | ✅ | ✅ | ✅ | ✅ | | Google | planned 0.2.0 | — | — | — | — | | Mistral | planned 0.3.0 | — | — | — | — | | Cohere | planned 0.3.0 | — | — | — | — | | xAI | planned 0.3.0 | — | — | — | — |

Auto-detection in 0.1.0 picks between OpenAI and Anthropic based on the first chunk.

Bundle size

npm run size

The published ESM bundle is under 3KB minified + gzipped. Verified in CI; the build fails if it regresses past the limit.

Error handling

  • Network or runtime errors propagate as error events with recoverable: false, followed by a done event with reason: "error". Iteration ends.
  • Malformed JSON inside an SSE payload emits an error event with recoverable: true. Iteration continues.
  • Aborting the AbortController ends iteration with { type: "done", reason: "error" }.

The library never throws synchronously from the iterator — every failure flows through events.

License

MIT — see LICENSE.

Contributing

PRs welcome. See CONTRIBUTING.md for guidelines on adding providers and capturing fixtures.