llm-sse 0.1.0

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] - 2026-06-03
8
+
9
+ ### Added
10
+
11
+ - `parseOpenAIStream`, `parseAnthropicStream`, `parseGeminiStream` and the
12
+ `parseStream(source, provider)` dispatcher — parse a provider stream into a
13
+ unified `AsyncGenerator<StreamEvent>`.
14
+ - `StreamEvent` model: `text`, `tool_call_start`, `tool_call_delta`, `finish`,
15
+ `error`.
16
+ - `collectStream(events)` — drain a stream into `{ text, toolCalls, finishReason }`.
17
+ - `sseData(source)` — a robust SSE parser exported for advanced use.
18
+ - Handles chunk-boundary splitting, CRLF, multi-line `data:` fields, comments,
19
+ and `Uint8Array` or string sources.
20
+ - Zero runtime dependencies; ESM + CJS builds with type declarations.
21
+
22
+ [0.1.0]: https://github.com/slegarraga/llm-sse/releases/tag/v0.1.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sebastian Legarraga
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # llm-sse
2
+
3
+ > Parse streaming responses from OpenAI, Anthropic and Gemini into one unified event format. Text and tool-call deltas, handled. **Zero dependencies.**
4
+
5
+ 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.
6
+
7
+ ```ts
8
+ import { parseOpenAIStream, collectStream } from 'llm-sse';
9
+
10
+ const res = await fetch('https://api.openai.com/v1/chat/completions', {
11
+ method: 'POST',
12
+ headers: {
13
+ Authorization: `Bearer ${key}`,
14
+ 'content-type': 'application/json',
15
+ },
16
+ body: JSON.stringify({ model, messages, stream: true }),
17
+ });
18
+
19
+ for await (const event of parseOpenAIStream(res.body)) {
20
+ if (event.type === 'text') process.stdout.write(event.text);
21
+ }
22
+ ```
23
+
24
+ ## Why
25
+
26
+ - **One event shape, three providers.** `text`, `tool_call_start`, `tool_call_delta`, `finish`, `error` — the same whether the bytes came from OpenAI, Anthropic or Gemini.
27
+ - **Tool calls just accumulate.** Streamed JSON argument fragments carry an `index`; concatenate by index (or let `collectStream` do it) to get the full call.
28
+ - **Correct SSE framing.** Robust to chunk boundaries splitting a line or event mid-way, CRLF, multi-line `data:` fields, comments and keep-alives.
29
+ - **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.
30
+ - **Zero dependencies**, ESM + CJS, fully typed.
31
+
32
+ ## Install
33
+
34
+ ```sh
35
+ npm install llm-sse
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### `parseOpenAIStream(source)` · `parseAnthropicStream(source)` · `parseGeminiStream(source)`
41
+
42
+ Each takes a `source` (`AsyncIterable<Uint8Array | string>` — `fetch().body` satisfies this) and returns an `AsyncGenerator<StreamEvent>`.
43
+
44
+ > Gemini: use the SSE form of the streaming endpoint (`streamGenerateContent?alt=sse`).
45
+
46
+ ### `parseStream(source, provider)`
47
+
48
+ Same thing, dispatching on `provider` (`'openai' | 'anthropic' | 'gemini'`).
49
+
50
+ ### `StreamEvent`
51
+
52
+ ```ts
53
+ type StreamEvent =
54
+ | { type: 'text'; text: string }
55
+ | { type: 'tool_call_start'; index: number; id?: string; name?: string }
56
+ | { type: 'tool_call_delta'; index: number; argumentsDelta: string }
57
+ | { type: 'finish'; reason?: string }
58
+ | { type: 'error'; error: unknown };
59
+ ```
60
+
61
+ ### `collectStream(events)`
62
+
63
+ Drains an event stream into a single message:
64
+
65
+ ```ts
66
+ const { text, toolCalls, finishReason } = await collectStream(
67
+ parseAnthropicStream(res.body),
68
+ );
69
+ // toolCalls: { index, id?, name?, arguments }[] — arguments is the joined JSON string
70
+ ```
71
+
72
+ ### `sseData(source)`
73
+
74
+ The underlying SSE parser, exported for advanced use: yields the `data` payload of each event as a string.
75
+
76
+ ## Tool calls
77
+
78
+ All three providers are normalized to the same pattern: a `tool_call_start` (with `index`, and `id` / `name` when available) followed by one or more `tool_call_delta`s whose `argumentsDelta` strings concatenate into the call's JSON arguments. OpenAI and Anthropic fragment the arguments; Gemini sends them whole in a single delta. `collectStream` joins them for you.
79
+
80
+ ## Related
81
+
82
+ - [`tool-schema`](https://www.npmjs.com/package/tool-schema) — convert a JSON Schema into OpenAI / Anthropic / Gemini / MCP tool schemas.
83
+ - [`llm-messages`](https://www.npmjs.com/package/llm-messages) — convert conversations and responses between providers.
84
+ - [`llm-errors`](https://www.npmjs.com/package/llm-errors) — normalize provider errors into one shape.
85
+
86
+ ## License
87
+
88
+ MIT © Sebastian Legarraga
package/dist/index.cjs ADDED
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ collectStream: () => collectStream,
24
+ parseAnthropicStream: () => parseAnthropicStream,
25
+ parseGeminiStream: () => parseGeminiStream,
26
+ parseOpenAIStream: () => parseOpenAIStream,
27
+ parseStream: () => parseStream,
28
+ sseData: () => sseData
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/providers/anthropic.ts
33
+ function mapAnthropic(event) {
34
+ const events = [];
35
+ switch (event?.type) {
36
+ case "content_block_start": {
37
+ const block = event.content_block;
38
+ if (block?.type === "tool_use") {
39
+ events.push({
40
+ type: "tool_call_start",
41
+ index: event.index ?? 0,
42
+ id: block.id,
43
+ name: block.name
44
+ });
45
+ }
46
+ break;
47
+ }
48
+ case "content_block_delta": {
49
+ const delta = event.delta;
50
+ if (delta?.type === "text_delta" && typeof delta.text === "string") {
51
+ events.push({ type: "text", text: delta.text });
52
+ } else if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
53
+ events.push({
54
+ type: "tool_call_delta",
55
+ index: event.index ?? 0,
56
+ argumentsDelta: delta.partial_json
57
+ });
58
+ }
59
+ break;
60
+ }
61
+ case "message_delta": {
62
+ const reason = event.delta?.stop_reason;
63
+ if (reason) {
64
+ events.push({ type: "finish", reason });
65
+ }
66
+ break;
67
+ }
68
+ case "error": {
69
+ events.push({ type: "error", error: event.error ?? event });
70
+ break;
71
+ }
72
+ }
73
+ return events;
74
+ }
75
+
76
+ // src/providers/gemini.ts
77
+ function mapGemini(chunk, state) {
78
+ const events = [];
79
+ const candidate = chunk?.candidates?.[0];
80
+ if (!candidate) {
81
+ return events;
82
+ }
83
+ const parts = candidate.content?.parts;
84
+ if (Array.isArray(parts)) {
85
+ for (const part of parts) {
86
+ if (typeof part.text === "string" && part.text.length > 0) {
87
+ events.push({ type: "text", text: part.text });
88
+ }
89
+ if (part.functionCall) {
90
+ const index = state.toolIndex++;
91
+ events.push({
92
+ type: "tool_call_start",
93
+ index,
94
+ name: part.functionCall.name
95
+ });
96
+ events.push({
97
+ type: "tool_call_delta",
98
+ index,
99
+ argumentsDelta: JSON.stringify(part.functionCall.args ?? {})
100
+ });
101
+ }
102
+ }
103
+ }
104
+ if (candidate.finishReason) {
105
+ events.push({ type: "finish", reason: candidate.finishReason });
106
+ }
107
+ return events;
108
+ }
109
+
110
+ // src/providers/openai.ts
111
+ function mapOpenAI(chunk) {
112
+ const events = [];
113
+ const choice = chunk?.choices?.[0];
114
+ if (!choice) {
115
+ return events;
116
+ }
117
+ const delta = choice.delta;
118
+ if (delta) {
119
+ if (typeof delta.content === "string" && delta.content.length > 0) {
120
+ events.push({ type: "text", text: delta.content });
121
+ }
122
+ if (Array.isArray(delta.tool_calls)) {
123
+ for (const call of delta.tool_calls) {
124
+ const index = typeof call.index === "number" ? call.index : 0;
125
+ if (call.id !== void 0 || call.function?.name !== void 0) {
126
+ events.push({
127
+ type: "tool_call_start",
128
+ index,
129
+ id: call.id,
130
+ name: call.function?.name
131
+ });
132
+ }
133
+ const args = call.function?.arguments;
134
+ if (typeof args === "string" && args.length > 0) {
135
+ events.push({ type: "tool_call_delta", index, argumentsDelta: args });
136
+ }
137
+ }
138
+ }
139
+ }
140
+ if (choice.finish_reason) {
141
+ events.push({ type: "finish", reason: choice.finish_reason });
142
+ }
143
+ return events;
144
+ }
145
+
146
+ // src/sse.ts
147
+ async function* decodeChunks(source) {
148
+ const decoder = new TextDecoder();
149
+ for await (const chunk of source) {
150
+ if (typeof chunk === "string") {
151
+ yield chunk;
152
+ } else {
153
+ const text = decoder.decode(chunk, { stream: true });
154
+ if (text) {
155
+ yield text;
156
+ }
157
+ }
158
+ }
159
+ const tail = decoder.decode();
160
+ if (tail) {
161
+ yield tail;
162
+ }
163
+ }
164
+ async function* sseData(source) {
165
+ let buffer = "";
166
+ let dataLines = [];
167
+ for await (const text of decodeChunks(source)) {
168
+ buffer += text;
169
+ let newline;
170
+ while ((newline = buffer.indexOf("\n")) !== -1) {
171
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
172
+ buffer = buffer.slice(newline + 1);
173
+ if (line === "") {
174
+ if (dataLines.length > 0) {
175
+ yield dataLines.join("\n");
176
+ dataLines = [];
177
+ }
178
+ continue;
179
+ }
180
+ if (line[0] === ":") {
181
+ continue;
182
+ }
183
+ if (line.startsWith("data:")) {
184
+ dataLines.push(line.slice(5).replace(/^ /, ""));
185
+ }
186
+ }
187
+ }
188
+ const last = buffer.replace(/\r$/, "");
189
+ if (last.startsWith("data:")) {
190
+ dataLines.push(last.slice(5).replace(/^ /, ""));
191
+ }
192
+ if (dataLines.length > 0) {
193
+ yield dataLines.join("\n");
194
+ }
195
+ }
196
+
197
+ // src/parse.ts
198
+ async function* parseWith(source, map) {
199
+ for await (const data of sseData(source)) {
200
+ if (data === "[DONE]") {
201
+ return;
202
+ }
203
+ let payload;
204
+ try {
205
+ payload = JSON.parse(data);
206
+ } catch {
207
+ continue;
208
+ }
209
+ for (const event of map(payload)) {
210
+ yield event;
211
+ }
212
+ }
213
+ }
214
+ function parseOpenAIStream(source) {
215
+ return parseWith(source, mapOpenAI);
216
+ }
217
+ function parseAnthropicStream(source) {
218
+ return parseWith(source, mapAnthropic);
219
+ }
220
+ async function* parseGeminiStream(source) {
221
+ const state = { toolIndex: 0 };
222
+ for await (const data of sseData(source)) {
223
+ if (data === "[DONE]") {
224
+ return;
225
+ }
226
+ let payload;
227
+ try {
228
+ payload = JSON.parse(data);
229
+ } catch {
230
+ continue;
231
+ }
232
+ for (const event of mapGemini(payload, state)) {
233
+ yield event;
234
+ }
235
+ }
236
+ }
237
+ function parseStream(source, provider) {
238
+ switch (provider) {
239
+ case "openai":
240
+ return parseOpenAIStream(source);
241
+ case "anthropic":
242
+ return parseAnthropicStream(source);
243
+ case "gemini":
244
+ return parseGeminiStream(source);
245
+ }
246
+ }
247
+
248
+ // src/collect.ts
249
+ async function collectStream(events) {
250
+ let text = "";
251
+ let finishReason;
252
+ const byIndex = /* @__PURE__ */ new Map();
253
+ const order = [];
254
+ const ensure = (index) => {
255
+ let call = byIndex.get(index);
256
+ if (!call) {
257
+ call = { index, arguments: "" };
258
+ byIndex.set(index, call);
259
+ order.push(index);
260
+ }
261
+ return call;
262
+ };
263
+ for await (const event of events) {
264
+ switch (event.type) {
265
+ case "text":
266
+ text += event.text;
267
+ break;
268
+ case "tool_call_start": {
269
+ const call = ensure(event.index);
270
+ if (event.id !== void 0) {
271
+ call.id = event.id;
272
+ }
273
+ if (event.name !== void 0) {
274
+ call.name = event.name;
275
+ }
276
+ break;
277
+ }
278
+ case "tool_call_delta":
279
+ ensure(event.index).arguments += event.argumentsDelta;
280
+ break;
281
+ case "finish":
282
+ finishReason = event.reason;
283
+ break;
284
+ case "error":
285
+ break;
286
+ }
287
+ }
288
+ return {
289
+ text,
290
+ toolCalls: order.map((index) => byIndex.get(index)),
291
+ finishReason
292
+ };
293
+ }
294
+ // Annotate the CommonJS export names for ESM import in node:
295
+ 0 && (module.exports = {
296
+ collectStream,
297
+ parseAnthropicStream,
298
+ parseGeminiStream,
299
+ parseOpenAIStream,
300
+ parseStream,
301
+ sseData
302
+ });
303
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/providers/openai.ts","../src/sse.ts","../src/parse.ts","../src/collect.ts"],"sourcesContent":["export {\n parseStream,\n parseOpenAIStream,\n parseAnthropicStream,\n parseGeminiStream,\n} from './parse.ts';\nexport { collectStream } from './collect.ts';\nexport { sseData } from './sse.ts';\nexport type {\n Provider,\n StreamEvent,\n CollectedMessage,\n CollectedToolCall,\n ChunkSource,\n} from './types.ts';\n","import type { StreamEvent } from '../types.ts';\n\n/**\n * Map one Anthropic Messages stream event into normalized events.\n *\n * Anthropic uses typed events: `content_block_start` opens a text or `tool_use`\n * block at an `index`, `content_block_delta` carries `text_delta` /\n * `input_json_delta` fragments, and `message_delta` carries the `stop_reason`.\n */\nexport function mapAnthropic(event: any): StreamEvent[] {\n const events: StreamEvent[] = [];\n\n switch (event?.type) {\n case 'content_block_start': {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n events.push({\n type: 'tool_call_start',\n index: event.index ?? 0,\n id: block.id,\n name: block.name,\n });\n }\n break;\n }\n case 'content_block_delta': {\n const delta = event.delta;\n if (delta?.type === 'text_delta' && typeof delta.text === 'string') {\n events.push({ type: 'text', text: delta.text });\n } else if (\n delta?.type === 'input_json_delta' &&\n typeof delta.partial_json === 'string'\n ) {\n events.push({\n type: 'tool_call_delta',\n index: event.index ?? 0,\n argumentsDelta: delta.partial_json,\n });\n }\n break;\n }\n case 'message_delta': {\n const reason = event.delta?.stop_reason;\n if (reason) {\n events.push({ type: 'finish', reason });\n }\n break;\n }\n case 'error': {\n events.push({ type: 'error', error: event.error ?? event });\n break;\n }\n }\n\n return events;\n}\n","import type { StreamEvent } from '../types.ts';\n\n/** Per-stream state for Gemini, which does not number its tool calls. */\nexport interface GeminiState {\n toolIndex: number;\n}\n\n/**\n * Map one Gemini `GenerateContentResponse` chunk into normalized events.\n *\n * Gemini streams `candidates[0].content.parts[]`: a part is either `text` or a\n * complete `functionCall` (`{ name, args }`) — it does not fragment arguments,\n * so the whole `args` object is emitted as a single tool-call delta. Calls are\n * numbered in the order they appear via `state`.\n */\nexport function mapGemini(chunk: any, state: GeminiState): StreamEvent[] {\n const events: StreamEvent[] = [];\n const candidate = chunk?.candidates?.[0];\n if (!candidate) {\n return events;\n }\n\n const parts = candidate.content?.parts;\n if (Array.isArray(parts)) {\n for (const part of parts) {\n if (typeof part.text === 'string' && part.text.length > 0) {\n events.push({ type: 'text', text: part.text });\n }\n if (part.functionCall) {\n const index = state.toolIndex++;\n events.push({\n type: 'tool_call_start',\n index,\n name: part.functionCall.name,\n });\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: JSON.stringify(part.functionCall.args ?? {}),\n });\n }\n }\n }\n\n if (candidate.finishReason) {\n events.push({ type: 'finish', reason: candidate.finishReason });\n }\n return events;\n}\n","import type { StreamEvent } from '../types.ts';\n\n/**\n * Map one OpenAI `chat.completion.chunk` into normalized events.\n *\n * OpenAI streams a `choices[0].delta`: `content` carries text, and\n * `tool_calls[]` carry an `index`, an `id` + `function.name` on the first\n * fragment, then `function.arguments` fragments thereafter.\n */\nexport function mapOpenAI(chunk: any): StreamEvent[] {\n const events: StreamEvent[] = [];\n const choice = chunk?.choices?.[0];\n if (!choice) {\n return events;\n }\n\n const delta = choice.delta;\n if (delta) {\n if (typeof delta.content === 'string' && delta.content.length > 0) {\n events.push({ type: 'text', text: delta.content });\n }\n if (Array.isArray(delta.tool_calls)) {\n for (const call of delta.tool_calls) {\n const index = typeof call.index === 'number' ? call.index : 0;\n if (call.id !== undefined || call.function?.name !== undefined) {\n events.push({\n type: 'tool_call_start',\n index,\n id: call.id,\n name: call.function?.name,\n });\n }\n const args = call.function?.arguments;\n if (typeof args === 'string' && args.length > 0) {\n events.push({ type: 'tool_call_delta', index, argumentsDelta: args });\n }\n }\n }\n }\n\n if (choice.finish_reason) {\n events.push({ type: 'finish', reason: choice.finish_reason });\n }\n return events;\n}\n","import type { ChunkSource } from './types.ts';\n\n/**\n * Decode a mixed byte/string chunk source into text. `Uint8Array` chunks are\n * decoded with a streaming `TextDecoder` so a multibyte UTF-8 character split\n * across two chunks is reassembled correctly.\n */\nasync function* decodeChunks(source: ChunkSource): AsyncGenerator<string> {\n const decoder = new TextDecoder();\n for await (const chunk of source) {\n if (typeof chunk === 'string') {\n yield chunk;\n } else {\n const text = decoder.decode(chunk, { stream: true });\n if (text) {\n yield text;\n }\n }\n }\n const tail = decoder.decode();\n if (tail) {\n yield tail;\n }\n}\n\n/**\n * Parse a Server-Sent Events stream and yield the `data` payload of each event.\n *\n * Robust to the realities of streamed HTTP: events and lines split across\n * chunk boundaries are buffered until complete, multi-line `data:` fields are\n * joined with `\\n` (per the SSE spec), and comments (`:`) and other fields\n * (`event:`, `id:`, `retry:`) are ignored — the payload's own `type` field is\n * what the provider parsers key on.\n */\nexport async function* sseData(source: ChunkSource): AsyncGenerator<string> {\n let buffer = '';\n let dataLines: string[] = [];\n\n for await (const text of decodeChunks(source)) {\n buffer += text;\n\n let newline: number;\n while ((newline = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '');\n buffer = buffer.slice(newline + 1);\n\n if (line === '') {\n // Blank line terminates an event.\n if (dataLines.length > 0) {\n yield dataLines.join('\\n');\n dataLines = [];\n }\n continue;\n }\n if (line[0] === ':') {\n continue; // comment\n }\n if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.replace(/\\r$/, '');\n if (last.startsWith('data:')) {\n dataLines.push(last.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length > 0) {\n yield dataLines.join('\\n');\n }\n}\n","import { mapAnthropic } from './providers/anthropic.ts';\nimport { mapGemini } from './providers/gemini.ts';\nimport { mapOpenAI } from './providers/openai.ts';\nimport { sseData } from './sse.ts';\nimport type { ChunkSource, Provider, StreamEvent } from './types.ts';\n\n/** Shared SSE-to-events driver for the stateless providers. */\nasync function* parseWith(\n source: ChunkSource,\n map: (payload: any) => StreamEvent[],\n): AsyncGenerator<StreamEvent> {\n for await (const data of sseData(source)) {\n if (data === '[DONE]') {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch {\n continue; // ignore keep-alive / non-JSON data lines\n }\n for (const event of map(payload)) {\n yield event;\n }\n }\n}\n\n/** Parse an OpenAI Chat Completions stream into normalized events. */\nexport function parseOpenAIStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n return parseWith(source, mapOpenAI);\n}\n\n/** Parse an Anthropic Messages stream into normalized events. */\nexport function parseAnthropicStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n return parseWith(source, mapAnthropic);\n}\n\n/** Parse a Gemini `streamGenerateContent` (SSE) stream into normalized events. */\nexport async function* parseGeminiStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n const state = { toolIndex: 0 };\n for await (const data of sseData(source)) {\n if (data === '[DONE]') {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n for (const event of mapGemini(payload, state)) {\n yield event;\n }\n }\n}\n\n/** Parse a provider stream into normalized events, dispatching on `provider`. */\nexport function parseStream(\n source: ChunkSource,\n provider: Provider,\n): AsyncGenerator<StreamEvent> {\n switch (provider) {\n case 'openai':\n return parseOpenAIStream(source);\n case 'anthropic':\n return parseAnthropicStream(source);\n case 'gemini':\n return parseGeminiStream(source);\n }\n}\n","import type {\n CollectedMessage,\n CollectedToolCall,\n StreamEvent,\n} from './types.ts';\n\n/**\n * Drain a normalized event stream into a single assistant message: all text\n * concatenated, tool calls accumulated by `index` (arguments joined into one\n * JSON string), and the final stop reason.\n *\n * `error` events are not accumulated here — iterate the events directly if you\n * need to react to them mid-stream.\n *\n * @example\n * ```ts\n * const { text, toolCalls } = await collectStream(parseOpenAIStream(res.body));\n * ```\n */\nexport async function collectStream(\n events: AsyncIterable<StreamEvent>,\n): Promise<CollectedMessage> {\n let text = '';\n let finishReason: string | undefined;\n const byIndex = new Map<number, CollectedToolCall>();\n const order: number[] = [];\n\n const ensure = (index: number): CollectedToolCall => {\n let call = byIndex.get(index);\n if (!call) {\n call = { index, arguments: '' };\n byIndex.set(index, call);\n order.push(index);\n }\n return call;\n };\n\n for await (const event of events) {\n switch (event.type) {\n case 'text':\n text += event.text;\n break;\n case 'tool_call_start': {\n const call = ensure(event.index);\n if (event.id !== undefined) {\n call.id = event.id;\n }\n if (event.name !== undefined) {\n call.name = event.name;\n }\n break;\n }\n case 'tool_call_delta':\n ensure(event.index).arguments += event.argumentsDelta;\n break;\n case 'finish':\n finishReason = event.reason;\n break;\n case 'error':\n break;\n }\n }\n\n return {\n text,\n toolCalls: order.map((index) => byIndex.get(index)!),\n finishReason,\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,99 @@
1
+ /** The provider whose streaming format is being parsed. */
2
+ type Provider = 'openai' | 'anthropic' | 'gemini';
3
+ /**
4
+ * A single normalized event emitted while parsing a provider stream.
5
+ *
6
+ * The three providers stream very differently — OpenAI sends `choices[].delta`
7
+ * chunks, Anthropic sends typed `content_block_*` / `message_*` events, Gemini
8
+ * sends `candidates[].content.parts` — but they all reduce to the same handful
9
+ * of things: text arrived, a tool call started, tool-call arguments arrived,
10
+ * the turn finished, or the provider reported an error.
11
+ */
12
+ type StreamEvent =
13
+ /** A chunk of assistant text. */
14
+ {
15
+ type: 'text';
16
+ text: string;
17
+ }
18
+ /**
19
+ * A tool/function call began. `index` identifies the call within the turn so
20
+ * later {@link ToolCallDeltaEvent}s can be matched to it.
21
+ */
22
+ | {
23
+ type: 'tool_call_start';
24
+ index: number;
25
+ id?: string;
26
+ name?: string;
27
+ }
28
+ /** A fragment of a tool call's JSON arguments (concatenate by `index`). */
29
+ | {
30
+ type: 'tool_call_delta';
31
+ index: number;
32
+ argumentsDelta: string;
33
+ }
34
+ /** The turn finished. `reason` is the provider's stop reason, if any. */
35
+ | {
36
+ type: 'finish';
37
+ reason?: string;
38
+ }
39
+ /** The provider emitted an error event mid-stream. */
40
+ | {
41
+ type: 'error';
42
+ error: unknown;
43
+ };
44
+ /** A fully accumulated tool call, produced by {@link collectStream}. */
45
+ interface CollectedToolCall {
46
+ index: number;
47
+ id?: string;
48
+ name?: string;
49
+ /** The concatenated raw JSON arguments string. */
50
+ arguments: string;
51
+ }
52
+ /** The result of draining a stream with {@link collectStream}. */
53
+ interface CollectedMessage {
54
+ /** All text deltas concatenated in order. */
55
+ text: string;
56
+ /** Tool calls accumulated in order of first appearance. */
57
+ toolCalls: CollectedToolCall[];
58
+ /** The stop reason from the final `finish` event, if any. */
59
+ finishReason?: string;
60
+ }
61
+ /** A source of stream bytes or text: the shape `fetch().body` and Node streams satisfy. */
62
+ type ChunkSource = AsyncIterable<Uint8Array | string>;
63
+
64
+ /** Parse an OpenAI Chat Completions stream into normalized events. */
65
+ declare function parseOpenAIStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
66
+ /** Parse an Anthropic Messages stream into normalized events. */
67
+ declare function parseAnthropicStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
68
+ /** Parse a Gemini `streamGenerateContent` (SSE) stream into normalized events. */
69
+ declare function parseGeminiStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
70
+ /** Parse a provider stream into normalized events, dispatching on `provider`. */
71
+ declare function parseStream(source: ChunkSource, provider: Provider): AsyncGenerator<StreamEvent>;
72
+
73
+ /**
74
+ * Drain a normalized event stream into a single assistant message: all text
75
+ * concatenated, tool calls accumulated by `index` (arguments joined into one
76
+ * JSON string), and the final stop reason.
77
+ *
78
+ * `error` events are not accumulated here — iterate the events directly if you
79
+ * need to react to them mid-stream.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const { text, toolCalls } = await collectStream(parseOpenAIStream(res.body));
84
+ * ```
85
+ */
86
+ declare function collectStream(events: AsyncIterable<StreamEvent>): Promise<CollectedMessage>;
87
+
88
+ /**
89
+ * Parse a Server-Sent Events stream and yield the `data` payload of each event.
90
+ *
91
+ * Robust to the realities of streamed HTTP: events and lines split across
92
+ * chunk boundaries are buffered until complete, multi-line `data:` fields are
93
+ * joined with `\n` (per the SSE spec), and comments (`:`) and other fields
94
+ * (`event:`, `id:`, `retry:`) are ignored — the payload's own `type` field is
95
+ * what the provider parsers key on.
96
+ */
97
+ declare function sseData(source: ChunkSource): AsyncGenerator<string>;
98
+
99
+ export { type ChunkSource, type CollectedMessage, type CollectedToolCall, type Provider, type StreamEvent, collectStream, parseAnthropicStream, parseGeminiStream, parseOpenAIStream, parseStream, sseData };
@@ -0,0 +1,99 @@
1
+ /** The provider whose streaming format is being parsed. */
2
+ type Provider = 'openai' | 'anthropic' | 'gemini';
3
+ /**
4
+ * A single normalized event emitted while parsing a provider stream.
5
+ *
6
+ * The three providers stream very differently — OpenAI sends `choices[].delta`
7
+ * chunks, Anthropic sends typed `content_block_*` / `message_*` events, Gemini
8
+ * sends `candidates[].content.parts` — but they all reduce to the same handful
9
+ * of things: text arrived, a tool call started, tool-call arguments arrived,
10
+ * the turn finished, or the provider reported an error.
11
+ */
12
+ type StreamEvent =
13
+ /** A chunk of assistant text. */
14
+ {
15
+ type: 'text';
16
+ text: string;
17
+ }
18
+ /**
19
+ * A tool/function call began. `index` identifies the call within the turn so
20
+ * later {@link ToolCallDeltaEvent}s can be matched to it.
21
+ */
22
+ | {
23
+ type: 'tool_call_start';
24
+ index: number;
25
+ id?: string;
26
+ name?: string;
27
+ }
28
+ /** A fragment of a tool call's JSON arguments (concatenate by `index`). */
29
+ | {
30
+ type: 'tool_call_delta';
31
+ index: number;
32
+ argumentsDelta: string;
33
+ }
34
+ /** The turn finished. `reason` is the provider's stop reason, if any. */
35
+ | {
36
+ type: 'finish';
37
+ reason?: string;
38
+ }
39
+ /** The provider emitted an error event mid-stream. */
40
+ | {
41
+ type: 'error';
42
+ error: unknown;
43
+ };
44
+ /** A fully accumulated tool call, produced by {@link collectStream}. */
45
+ interface CollectedToolCall {
46
+ index: number;
47
+ id?: string;
48
+ name?: string;
49
+ /** The concatenated raw JSON arguments string. */
50
+ arguments: string;
51
+ }
52
+ /** The result of draining a stream with {@link collectStream}. */
53
+ interface CollectedMessage {
54
+ /** All text deltas concatenated in order. */
55
+ text: string;
56
+ /** Tool calls accumulated in order of first appearance. */
57
+ toolCalls: CollectedToolCall[];
58
+ /** The stop reason from the final `finish` event, if any. */
59
+ finishReason?: string;
60
+ }
61
+ /** A source of stream bytes or text: the shape `fetch().body` and Node streams satisfy. */
62
+ type ChunkSource = AsyncIterable<Uint8Array | string>;
63
+
64
+ /** Parse an OpenAI Chat Completions stream into normalized events. */
65
+ declare function parseOpenAIStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
66
+ /** Parse an Anthropic Messages stream into normalized events. */
67
+ declare function parseAnthropicStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
68
+ /** Parse a Gemini `streamGenerateContent` (SSE) stream into normalized events. */
69
+ declare function parseGeminiStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
70
+ /** Parse a provider stream into normalized events, dispatching on `provider`. */
71
+ declare function parseStream(source: ChunkSource, provider: Provider): AsyncGenerator<StreamEvent>;
72
+
73
+ /**
74
+ * Drain a normalized event stream into a single assistant message: all text
75
+ * concatenated, tool calls accumulated by `index` (arguments joined into one
76
+ * JSON string), and the final stop reason.
77
+ *
78
+ * `error` events are not accumulated here — iterate the events directly if you
79
+ * need to react to them mid-stream.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const { text, toolCalls } = await collectStream(parseOpenAIStream(res.body));
84
+ * ```
85
+ */
86
+ declare function collectStream(events: AsyncIterable<StreamEvent>): Promise<CollectedMessage>;
87
+
88
+ /**
89
+ * Parse a Server-Sent Events stream and yield the `data` payload of each event.
90
+ *
91
+ * Robust to the realities of streamed HTTP: events and lines split across
92
+ * chunk boundaries are buffered until complete, multi-line `data:` fields are
93
+ * joined with `\n` (per the SSE spec), and comments (`:`) and other fields
94
+ * (`event:`, `id:`, `retry:`) are ignored — the payload's own `type` field is
95
+ * what the provider parsers key on.
96
+ */
97
+ declare function sseData(source: ChunkSource): AsyncGenerator<string>;
98
+
99
+ export { type ChunkSource, type CollectedMessage, type CollectedToolCall, type Provider, type StreamEvent, collectStream, parseAnthropicStream, parseGeminiStream, parseOpenAIStream, parseStream, sseData };
package/dist/index.js ADDED
@@ -0,0 +1,271 @@
1
+ // src/providers/anthropic.ts
2
+ function mapAnthropic(event) {
3
+ const events = [];
4
+ switch (event?.type) {
5
+ case "content_block_start": {
6
+ const block = event.content_block;
7
+ if (block?.type === "tool_use") {
8
+ events.push({
9
+ type: "tool_call_start",
10
+ index: event.index ?? 0,
11
+ id: block.id,
12
+ name: block.name
13
+ });
14
+ }
15
+ break;
16
+ }
17
+ case "content_block_delta": {
18
+ const delta = event.delta;
19
+ if (delta?.type === "text_delta" && typeof delta.text === "string") {
20
+ events.push({ type: "text", text: delta.text });
21
+ } else if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
22
+ events.push({
23
+ type: "tool_call_delta",
24
+ index: event.index ?? 0,
25
+ argumentsDelta: delta.partial_json
26
+ });
27
+ }
28
+ break;
29
+ }
30
+ case "message_delta": {
31
+ const reason = event.delta?.stop_reason;
32
+ if (reason) {
33
+ events.push({ type: "finish", reason });
34
+ }
35
+ break;
36
+ }
37
+ case "error": {
38
+ events.push({ type: "error", error: event.error ?? event });
39
+ break;
40
+ }
41
+ }
42
+ return events;
43
+ }
44
+
45
+ // src/providers/gemini.ts
46
+ function mapGemini(chunk, state) {
47
+ const events = [];
48
+ const candidate = chunk?.candidates?.[0];
49
+ if (!candidate) {
50
+ return events;
51
+ }
52
+ const parts = candidate.content?.parts;
53
+ if (Array.isArray(parts)) {
54
+ for (const part of parts) {
55
+ if (typeof part.text === "string" && part.text.length > 0) {
56
+ events.push({ type: "text", text: part.text });
57
+ }
58
+ if (part.functionCall) {
59
+ const index = state.toolIndex++;
60
+ events.push({
61
+ type: "tool_call_start",
62
+ index,
63
+ name: part.functionCall.name
64
+ });
65
+ events.push({
66
+ type: "tool_call_delta",
67
+ index,
68
+ argumentsDelta: JSON.stringify(part.functionCall.args ?? {})
69
+ });
70
+ }
71
+ }
72
+ }
73
+ if (candidate.finishReason) {
74
+ events.push({ type: "finish", reason: candidate.finishReason });
75
+ }
76
+ return events;
77
+ }
78
+
79
+ // src/providers/openai.ts
80
+ function mapOpenAI(chunk) {
81
+ const events = [];
82
+ const choice = chunk?.choices?.[0];
83
+ if (!choice) {
84
+ return events;
85
+ }
86
+ const delta = choice.delta;
87
+ if (delta) {
88
+ if (typeof delta.content === "string" && delta.content.length > 0) {
89
+ events.push({ type: "text", text: delta.content });
90
+ }
91
+ if (Array.isArray(delta.tool_calls)) {
92
+ for (const call of delta.tool_calls) {
93
+ const index = typeof call.index === "number" ? call.index : 0;
94
+ if (call.id !== void 0 || call.function?.name !== void 0) {
95
+ events.push({
96
+ type: "tool_call_start",
97
+ index,
98
+ id: call.id,
99
+ name: call.function?.name
100
+ });
101
+ }
102
+ const args = call.function?.arguments;
103
+ if (typeof args === "string" && args.length > 0) {
104
+ events.push({ type: "tool_call_delta", index, argumentsDelta: args });
105
+ }
106
+ }
107
+ }
108
+ }
109
+ if (choice.finish_reason) {
110
+ events.push({ type: "finish", reason: choice.finish_reason });
111
+ }
112
+ return events;
113
+ }
114
+
115
+ // src/sse.ts
116
+ async function* decodeChunks(source) {
117
+ const decoder = new TextDecoder();
118
+ for await (const chunk of source) {
119
+ if (typeof chunk === "string") {
120
+ yield chunk;
121
+ } else {
122
+ const text = decoder.decode(chunk, { stream: true });
123
+ if (text) {
124
+ yield text;
125
+ }
126
+ }
127
+ }
128
+ const tail = decoder.decode();
129
+ if (tail) {
130
+ yield tail;
131
+ }
132
+ }
133
+ async function* sseData(source) {
134
+ let buffer = "";
135
+ let dataLines = [];
136
+ for await (const text of decodeChunks(source)) {
137
+ buffer += text;
138
+ let newline;
139
+ while ((newline = buffer.indexOf("\n")) !== -1) {
140
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
141
+ buffer = buffer.slice(newline + 1);
142
+ if (line === "") {
143
+ if (dataLines.length > 0) {
144
+ yield dataLines.join("\n");
145
+ dataLines = [];
146
+ }
147
+ continue;
148
+ }
149
+ if (line[0] === ":") {
150
+ continue;
151
+ }
152
+ if (line.startsWith("data:")) {
153
+ dataLines.push(line.slice(5).replace(/^ /, ""));
154
+ }
155
+ }
156
+ }
157
+ const last = buffer.replace(/\r$/, "");
158
+ if (last.startsWith("data:")) {
159
+ dataLines.push(last.slice(5).replace(/^ /, ""));
160
+ }
161
+ if (dataLines.length > 0) {
162
+ yield dataLines.join("\n");
163
+ }
164
+ }
165
+
166
+ // src/parse.ts
167
+ async function* parseWith(source, map) {
168
+ for await (const data of sseData(source)) {
169
+ if (data === "[DONE]") {
170
+ return;
171
+ }
172
+ let payload;
173
+ try {
174
+ payload = JSON.parse(data);
175
+ } catch {
176
+ continue;
177
+ }
178
+ for (const event of map(payload)) {
179
+ yield event;
180
+ }
181
+ }
182
+ }
183
+ function parseOpenAIStream(source) {
184
+ return parseWith(source, mapOpenAI);
185
+ }
186
+ function parseAnthropicStream(source) {
187
+ return parseWith(source, mapAnthropic);
188
+ }
189
+ async function* parseGeminiStream(source) {
190
+ const state = { toolIndex: 0 };
191
+ for await (const data of sseData(source)) {
192
+ if (data === "[DONE]") {
193
+ return;
194
+ }
195
+ let payload;
196
+ try {
197
+ payload = JSON.parse(data);
198
+ } catch {
199
+ continue;
200
+ }
201
+ for (const event of mapGemini(payload, state)) {
202
+ yield event;
203
+ }
204
+ }
205
+ }
206
+ function parseStream(source, provider) {
207
+ switch (provider) {
208
+ case "openai":
209
+ return parseOpenAIStream(source);
210
+ case "anthropic":
211
+ return parseAnthropicStream(source);
212
+ case "gemini":
213
+ return parseGeminiStream(source);
214
+ }
215
+ }
216
+
217
+ // src/collect.ts
218
+ async function collectStream(events) {
219
+ let text = "";
220
+ let finishReason;
221
+ const byIndex = /* @__PURE__ */ new Map();
222
+ const order = [];
223
+ const ensure = (index) => {
224
+ let call = byIndex.get(index);
225
+ if (!call) {
226
+ call = { index, arguments: "" };
227
+ byIndex.set(index, call);
228
+ order.push(index);
229
+ }
230
+ return call;
231
+ };
232
+ for await (const event of events) {
233
+ switch (event.type) {
234
+ case "text":
235
+ text += event.text;
236
+ break;
237
+ case "tool_call_start": {
238
+ const call = ensure(event.index);
239
+ if (event.id !== void 0) {
240
+ call.id = event.id;
241
+ }
242
+ if (event.name !== void 0) {
243
+ call.name = event.name;
244
+ }
245
+ break;
246
+ }
247
+ case "tool_call_delta":
248
+ ensure(event.index).arguments += event.argumentsDelta;
249
+ break;
250
+ case "finish":
251
+ finishReason = event.reason;
252
+ break;
253
+ case "error":
254
+ break;
255
+ }
256
+ }
257
+ return {
258
+ text,
259
+ toolCalls: order.map((index) => byIndex.get(index)),
260
+ finishReason
261
+ };
262
+ }
263
+ export {
264
+ collectStream,
265
+ parseAnthropicStream,
266
+ parseGeminiStream,
267
+ parseOpenAIStream,
268
+ parseStream,
269
+ sseData
270
+ };
271
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/providers/openai.ts","../src/sse.ts","../src/parse.ts","../src/collect.ts"],"sourcesContent":["import type { StreamEvent } from '../types.ts';\n\n/**\n * Map one Anthropic Messages stream event into normalized events.\n *\n * Anthropic uses typed events: `content_block_start` opens a text or `tool_use`\n * block at an `index`, `content_block_delta` carries `text_delta` /\n * `input_json_delta` fragments, and `message_delta` carries the `stop_reason`.\n */\nexport function mapAnthropic(event: any): StreamEvent[] {\n const events: StreamEvent[] = [];\n\n switch (event?.type) {\n case 'content_block_start': {\n const block = event.content_block;\n if (block?.type === 'tool_use') {\n events.push({\n type: 'tool_call_start',\n index: event.index ?? 0,\n id: block.id,\n name: block.name,\n });\n }\n break;\n }\n case 'content_block_delta': {\n const delta = event.delta;\n if (delta?.type === 'text_delta' && typeof delta.text === 'string') {\n events.push({ type: 'text', text: delta.text });\n } else if (\n delta?.type === 'input_json_delta' &&\n typeof delta.partial_json === 'string'\n ) {\n events.push({\n type: 'tool_call_delta',\n index: event.index ?? 0,\n argumentsDelta: delta.partial_json,\n });\n }\n break;\n }\n case 'message_delta': {\n const reason = event.delta?.stop_reason;\n if (reason) {\n events.push({ type: 'finish', reason });\n }\n break;\n }\n case 'error': {\n events.push({ type: 'error', error: event.error ?? event });\n break;\n }\n }\n\n return events;\n}\n","import type { StreamEvent } from '../types.ts';\n\n/** Per-stream state for Gemini, which does not number its tool calls. */\nexport interface GeminiState {\n toolIndex: number;\n}\n\n/**\n * Map one Gemini `GenerateContentResponse` chunk into normalized events.\n *\n * Gemini streams `candidates[0].content.parts[]`: a part is either `text` or a\n * complete `functionCall` (`{ name, args }`) — it does not fragment arguments,\n * so the whole `args` object is emitted as a single tool-call delta. Calls are\n * numbered in the order they appear via `state`.\n */\nexport function mapGemini(chunk: any, state: GeminiState): StreamEvent[] {\n const events: StreamEvent[] = [];\n const candidate = chunk?.candidates?.[0];\n if (!candidate) {\n return events;\n }\n\n const parts = candidate.content?.parts;\n if (Array.isArray(parts)) {\n for (const part of parts) {\n if (typeof part.text === 'string' && part.text.length > 0) {\n events.push({ type: 'text', text: part.text });\n }\n if (part.functionCall) {\n const index = state.toolIndex++;\n events.push({\n type: 'tool_call_start',\n index,\n name: part.functionCall.name,\n });\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: JSON.stringify(part.functionCall.args ?? {}),\n });\n }\n }\n }\n\n if (candidate.finishReason) {\n events.push({ type: 'finish', reason: candidate.finishReason });\n }\n return events;\n}\n","import type { StreamEvent } from '../types.ts';\n\n/**\n * Map one OpenAI `chat.completion.chunk` into normalized events.\n *\n * OpenAI streams a `choices[0].delta`: `content` carries text, and\n * `tool_calls[]` carry an `index`, an `id` + `function.name` on the first\n * fragment, then `function.arguments` fragments thereafter.\n */\nexport function mapOpenAI(chunk: any): StreamEvent[] {\n const events: StreamEvent[] = [];\n const choice = chunk?.choices?.[0];\n if (!choice) {\n return events;\n }\n\n const delta = choice.delta;\n if (delta) {\n if (typeof delta.content === 'string' && delta.content.length > 0) {\n events.push({ type: 'text', text: delta.content });\n }\n if (Array.isArray(delta.tool_calls)) {\n for (const call of delta.tool_calls) {\n const index = typeof call.index === 'number' ? call.index : 0;\n if (call.id !== undefined || call.function?.name !== undefined) {\n events.push({\n type: 'tool_call_start',\n index,\n id: call.id,\n name: call.function?.name,\n });\n }\n const args = call.function?.arguments;\n if (typeof args === 'string' && args.length > 0) {\n events.push({ type: 'tool_call_delta', index, argumentsDelta: args });\n }\n }\n }\n }\n\n if (choice.finish_reason) {\n events.push({ type: 'finish', reason: choice.finish_reason });\n }\n return events;\n}\n","import type { ChunkSource } from './types.ts';\n\n/**\n * Decode a mixed byte/string chunk source into text. `Uint8Array` chunks are\n * decoded with a streaming `TextDecoder` so a multibyte UTF-8 character split\n * across two chunks is reassembled correctly.\n */\nasync function* decodeChunks(source: ChunkSource): AsyncGenerator<string> {\n const decoder = new TextDecoder();\n for await (const chunk of source) {\n if (typeof chunk === 'string') {\n yield chunk;\n } else {\n const text = decoder.decode(chunk, { stream: true });\n if (text) {\n yield text;\n }\n }\n }\n const tail = decoder.decode();\n if (tail) {\n yield tail;\n }\n}\n\n/**\n * Parse a Server-Sent Events stream and yield the `data` payload of each event.\n *\n * Robust to the realities of streamed HTTP: events and lines split across\n * chunk boundaries are buffered until complete, multi-line `data:` fields are\n * joined with `\\n` (per the SSE spec), and comments (`:`) and other fields\n * (`event:`, `id:`, `retry:`) are ignored — the payload's own `type` field is\n * what the provider parsers key on.\n */\nexport async function* sseData(source: ChunkSource): AsyncGenerator<string> {\n let buffer = '';\n let dataLines: string[] = [];\n\n for await (const text of decodeChunks(source)) {\n buffer += text;\n\n let newline: number;\n while ((newline = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newline).replace(/\\r$/, '');\n buffer = buffer.slice(newline + 1);\n\n if (line === '') {\n // Blank line terminates an event.\n if (dataLines.length > 0) {\n yield dataLines.join('\\n');\n dataLines = [];\n }\n continue;\n }\n if (line[0] === ':') {\n continue; // comment\n }\n if (line.startsWith('data:')) {\n dataLines.push(line.slice(5).replace(/^ /, ''));\n }\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.replace(/\\r$/, '');\n if (last.startsWith('data:')) {\n dataLines.push(last.slice(5).replace(/^ /, ''));\n }\n if (dataLines.length > 0) {\n yield dataLines.join('\\n');\n }\n}\n","import { mapAnthropic } from './providers/anthropic.ts';\nimport { mapGemini } from './providers/gemini.ts';\nimport { mapOpenAI } from './providers/openai.ts';\nimport { sseData } from './sse.ts';\nimport type { ChunkSource, Provider, StreamEvent } from './types.ts';\n\n/** Shared SSE-to-events driver for the stateless providers. */\nasync function* parseWith(\n source: ChunkSource,\n map: (payload: any) => StreamEvent[],\n): AsyncGenerator<StreamEvent> {\n for await (const data of sseData(source)) {\n if (data === '[DONE]') {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch {\n continue; // ignore keep-alive / non-JSON data lines\n }\n for (const event of map(payload)) {\n yield event;\n }\n }\n}\n\n/** Parse an OpenAI Chat Completions stream into normalized events. */\nexport function parseOpenAIStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n return parseWith(source, mapOpenAI);\n}\n\n/** Parse an Anthropic Messages stream into normalized events. */\nexport function parseAnthropicStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n return parseWith(source, mapAnthropic);\n}\n\n/** Parse a Gemini `streamGenerateContent` (SSE) stream into normalized events. */\nexport async function* parseGeminiStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n const state = { toolIndex: 0 };\n for await (const data of sseData(source)) {\n if (data === '[DONE]') {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch {\n continue;\n }\n for (const event of mapGemini(payload, state)) {\n yield event;\n }\n }\n}\n\n/** Parse a provider stream into normalized events, dispatching on `provider`. */\nexport function parseStream(\n source: ChunkSource,\n provider: Provider,\n): AsyncGenerator<StreamEvent> {\n switch (provider) {\n case 'openai':\n return parseOpenAIStream(source);\n case 'anthropic':\n return parseAnthropicStream(source);\n case 'gemini':\n return parseGeminiStream(source);\n }\n}\n","import type {\n CollectedMessage,\n CollectedToolCall,\n StreamEvent,\n} from './types.ts';\n\n/**\n * Drain a normalized event stream into a single assistant message: all text\n * concatenated, tool calls accumulated by `index` (arguments joined into one\n * JSON string), and the final stop reason.\n *\n * `error` events are not accumulated here — iterate the events directly if you\n * need to react to them mid-stream.\n *\n * @example\n * ```ts\n * const { text, toolCalls } = await collectStream(parseOpenAIStream(res.body));\n * ```\n */\nexport async function collectStream(\n events: AsyncIterable<StreamEvent>,\n): Promise<CollectedMessage> {\n let text = '';\n let finishReason: string | undefined;\n const byIndex = new Map<number, CollectedToolCall>();\n const order: number[] = [];\n\n const ensure = (index: number): CollectedToolCall => {\n let call = byIndex.get(index);\n if (!call) {\n call = { index, arguments: '' };\n byIndex.set(index, call);\n order.push(index);\n }\n return call;\n };\n\n for await (const event of events) {\n switch (event.type) {\n case 'text':\n text += event.text;\n break;\n case 'tool_call_start': {\n const call = ensure(event.index);\n if (event.id !== undefined) {\n call.id = event.id;\n }\n if (event.name !== undefined) {\n call.name = event.name;\n }\n break;\n }\n case 'tool_call_delta':\n ensure(event.index).arguments += event.argumentsDelta;\n break;\n case 'finish':\n finishReason = event.reason;\n break;\n case 'error':\n break;\n }\n }\n\n return {\n text,\n toolCalls: order.map((index) => byIndex.get(index)!),\n finishReason,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "llm-sse",
3
+ "version": "0.1.0",
4
+ "description": "Parse streaming SSE responses from OpenAI, Anthropic and Gemini into one unified event format. Text and tool-call deltas. Zero dependencies.",
5
+ "keywords": [
6
+ "openai",
7
+ "anthropic",
8
+ "claude",
9
+ "gemini",
10
+ "llm",
11
+ "stream",
12
+ "streaming",
13
+ "sse",
14
+ "server-sent-events",
15
+ "tool-calls",
16
+ "delta",
17
+ "provider",
18
+ "portability",
19
+ "ai",
20
+ "agents"
21
+ ],
22
+ "license": "MIT",
23
+ "author": "Sebastian Legarraga",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/slegarraga/llm-sse.git"
27
+ },
28
+ "homepage": "https://github.com/slegarraga/llm-sse#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/slegarraga/llm-sse/issues"
31
+ },
32
+ "type": "module",
33
+ "main": "./dist/index.cjs",
34
+ "module": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "require": "./dist/index.cjs"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ },
53
+ "sideEffects": false,
54
+ "scripts": {
55
+ "build": "tsup",
56
+ "typecheck": "tsc --noEmit",
57
+ "test": "vitest run",
58
+ "test:watch": "vitest",
59
+ "lint": "eslint .",
60
+ "format": "prettier --write .",
61
+ "format:check": "prettier --check .",
62
+ "prepublishOnly": "npm run build",
63
+ "prepare": "npm run build"
64
+ },
65
+ "devDependencies": {
66
+ "@eslint/js": "^10.0.1",
67
+ "@types/node": "^25.9.1",
68
+ "eslint": "^10.4.1",
69
+ "prettier": "^3.4.2",
70
+ "tsup": "^8.3.5",
71
+ "typescript": "^5.7.2",
72
+ "typescript-eslint": "^8.60.0",
73
+ "vitest": "^2.1.8"
74
+ }
75
+ }