llm-sse 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +170 -11
  3. package/package.json +3 -5
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.8] - 2026-06-29
10
+
11
+ ### Changed
12
+
13
+ - Documentation: add provider-SDK recipes (OpenAI, Anthropic, Vercel AI SDK), a "why not alternatives" section, complete cross-links to the sibling packages, and install / bundle-size badges. No code changes.
14
+
15
+ ## [0.4.7] - 2026-06-29
16
+
17
+ ### Fixed
18
+
19
+ - The npm package page showed a broken "resource not found" downloads badge after the self-hosted badge JSON was removed. The README now uses shields.io's native `npm/dm` badge, which renders the live download count directly on npm.
20
+
9
21
  ## [0.4.6] - 2026-06-12
10
22
 
11
23
  ### Fixed
package/README.md CHANGED
@@ -1,21 +1,27 @@
1
1
  # llm-sse
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/llm-sse.svg)](https://www.npmjs.com/package/llm-sse)
4
- [![npm downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fslegarraga%2Fllm-sse%2Fmain%2Fbadges%2Fnpm-downloads%2Fllm-sse.json)](https://www.npmjs.com/package/llm-sse)
4
+ [![npm downloads](https://img.shields.io/npm/dm/llm-sse?logo=npm&label=downloads)](https://www.npmjs.com/package/llm-sse)
5
5
  [![CI](https://github.com/slegarraga/llm-sse/actions/workflows/ci.yml/badge.svg)](https://github.com/slegarraga/llm-sse/actions/workflows/ci.yml)
6
6
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/slegarraga/llm-sse/badge)](https://scorecard.dev/viewer/?uri=github.com/slegarraga/llm-sse)
7
+ [![install size](https://packagephobia.com/badge?p=llm-sse)](https://packagephobia.com/result?p=llm-sse)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/llm-sse?label=min%2Bgzip)](https://bundlephobia.com/package/llm-sse)
7
9
  [![license](https://img.shields.io/npm/l/llm-sse.svg)](./LICENSE)
8
10
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](./package.json)
9
11
 
10
- > Parse streaming responses from OpenAI, Anthropic, Gemini and OpenAI-compatible providers into one unified event format. Text, reasoning and tool-call deltas, handled. **Zero dependencies.**
12
+ > Zero-dependency SSE parser that turns OpenAI, Anthropic, Gemini and any OpenAI-compatible stream into one unified event shape: text deltas, reasoning, tool-call fragments and finish reasons, handled the same way regardless of provider.
11
13
 
12
- Security posture is tracked in [docs/security-posture.md](./docs/security-posture.md),
14
+ Security posture is tracked in [docs/security-posture.md](https://github.com/slegarraga/llm-sse/blob/main/docs/security-posture.md),
13
15
  including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.
14
16
 
15
17
  Each provider streams differently. OpenAI sends `choices[].delta` chunks, Anthropic sends typed `content_block_*` / `message_*` events, Gemini sends `candidates[].content.parts` — and the SSE framing, tool-call argument fragments and stop reasons are all shaped differently. `llm-sse` turns any of them into the same small set of events, so your streaming UI or agent loop stays provider-agnostic.
16
18
 
19
+ ## Quickstart
20
+
21
+ ### OpenAI
22
+
17
23
  ```ts
18
- import { parseOpenAIStream, collectStream } from 'llm-sse';
24
+ import { parseOpenAIStream } from 'llm-sse';
19
25
 
20
26
  const res = await fetch('https://api.openai.com/v1/chat/completions', {
21
27
  method: 'POST',
@@ -23,7 +29,80 @@ const res = await fetch('https://api.openai.com/v1/chat/completions', {
23
29
  Authorization: `Bearer ${key}`,
24
30
  'content-type': 'application/json',
25
31
  },
26
- body: JSON.stringify({ model, messages, stream: true }),
32
+ body: JSON.stringify({ model: 'gpt-4o', messages, stream: true }),
33
+ });
34
+
35
+ for await (const event of parseOpenAIStream(res.body)) {
36
+ if (event.type === 'text') process.stdout.write(event.text);
37
+ }
38
+ ```
39
+
40
+ ### Anthropic
41
+
42
+ ```ts
43
+ import { parseAnthropicStream } from 'llm-sse';
44
+
45
+ const res = await fetch('https://api.anthropic.com/v1/messages', {
46
+ method: 'POST',
47
+ headers: {
48
+ 'x-api-key': key,
49
+ 'anthropic-version': '2023-06-01',
50
+ 'content-type': 'application/json',
51
+ },
52
+ body: JSON.stringify({
53
+ model: 'claude-opus-4-8',
54
+ max_tokens: 1024,
55
+ stream: true,
56
+ messages,
57
+ }),
58
+ });
59
+
60
+ for await (const event of parseAnthropicStream(res.body)) {
61
+ if (event.type === 'text') process.stdout.write(event.text);
62
+ if (event.type === 'reasoning') process.stderr.write(event.text); // extended thinking
63
+ }
64
+ ```
65
+
66
+ ### Gemini
67
+
68
+ ```ts
69
+ import { parseGeminiStream } from 'llm-sse';
70
+
71
+ // Use the SSE endpoint: streamGenerateContent?alt=sse
72
+ const res = await fetch(
73
+ `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key=${key}`,
74
+ {
75
+ method: 'POST',
76
+ headers: { 'content-type': 'application/json' },
77
+ body: JSON.stringify({
78
+ contents: [{ role: 'user', parts: [{ text: prompt }] }],
79
+ }),
80
+ },
81
+ );
82
+
83
+ for await (const event of parseGeminiStream(res.body)) {
84
+ if (event.type === 'text') process.stdout.write(event.text);
85
+ }
86
+ ```
87
+
88
+ ### OpenAI-compatible providers (Groq, DeepSeek, Ollama, OpenRouter...)
89
+
90
+ Any provider that speaks the OpenAI chunk format works with `parseOpenAIStream`. DeepSeek R1 reasoning tokens surface as `reasoning` events automatically.
91
+
92
+ ```ts
93
+ import { parseOpenAIStream } from 'llm-sse';
94
+
95
+ const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
96
+ method: 'POST',
97
+ headers: {
98
+ Authorization: `Bearer ${key}`,
99
+ 'content-type': 'application/json',
100
+ },
101
+ body: JSON.stringify({
102
+ model: 'llama-3.3-70b-versatile',
103
+ messages,
104
+ stream: true,
105
+ }),
27
106
  });
28
107
 
29
108
  for await (const event of parseOpenAIStream(res.body)) {
@@ -40,6 +119,10 @@ for await (const event of parseOpenAIStream(res.body)) {
40
119
  - **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.
41
120
  - **Zero dependencies**, ESM + CJS, fully typed.
42
121
 
122
+ ## Why not the provider SDK?
123
+
124
+ The official SDKs (openai, @anthropic-ai/sdk, @google/generative-ai) each ship their own streaming abstraction, so combining two providers means learning two APIs and writing two 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/auth logic, and every provider becomes the same three event types. If you are already using a provider SDK exclusively and do not plan to switch, the SDK's streaming helpers may be sufficient; this library is most useful when you need provider portability, a minimal footprint, or control over the HTTP layer.
125
+
43
126
  ## Install
44
127
 
45
128
  ```sh
@@ -54,7 +137,7 @@ Each takes a `source` (`AsyncIterable<Uint8Array | string>` — `fetch().body` s
54
137
 
55
138
  > Gemini: use the SSE form of the streaming endpoint (`streamGenerateContent?alt=sse`).
56
139
 
57
- > **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.
140
+ > **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.
58
141
 
59
142
  ### `parseStream(source, provider)`
60
143
 
@@ -72,7 +155,7 @@ type StreamEvent =
72
155
  | { type: 'error'; error: unknown };
73
156
  ```
74
157
 
75
- > `reasoning` carries the model's thinking Anthropic extended thinking (`thinking_delta`) and Gemini `thought` parts separately from `text`, so you can render it in its own affordance or drop it.
158
+ > `reasoning` carries the model's thinking (Anthropic extended thinking `thinking_delta` and Gemini `thought` parts) separately from `text`, so you can render it in its own affordance or drop it.
76
159
 
77
160
  ### `collectStream(events)`
78
161
 
@@ -103,6 +186,81 @@ const claudeBody = toAnthropic([...history, message]); // continue on Claude
103
186
 
104
187
  The underlying SSE parser, exported for advanced use: yields the `data` payload of each event as a string.
105
188
 
189
+ ## Recipes
190
+
191
+ ### Consume a Node.js `http` / `https` response body
192
+
193
+ Node `IncomingMessage` is an async iterable of `Buffer`, which satisfies `AsyncIterable<Uint8Array>`:
194
+
195
+ ```ts
196
+ import https from 'node:https';
197
+ import { parseOpenAIStream } from 'llm-sse';
198
+
199
+ function streamCompletion(options: https.RequestOptions, body: string) {
200
+ return new Promise<void>((resolve, reject) => {
201
+ const req = https.request(options, async (res) => {
202
+ for await (const event of parseOpenAIStream(res)) {
203
+ if (event.type === 'text') process.stdout.write(event.text);
204
+ }
205
+ resolve();
206
+ });
207
+ req.on('error', reject);
208
+ req.write(body);
209
+ req.end();
210
+ });
211
+ }
212
+ ```
213
+
214
+ ### Agent tool-call loop
215
+
216
+ ```ts
217
+ import { parseOpenAIStream, collectStream, toAssistantMessage } from 'llm-sse';
218
+
219
+ async function agentLoop(messages: unknown[]) {
220
+ while (true) {
221
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
222
+ method: 'POST',
223
+ headers: {
224
+ Authorization: `Bearer ${key}`,
225
+ 'content-type': 'application/json',
226
+ },
227
+ body: JSON.stringify({ model: 'gpt-4o', messages, tools, stream: true }),
228
+ });
229
+
230
+ const collected = await collectStream(parseOpenAIStream(res.body));
231
+ messages.push(toAssistantMessage(collected));
232
+
233
+ if (!collected.toolCalls.length) {
234
+ console.log(collected.text);
235
+ break;
236
+ }
237
+
238
+ for (const call of collected.toolCalls) {
239
+ const result = await dispatch(call.name, JSON.parse(call.arguments));
240
+ messages.push({
241
+ role: 'tool',
242
+ tool_call_id: call.id,
243
+ content: JSON.stringify(result),
244
+ });
245
+ }
246
+ }
247
+ }
248
+ ```
249
+
250
+ ### Provider-agnostic wrapper
251
+
252
+ ```ts
253
+ import { parseStream, collectStream, type Provider } from 'llm-sse';
254
+
255
+ async function complete(provider: Provider, fetchBody: Response) {
256
+ return collectStream(parseStream(fetchBody.body, provider));
257
+ }
258
+
259
+ // same call site for any provider
260
+ const result = await complete('anthropic', anthropicResponse);
261
+ const result2 = await complete('openai', openaiResponse);
262
+ ```
263
+
106
264
  ## Caveats
107
265
 
108
266
  - Non-JSON `data:` payloads are treated as keep-alives and skipped by provider parsers.
@@ -117,7 +275,7 @@ All three providers are normalized to the same pattern: a `tool_call_start` (wit
117
275
 
118
276
  ## Fixture corpus
119
277
 
120
- The package includes a small public fixture corpus under [`fixtures/`](./fixtures):
278
+ The package includes a small public fixture corpus under [`fixtures/`](https://github.com/slegarraga/llm-sse/tree/main/fixtures):
121
279
 
122
280
  - `openai-weather-tool.sse`
123
281
  - `anthropic-weather-tool.sse`
@@ -131,9 +289,10 @@ so contributors can change parsers with a stable cross-provider contract.
131
289
 
132
290
  ## Related
133
291
 
134
- - [`tool-schema`](https://www.npmjs.com/package/tool-schema) convert a JSON Schema into OpenAI / Anthropic / Gemini / MCP tool schemas.
135
- - [`llm-messages`](https://www.npmjs.com/package/llm-messages) convert conversations and responses between providers.
136
- - [`llm-errors`](https://www.npmjs.com/package/llm-errors) normalize provider errors into one shape.
292
+ - [`json-from-llm`](https://www.npmjs.com/package/json-from-llm): extract valid JSON from an LLM response, even inside reasoning tags, fenced blocks or prose
293
+ - [`tool-schema`](https://www.npmjs.com/package/tool-schema): convert a JSON Schema into a provider tool / function-calling schema for OpenAI, Anthropic, Gemini and MCP
294
+ - [`llm-messages`](https://www.npmjs.com/package/llm-messages): convert chat messages between OpenAI, Anthropic and Gemini formats
295
+ - [`llm-errors`](https://www.npmjs.com/package/llm-errors): normalize provider errors (rate limits, retries, status) into one shape
137
296
 
138
297
  ## License
139
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-sse",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "Parse streaming SSE responses from OpenAI, Anthropic, Gemini and OpenAI-compatible providers into one unified event format. Text, reasoning and tool-call deltas. Zero dependencies.",
5
5
  "keywords": [
6
6
  "openai",
@@ -65,13 +65,11 @@
65
65
  "format": "prettier --write .",
66
66
  "format:check": "prettier --check .",
67
67
  "prepublishOnly": "npm run build",
68
- "prepare": "npm run build",
69
- "badges:downloads": "node scripts/update-download-badge.mjs",
70
- "badges:downloads:check": "node scripts/update-download-badge.mjs --check"
68
+ "prepare": "npm run build"
71
69
  },
72
70
  "devDependencies": {
73
71
  "@eslint/js": "^10.0.1",
74
- "@types/node": "^25.9.1",
72
+ "@types/node": "^26.0.1",
75
73
  "eslint": "^10.4.1",
76
74
  "prettier": "^3.4.2",
77
75
  "tsup": "^8.3.5",