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

llm-sse

v0.5.0

Published

Parse OpenAI Responses and Chat Completions, Anthropic, Gemini and OpenAI-compatible SSE streams into one unified event format. Zero dependencies.

Readme

llm-sse

npm version npm downloads CI OpenSSF Scorecard install size bundle size license zero dependencies

Zero-dependency SSE parser that turns OpenAI Responses, OpenAI Chat Completions, Anthropic, Gemini and OpenAI-compatible streams into one unified event shape: text deltas, reasoning, tool-call fragments and finish reasons, handled the same way regardless of provider.

Security posture is tracked in docs/security-posture.md, including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.

Each API streams differently. OpenAI Responses sends typed response.* events, OpenAI Chat Completions sends choices[].delta chunks, Anthropic sends typed content_block_* / message_* events, and Gemini sends candidates[].content.parts. llm-sse turns all of them into the same small set of events, so your streaming UI or agent loop stays provider-agnostic.

Project

  • Roadmap: compatibility, conformance, and sustainability priorities
  • Contributing: local setup, tests, and pull-request expectations
  • Governance: decision process and the path to reviewer or maintainer
  • Support: where to ask questions or report reproducible bugs
  • Security: private vulnerability reporting and supported versions
  • Changelog: release history and migration notes
  • Code of Conduct: community standards

Quickstart

OpenAI Responses API

import { parseOpenAIResponsesStream } from 'llm-sse';

const res = await fetch('https://api.openai.com/v1/responses', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${key}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    model: process.env.OPENAI_MODEL,
    input: 'What is the weather in Santiago?',
    tools,
    stream: true,
  }),
});

for await (const event of parseOpenAIResponsesStream(res.body)) {
  if (event.type === 'text') process.stdout.write(event.text);
  if (event.type === 'tool_call_delta') {
    process.stderr.write(event.argumentsDelta);
  }
}

The parser follows the official Responses streaming event reference, including output text, reasoning summaries, function-call arguments, refusal text, completion, incomplete responses, and errors. Refusal deltas use the normalized text channel so provider-agnostic UIs do not drop the model's response.

OpenAI Chat Completions

import { parseOpenAIStream } from 'llm-sse';

const res = await fetch('https://api.openai.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${key}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    model: process.env.OPENAI_MODEL,
    messages,
    stream: true,
  }),
});

for await (const event of parseOpenAIStream(res.body)) {
  if (event.type === 'text') process.stdout.write(event.text);
}

Anthropic

import { parseAnthropicStream } from 'llm-sse';

const res = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: {
    'x-api-key': key,
    'anthropic-version': '2023-06-01',
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    model: 'claude-opus-4-8',
    max_tokens: 1024,
    stream: true,
    messages,
  }),
});

for await (const event of parseAnthropicStream(res.body)) {
  if (event.type === 'text') process.stdout.write(event.text);
  if (event.type === 'reasoning') process.stderr.write(event.text); // extended thinking
}

Gemini

import { parseGeminiStream } from 'llm-sse';

// Use the SSE endpoint: streamGenerateContent?alt=sse
const res = await fetch(
  `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=${key}`,
  {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      contents: [{ role: 'user', parts: [{ text: prompt }] }],
    }),
  },
);

for await (const event of parseGeminiStream(res.body)) {
  if (event.type === 'text') process.stdout.write(event.text);
}

OpenAI-compatible providers (Groq, DeepSeek, Ollama, OpenRouter...)

Any provider that speaks the OpenAI chunk format works with parseOpenAIStream. DeepSeek R1 reasoning tokens surface as reasoning events automatically.

import { parseOpenAIStream } from 'llm-sse';

const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${key}`,
    'content-type': 'application/json',
  },
  body: JSON.stringify({
    model: 'llama-3.3-70b-versatile',
    messages,
    stream: true,
  }),
});

for await (const event of parseOpenAIStream(res.body)) {
  if (event.type === 'text') process.stdout.write(event.text);
}

Why

  • One event shape, four streaming formats. text, reasoning, tool_call_start, tool_call_delta, finish, and error, whether the bytes came from OpenAI Responses, OpenAI Chat Completions, Anthropic, or Gemini.
  • Tool calls just accumulate. Streamed JSON argument fragments carry an index; concatenate by index (or let collectStream do it) to get the full call.
  • Correct SSE framing. Robust to chunk boundaries splitting a line or event mid-way, CRLF, multi-line data: fields, comments and keep-alives.
  • Fixture-backed provider coverage. Public OpenAI, Anthropic and Gemini .sse fixtures exercise text, reasoning, tool-call arguments and finish reasons.
  • Bytes or strings. Feed it a fetch() ReadableStream<Uint8Array>, a Node stream, or an async iterable of strings. Multibyte UTF-8 split across chunks is handled.
  • Zero dependencies, ESM + CJS, fully typed.

| Streaming source | Parser | Text | Reasoning | Function calls | Finish and errors | | ------------------------------ | ---------------------------- | :--: | :---------------: | :------------: | :---------------: | | OpenAI Responses API | parseOpenAIResponsesStream | ✓ | ✓ | ✓ | ✓ | | OpenAI Chat Completions | parseOpenAIStream | ✓ | compatible fields | ✓ | ✓ | | Anthropic Messages | parseAnthropicStream | ✓ | ✓ | ✓ | ✓ | | Gemini streamGenerateContent | parseGeminiStream | ✓ | ✓ | ✓ | ✓ |

Why not the provider SDK?

The official SDKs each ship their own streaming abstraction, so combining APIs means learning multiple event models and writing multiple adapter paths in your agent or UI. llm-sse is a thin, zero-dependency layer that normalizes the wire format only. You keep your own fetch, retry, and authentication logic. If you use one provider exclusively, its SDK helpers may be sufficient; this library is most useful when you need provider portability, a minimal footprint, or control over the HTTP layer.

Install

npm install llm-sse

API

parseOpenAIResponsesStream(source)

Parses typed OpenAI Responses API SSE events. It maps response.output_text.delta, reasoning deltas, function-call item and argument events, and terminal response states into normalized events. The Responses output_index is used as the normalized tool-call index.

parseOpenAIStream(source) · parseAnthropicStream(source) · parseGeminiStream(source)

Each takes a source (AsyncIterable<Uint8Array | string>, fetch().body satisfies this) and returns an AsyncGenerator<StreamEvent>.

Gemini: use the SSE form of the streaming endpoint (streamGenerateContent?alt=sse).

OpenAI-compatible providers (Groq, DeepSeek, OpenRouter, Together, Fireworks, Ollama, ...) emit the same chunk format; use parseOpenAIStream for them. Reasoning models that stream reasoning_content (e.g. DeepSeek R1) surface as reasoning events.

parseStream(source, provider)

Same thing, dispatching on provider ('openai-responses' | 'openai' | 'anthropic' | 'gemini').

StreamEvent

type StreamEvent =
  | { type: 'text'; text: string }
  | { type: 'reasoning'; text: string } // extended thinking, kept apart from text
  | { type: 'tool_call_start'; index: number; id?: string; name?: string }
  | { type: 'tool_call_delta'; index: number; argumentsDelta: string }
  | { type: 'finish'; reason?: string }
  | { type: 'error'; error: unknown };

reasoning carries OpenAI reasoning text or summaries, Anthropic extended thinking, and Gemini thought parts separately from text, so you can render it in its own affordance or drop it.

collectStream(events)

Drains an event stream into a single message:

const { text, reasoning, toolCalls, finishReason } = await collectStream(
  parseAnthropicStream(res.body),
);
// toolCalls: { index, id?, name?, arguments }[], arguments is the joined JSON string

toAssistantMessage(collected)

Turn a collected stream into a standard OpenAI-shape assistant message, the format llm-messages treats as canonical, so a streamed response composes straight back into your history or into a different provider:

import { collectStream, toAssistantMessage } from 'llm-sse';
import { toAnthropic } from 'llm-messages';

const message = toAssistantMessage(
  await collectStream(parseOpenAIStream(res.body)),
);
const claudeBody = toAnthropic([...history, message]); // continue on Claude

sseData(source)

The underlying SSE parser, exported for advanced use: yields the data payload of each event as a string.

Recipes

Consume a Node.js http / https response body

Node IncomingMessage is an async iterable of Buffer, which satisfies AsyncIterable<Uint8Array>:

import https from 'node:https';
import { parseOpenAIStream } from 'llm-sse';

function streamCompletion(options: https.RequestOptions, body: string) {
  return new Promise<void>((resolve, reject) => {
    const req = https.request(options, async (res) => {
      for await (const event of parseOpenAIStream(res)) {
        if (event.type === 'text') process.stdout.write(event.text);
      }
      resolve();
    });
    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

Agent tool-call loop

import { parseOpenAIStream, collectStream, toAssistantMessage } from 'llm-sse';

async function agentLoop(messages: unknown[]) {
  while (true) {
    const res = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${key}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify({ model: 'gpt-4o', messages, tools, stream: true }),
    });

    const collected = await collectStream(parseOpenAIStream(res.body));
    messages.push(toAssistantMessage(collected));

    if (!collected.toolCalls.length) {
      console.log(collected.text);
      break;
    }

    for (const call of collected.toolCalls) {
      const result = await dispatch(call.name, JSON.parse(call.arguments));
      messages.push({
        role: 'tool',
        tool_call_id: call.id,
        content: JSON.stringify(result),
      });
    }
  }
}

Provider-agnostic wrapper

import { parseStream, collectStream, type Provider } from 'llm-sse';

async function complete(provider: Provider, fetchBody: Response) {
  return collectStream(parseStream(fetchBody.body, provider));
}

// same call site for any provider
const result = await complete('anthropic', anthropicResponse);
const result2 = await complete('openai', openaiResponse);

Caveats

  • Non-JSON data: payloads are treated as keep-alives and skipped by provider parsers.
  • JSON-looking malformed payloads surface as error events and parsing continues with later events.
  • Provider parsers ignore SSE event: names and key off the JSON data: payload shape.
  • Malformed provider event shapes are skipped when they cannot produce a valid normalized event.
  • OpenAI chunks with multiple choices are emitted in provider order, but normalized events do not carry a choice index.

Tool calls

All supported APIs are normalized to the same pattern: a tool_call_start (with index, and id / name when available) followed by one or more tool_call_deltas whose argumentsDelta strings concatenate into the call's JSON arguments. OpenAI Responses uses its stable output_index; Chat Completions and Anthropic stream argument fragments; Gemini sends arguments whole in a single delta. collectStream joins them for you.

Fixture corpus

The package includes a small public fixture corpus under fixtures/:

  • openai-weather-tool.sse
  • openai-responses-weather-tool.sse
  • anthropic-weather-tool.sse
  • gemini-weather-tool.sse
  • expected normalized events and collected messages under fixtures/expected/

Each fixture describes the same semantic turn: reasoning, visible text, a get_weather tool call, JSON arguments and provider-specific finish reason. The tests parse the fixtures directly, including byte-split stream boundaries, so contributors can change parsers with a stable cross-provider contract.

Community

Contributions are welcome, especially sanitized provider fixtures, reproducible edge cases, interoperability examples, and review help. Start with CONTRIBUTING.md, use Discussions for design questions, and see GOVERNANCE.md for how sustained contributors can become reviewers or maintainers.

Related

  • json-from-llm: extract valid JSON from an LLM response, even inside reasoning tags, fenced blocks or prose
  • tool-schema: convert a JSON Schema into a provider tool / function-calling schema for OpenAI, Anthropic, Gemini and MCP
  • llm-messages: convert chat messages between OpenAI, Anthropic and Gemini formats
  • llm-errors: normalize provider errors (rate limits, retries, status) into one shape

License

MIT © Sebastian Legarraga