llm-sse 0.4.3 → 0.4.4

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 CHANGED
@@ -4,6 +4,18 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
5
5
  to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [Unreleased]
8
+
9
+ ## [0.4.4] - 2026-06-07
10
+
11
+ ### Changed
12
+
13
+ - Hardened streaming parsers around malformed JSON-like events, whitespace-padded
14
+ `[DONE]` sentinels, malformed Anthropic/OpenAI tool-call entries, malformed
15
+ Gemini parts and OpenAI chunks containing multiple choices.
16
+ - Documented parser caveats for non-JSON keep-alives, malformed JSON recovery,
17
+ malformed provider event shapes and OpenAI multi-choice chunks.
18
+
7
19
  ## [0.4.3] - 2026-06-05
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -103,6 +103,14 @@ const claudeBody = toAnthropic([...history, message]); // continue on Claude
103
103
 
104
104
  The underlying SSE parser, exported for advanced use: yields the `data` payload of each event as a string.
105
105
 
106
+ ## Caveats
107
+
108
+ - Non-JSON `data:` payloads are treated as keep-alives and skipped by provider parsers.
109
+ - JSON-looking malformed payloads surface as `error` events and parsing continues with later events.
110
+ - Provider parsers ignore SSE `event:` names and key off the JSON `data:` payload shape.
111
+ - Malformed provider event shapes are skipped when they cannot produce a valid normalized event.
112
+ - OpenAI chunks with multiple `choices` are emitted in provider order, but normalized events do not carry a choice index.
113
+
106
114
  ## Tool calls
107
115
 
108
116
  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.
package/dist/index.cjs CHANGED
@@ -37,11 +37,15 @@ function mapAnthropic(event) {
37
37
  case "content_block_start": {
38
38
  const block = event.content_block;
39
39
  if (block?.type === "tool_use") {
40
+ const index = blockIndex(event.index);
41
+ if (index === void 0) {
42
+ break;
43
+ }
40
44
  events.push({
41
45
  type: "tool_call_start",
42
- index: event.index ?? 0,
43
- id: block.id,
44
- name: block.name
46
+ index,
47
+ id: typeof block.id === "string" ? block.id : void 0,
48
+ name: typeof block.name === "string" ? block.name : void 0
45
49
  });
46
50
  }
47
51
  break;
@@ -53,9 +57,13 @@ function mapAnthropic(event) {
53
57
  } else if (delta?.type === "thinking_delta" && typeof delta.thinking === "string") {
54
58
  events.push({ type: "reasoning", text: delta.thinking });
55
59
  } else if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
60
+ const index = blockIndex(event.index);
61
+ if (index === void 0) {
62
+ break;
63
+ }
56
64
  events.push({
57
65
  type: "tool_call_delta",
58
- index: event.index ?? 0,
66
+ index,
59
67
  argumentsDelta: delta.partial_json
60
68
  });
61
69
  }
@@ -63,7 +71,7 @@ function mapAnthropic(event) {
63
71
  }
64
72
  case "message_delta": {
65
73
  const reason = event.delta?.stop_reason;
66
- if (reason) {
74
+ if (typeof reason === "string" && reason.length > 0) {
67
75
  events.push({ type: "finish", reason });
68
76
  }
69
77
  break;
@@ -75,6 +83,9 @@ function mapAnthropic(event) {
75
83
  }
76
84
  return events;
77
85
  }
86
+ function blockIndex(index) {
87
+ return typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : void 0;
88
+ }
78
89
 
79
90
  // src/providers/gemini.ts
80
91
  function mapGemini(chunk, state) {
@@ -86,28 +97,32 @@ function mapGemini(chunk, state) {
86
97
  const parts = candidate.content?.parts;
87
98
  if (Array.isArray(parts)) {
88
99
  for (const part of parts) {
100
+ if (!part || typeof part !== "object") {
101
+ continue;
102
+ }
89
103
  if (typeof part.text === "string" && part.text.length > 0) {
90
104
  events.push({
91
105
  type: part.thought === true ? "reasoning" : "text",
92
106
  text: part.text
93
107
  });
94
108
  }
95
- if (part.functionCall) {
109
+ const functionCall = part.functionCall;
110
+ if (functionCall && typeof functionCall === "object" && !Array.isArray(functionCall) && typeof functionCall.name === "string" && functionCall.name.length > 0) {
96
111
  const index = state.toolIndex++;
97
112
  events.push({
98
113
  type: "tool_call_start",
99
114
  index,
100
- name: part.functionCall.name
115
+ name: functionCall.name
101
116
  });
102
117
  events.push({
103
118
  type: "tool_call_delta",
104
119
  index,
105
- argumentsDelta: JSON.stringify(part.functionCall.args ?? {})
120
+ argumentsDelta: JSON.stringify(functionCall.args ?? {})
106
121
  });
107
122
  }
108
123
  }
109
124
  }
110
- if (candidate.finishReason) {
125
+ if (typeof candidate.finishReason === "string" && candidate.finishReason.length > 0) {
111
126
  events.push({ type: "finish", reason: candidate.finishReason });
112
127
  }
113
128
  return events;
@@ -116,39 +131,54 @@ function mapGemini(chunk, state) {
116
131
  // src/providers/openai.ts
117
132
  function mapOpenAI(chunk) {
118
133
  const events = [];
119
- const choice = chunk?.choices?.[0];
120
- if (!choice) {
134
+ const choices = Array.isArray(chunk?.choices) ? chunk.choices : [];
135
+ if (choices.length === 0) {
121
136
  return events;
122
137
  }
123
- const delta = choice.delta;
124
- if (delta) {
125
- const reasoning = delta.reasoning_content ?? delta.reasoning;
126
- if (typeof reasoning === "string" && reasoning.length > 0) {
127
- events.push({ type: "reasoning", text: reasoning });
128
- }
129
- if (typeof delta.content === "string" && delta.content.length > 0) {
130
- events.push({ type: "text", text: delta.content });
138
+ for (const choice of choices) {
139
+ if (!choice || typeof choice !== "object") {
140
+ continue;
131
141
  }
132
- if (Array.isArray(delta.tool_calls)) {
133
- for (const call of delta.tool_calls) {
134
- const index = typeof call.index === "number" ? call.index : 0;
135
- if (call.id !== void 0 || call.function?.name !== void 0) {
136
- events.push({
137
- type: "tool_call_start",
138
- index,
139
- id: call.id,
140
- name: call.function?.name
141
- });
142
- }
143
- const args = call.function?.arguments;
144
- if (typeof args === "string" && args.length > 0) {
145
- events.push({ type: "tool_call_delta", index, argumentsDelta: args });
142
+ const delta = choice.delta;
143
+ if (delta && typeof delta === "object") {
144
+ const reasoning = delta.reasoning_content ?? delta.reasoning;
145
+ if (typeof reasoning === "string" && reasoning.length > 0) {
146
+ events.push({ type: "reasoning", text: reasoning });
147
+ }
148
+ if (typeof delta.content === "string" && delta.content.length > 0) {
149
+ events.push({ type: "text", text: delta.content });
150
+ }
151
+ if (Array.isArray(delta.tool_calls)) {
152
+ for (const call of delta.tool_calls) {
153
+ if (!call || typeof call !== "object") {
154
+ continue;
155
+ }
156
+ const index = typeof call.index === "number" ? call.index : 0;
157
+ const fn = call.function && typeof call.function === "object" ? call.function : void 0;
158
+ const id = typeof call.id === "string" ? call.id : void 0;
159
+ const name = typeof fn?.name === "string" ? fn.name : void 0;
160
+ if (id !== void 0 || name !== void 0) {
161
+ events.push({
162
+ type: "tool_call_start",
163
+ index,
164
+ id,
165
+ name
166
+ });
167
+ }
168
+ const args = fn?.arguments;
169
+ if (typeof args === "string" && args.length > 0) {
170
+ events.push({
171
+ type: "tool_call_delta",
172
+ index,
173
+ argumentsDelta: args
174
+ });
175
+ }
146
176
  }
147
177
  }
148
178
  }
149
- }
150
- if (choice.finish_reason) {
151
- events.push({ type: "finish", reason: choice.finish_reason });
179
+ if (typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
180
+ events.push({ type: "finish", reason: choice.finish_reason });
181
+ }
152
182
  }
153
183
  return events;
154
184
  }
@@ -174,6 +204,28 @@ async function* decodeChunks(source) {
174
204
  async function* sseData(source) {
175
205
  let buffer = "";
176
206
  let dataLines = [];
207
+ const addLine = (line) => {
208
+ if (line[0] === ":") {
209
+ return;
210
+ }
211
+ const separator = line.indexOf(":");
212
+ const field = separator === -1 ? line : line.slice(0, separator);
213
+ let value = separator === -1 ? "" : line.slice(separator + 1);
214
+ if (value.startsWith(" ")) {
215
+ value = value.slice(1);
216
+ }
217
+ if (field === "data") {
218
+ dataLines.push(value);
219
+ }
220
+ };
221
+ const finishEvent = () => {
222
+ if (dataLines.length === 0) {
223
+ return void 0;
224
+ }
225
+ const data2 = dataLines.join("\n");
226
+ dataLines = [];
227
+ return data2;
228
+ };
177
229
  for await (const text of decodeChunks(source)) {
178
230
  buffer += text;
179
231
  let newline;
@@ -181,39 +233,39 @@ async function* sseData(source) {
181
233
  const line = buffer.slice(0, newline).replace(/\r$/, "");
182
234
  buffer = buffer.slice(newline + 1);
183
235
  if (line === "") {
184
- if (dataLines.length > 0) {
185
- yield dataLines.join("\n");
186
- dataLines = [];
236
+ const data2 = finishEvent();
237
+ if (data2 !== void 0) {
238
+ yield data2;
187
239
  }
188
240
  continue;
189
241
  }
190
- if (line[0] === ":") {
191
- continue;
192
- }
193
- if (line.startsWith("data:")) {
194
- dataLines.push(line.slice(5).replace(/^ /, ""));
195
- }
242
+ addLine(line);
196
243
  }
197
244
  }
198
245
  const last = buffer.replace(/\r$/, "");
199
- if (last.startsWith("data:")) {
200
- dataLines.push(last.slice(5).replace(/^ /, ""));
246
+ if (last !== "") {
247
+ addLine(last);
201
248
  }
202
- if (dataLines.length > 0) {
203
- yield dataLines.join("\n");
249
+ const data = finishEvent();
250
+ if (data !== void 0) {
251
+ yield data;
204
252
  }
205
253
  }
206
254
 
207
255
  // src/parse.ts
208
256
  async function* parseWith(source, map) {
209
257
  for await (const data of sseData(source)) {
210
- if (data === "[DONE]") {
258
+ if (isDone(data)) {
211
259
  return;
212
260
  }
213
261
  let payload;
214
262
  try {
215
263
  payload = JSON.parse(data);
216
- } catch {
264
+ } catch (error) {
265
+ const trimmed = data.trimStart();
266
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
267
+ yield malformedJsonEvent(error);
268
+ }
217
269
  continue;
218
270
  }
219
271
  for (const event of map(payload)) {
@@ -230,13 +282,17 @@ function parseAnthropicStream(source) {
230
282
  async function* parseGeminiStream(source) {
231
283
  const state = { toolIndex: 0 };
232
284
  for await (const data of sseData(source)) {
233
- if (data === "[DONE]") {
285
+ if (isDone(data)) {
234
286
  return;
235
287
  }
236
288
  let payload;
237
289
  try {
238
290
  payload = JSON.parse(data);
239
- } catch {
291
+ } catch (error) {
292
+ const trimmed = data.trimStart();
293
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
294
+ yield malformedJsonEvent(error);
295
+ }
240
296
  continue;
241
297
  }
242
298
  for (const event of mapGemini(payload, state)) {
@@ -254,6 +310,18 @@ function parseStream(source, provider) {
254
310
  return parseGeminiStream(source);
255
311
  }
256
312
  }
313
+ function malformedJsonEvent(error) {
314
+ return {
315
+ type: "error",
316
+ error: {
317
+ type: "malformed_json",
318
+ message: error instanceof Error ? error.message : String(error)
319
+ }
320
+ };
321
+ }
322
+ function isDone(data) {
323
+ return data.trim() === "[DONE]";
324
+ }
257
325
 
258
326
  // src/collect.ts
259
327
  async function collectStream(events) {
@@ -1 +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","../src/message.ts"],"sourcesContent":["export {\n parseStream,\n parseOpenAIStream,\n parseAnthropicStream,\n parseGeminiStream,\n} from './parse.ts';\nexport { collectStream } from './collect.ts';\nexport { toAssistantMessage } from './message.ts';\nexport type { AssistantMessage, AssistantToolCall } from './message.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 === 'thinking_delta' &&\n typeof delta.thinking === 'string'\n ) {\n events.push({ type: 'reasoning', text: delta.thinking });\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 // Gemini flags a thinking part with `thought: true`.\n events.push({\n type: part.thought === true ? 'reasoning' : 'text',\n text: part.text,\n });\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 // Reasoning models on OpenAI-compatible endpoints (e.g. DeepSeek R1) stream\n // their thinking in `reasoning_content` (some use `reasoning`).\n const reasoning = delta.reasoning_content ?? delta.reasoning;\n if (typeof reasoning === 'string' && reasoning.length > 0) {\n events.push({ type: 'reasoning', text: reasoning });\n }\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 reasoning = '';\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 'reasoning':\n reasoning += 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 reasoning,\n toolCalls: order.map((index) => byIndex.get(index)!),\n finishReason,\n };\n}\n","import type { CollectedMessage } from './types.ts';\n\n/**\n * An assistant message in OpenAI Chat Completions shape — the canonical \"hub\"\n * format. Append it to your message history to continue the conversation, or\n * pass it to `llm-messages` to port it to Anthropic or Gemini.\n */\nexport interface AssistantMessage {\n role: 'assistant';\n /** The assistant text, or `null` when the turn was only tool calls. */\n content: string | null;\n /** Present only when the turn produced tool calls. */\n tool_calls?: AssistantToolCall[];\n}\n\nexport interface AssistantToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string };\n}\n\n/**\n * Turn a {@link CollectedMessage} (from {@link collectStream}) into a standard\n * assistant message you can put back into a conversation.\n *\n * Output is the OpenAI Chat Completions shape, which is the format `llm-messages`\n * treats as canonical — so this composes directly with its `toAnthropic` /\n * `toGemini` converters. Tool calls without an id (e.g. from Gemini) get a\n * stable synthetic `call_<n>` id. Reasoning is intentionally omitted: it is not\n * part of the portable assistant message.\n *\n * @example\n * ```ts\n * const collected = await collectStream(parseOpenAIStream(res.body));\n * const message = toAssistantMessage(collected);\n * history.push(message); // or: toAnthropic([...history, message])\n * ```\n */\nexport function toAssistantMessage(\n collected: CollectedMessage,\n): AssistantMessage {\n const message: AssistantMessage = {\n role: 'assistant',\n content: collected.text.length > 0 ? collected.text : null,\n };\n\n if (collected.toolCalls.length > 0) {\n message.tool_calls = collected.toolCalls.map((call, position) => ({\n id: call.id ?? `call_${position}`,\n type: 'function',\n function: {\n name: call.name ?? '',\n arguments: call.arguments.length > 0 ? call.arguments : '{}',\n },\n }));\n }\n\n return message;\n}\n"],"mappings":"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","names":[]}
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","../src/message.ts"],"sourcesContent":["export {\n parseStream,\n parseOpenAIStream,\n parseAnthropicStream,\n parseGeminiStream,\n} from './parse.ts';\nexport { collectStream } from './collect.ts';\nexport { toAssistantMessage } from './message.ts';\nexport type { AssistantMessage, AssistantToolCall } from './message.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 const index = blockIndex(event.index);\n if (index === undefined) {\n break;\n }\n events.push({\n type: 'tool_call_start',\n index,\n id: typeof block.id === 'string' ? block.id : undefined,\n name: typeof block.name === 'string' ? block.name : undefined,\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 === 'thinking_delta' &&\n typeof delta.thinking === 'string'\n ) {\n events.push({ type: 'reasoning', text: delta.thinking });\n } else if (\n delta?.type === 'input_json_delta' &&\n typeof delta.partial_json === 'string'\n ) {\n const index = blockIndex(event.index);\n if (index === undefined) {\n break;\n }\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: delta.partial_json,\n });\n }\n break;\n }\n case 'message_delta': {\n const reason = event.delta?.stop_reason;\n if (typeof reason === 'string' && reason.length > 0) {\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\nfunction blockIndex(index: unknown): number | undefined {\n return typeof index === 'number' && Number.isInteger(index) && index >= 0\n ? index\n : undefined;\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 (!part || typeof part !== 'object') {\n continue;\n }\n\n if (typeof part.text === 'string' && part.text.length > 0) {\n // Gemini flags a thinking part with `thought: true`.\n events.push({\n type: part.thought === true ? 'reasoning' : 'text',\n text: part.text,\n });\n }\n const functionCall = part.functionCall;\n if (\n functionCall &&\n typeof functionCall === 'object' &&\n !Array.isArray(functionCall) &&\n typeof functionCall.name === 'string' &&\n functionCall.name.length > 0\n ) {\n const index = state.toolIndex++;\n events.push({\n type: 'tool_call_start',\n index,\n name: functionCall.name,\n });\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: JSON.stringify(functionCall.args ?? {}),\n });\n }\n }\n }\n\n if (\n typeof candidate.finishReason === 'string' &&\n candidate.finishReason.length > 0\n ) {\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 `choices[].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 choices = Array.isArray(chunk?.choices) ? chunk.choices : [];\n if (choices.length === 0) {\n return events;\n }\n\n for (const choice of choices) {\n if (!choice || typeof choice !== 'object') {\n continue;\n }\n\n const delta = choice.delta;\n if (delta && typeof delta === 'object') {\n // Reasoning models on OpenAI-compatible endpoints (e.g. DeepSeek R1)\n // stream their thinking in `reasoning_content` (some use `reasoning`).\n const reasoning = delta.reasoning_content ?? delta.reasoning;\n if (typeof reasoning === 'string' && reasoning.length > 0) {\n events.push({ type: 'reasoning', text: reasoning });\n }\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 if (!call || typeof call !== 'object') {\n continue;\n }\n\n const index = typeof call.index === 'number' ? call.index : 0;\n const fn =\n call.function && typeof call.function === 'object'\n ? call.function\n : undefined;\n const id = typeof call.id === 'string' ? call.id : undefined;\n const name = typeof fn?.name === 'string' ? fn.name : undefined;\n\n if (id !== undefined || name !== undefined) {\n events.push({\n type: 'tool_call_start',\n index,\n id,\n name,\n });\n }\n const args = fn?.arguments;\n if (typeof args === 'string' && args.length > 0) {\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: args,\n });\n }\n }\n }\n }\n\n if (\n typeof choice.finish_reason === 'string' &&\n choice.finish_reason.length > 0\n ) {\n events.push({ type: 'finish', reason: choice.finish_reason });\n }\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 const addLine = (line: string): void => {\n if (line[0] === ':') {\n return; // comment\n }\n\n const separator = line.indexOf(':');\n const field = separator === -1 ? line : line.slice(0, separator);\n let value = separator === -1 ? '' : line.slice(separator + 1);\n if (value.startsWith(' ')) {\n value = value.slice(1);\n }\n\n if (field === 'data') {\n dataLines.push(value);\n }\n };\n\n const finishEvent = (): string | undefined => {\n if (dataLines.length === 0) {\n return undefined;\n }\n const data = dataLines.join('\\n');\n dataLines = [];\n return data;\n };\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 const data = finishEvent();\n if (data !== undefined) {\n yield data;\n }\n continue;\n }\n addLine(line);\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.replace(/\\r$/, '');\n if (last !== '') {\n addLine(last);\n }\n const data = finishEvent();\n if (data !== undefined) {\n yield data;\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 (isDone(data)) {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch (error) {\n const trimmed = data.trimStart();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n yield malformedJsonEvent(error);\n }\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 (isDone(data)) {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch (error) {\n const trimmed = data.trimStart();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n yield malformedJsonEvent(error);\n }\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\nfunction malformedJsonEvent(error: unknown): StreamEvent {\n return {\n type: 'error',\n error: {\n type: 'malformed_json',\n message: error instanceof Error ? error.message : String(error),\n },\n };\n}\n\nfunction isDone(data: string): boolean {\n return data.trim() === '[DONE]';\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 reasoning = '';\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 'reasoning':\n reasoning += 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 reasoning,\n toolCalls: order.map((index) => byIndex.get(index)!),\n finishReason,\n };\n}\n","import type { CollectedMessage } from './types.ts';\n\n/**\n * An assistant message in OpenAI Chat Completions shape — the canonical \"hub\"\n * format. Append it to your message history to continue the conversation, or\n * pass it to `llm-messages` to port it to Anthropic or Gemini.\n */\nexport interface AssistantMessage {\n role: 'assistant';\n /** The assistant text, or `null` when the turn was only tool calls. */\n content: string | null;\n /** Present only when the turn produced tool calls. */\n tool_calls?: AssistantToolCall[];\n}\n\nexport interface AssistantToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string };\n}\n\n/**\n * Turn a {@link CollectedMessage} (from {@link collectStream}) into a standard\n * assistant message you can put back into a conversation.\n *\n * Output is the OpenAI Chat Completions shape, which is the format `llm-messages`\n * treats as canonical — so this composes directly with its `toAnthropic` /\n * `toGemini` converters. Tool calls without an id (e.g. from Gemini) get a\n * stable synthetic `call_<n>` id. Reasoning is intentionally omitted: it is not\n * part of the portable assistant message.\n *\n * @example\n * ```ts\n * const collected = await collectStream(parseOpenAIStream(res.body));\n * const message = toAssistantMessage(collected);\n * history.push(message); // or: toAnthropic([...history, message])\n * ```\n */\nexport function toAssistantMessage(\n collected: CollectedMessage,\n): AssistantMessage {\n const message: AssistantMessage = {\n role: 'assistant',\n content: collected.text.length > 0 ? collected.text : null,\n };\n\n if (collected.toolCalls.length > 0) {\n message.tool_calls = collected.toolCalls.map((call, position) => ({\n id: call.id ?? `call_${position}`,\n type: 'function',\n function: {\n name: call.name ?? '',\n arguments: call.arguments.length > 0 ? call.arguments : '{}',\n },\n }));\n }\n\n return message;\n}\n"],"mappings":"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","names":["data"]}
package/dist/index.js CHANGED
@@ -5,11 +5,15 @@ function mapAnthropic(event) {
5
5
  case "content_block_start": {
6
6
  const block = event.content_block;
7
7
  if (block?.type === "tool_use") {
8
+ const index = blockIndex(event.index);
9
+ if (index === void 0) {
10
+ break;
11
+ }
8
12
  events.push({
9
13
  type: "tool_call_start",
10
- index: event.index ?? 0,
11
- id: block.id,
12
- name: block.name
14
+ index,
15
+ id: typeof block.id === "string" ? block.id : void 0,
16
+ name: typeof block.name === "string" ? block.name : void 0
13
17
  });
14
18
  }
15
19
  break;
@@ -21,9 +25,13 @@ function mapAnthropic(event) {
21
25
  } else if (delta?.type === "thinking_delta" && typeof delta.thinking === "string") {
22
26
  events.push({ type: "reasoning", text: delta.thinking });
23
27
  } else if (delta?.type === "input_json_delta" && typeof delta.partial_json === "string") {
28
+ const index = blockIndex(event.index);
29
+ if (index === void 0) {
30
+ break;
31
+ }
24
32
  events.push({
25
33
  type: "tool_call_delta",
26
- index: event.index ?? 0,
34
+ index,
27
35
  argumentsDelta: delta.partial_json
28
36
  });
29
37
  }
@@ -31,7 +39,7 @@ function mapAnthropic(event) {
31
39
  }
32
40
  case "message_delta": {
33
41
  const reason = event.delta?.stop_reason;
34
- if (reason) {
42
+ if (typeof reason === "string" && reason.length > 0) {
35
43
  events.push({ type: "finish", reason });
36
44
  }
37
45
  break;
@@ -43,6 +51,9 @@ function mapAnthropic(event) {
43
51
  }
44
52
  return events;
45
53
  }
54
+ function blockIndex(index) {
55
+ return typeof index === "number" && Number.isInteger(index) && index >= 0 ? index : void 0;
56
+ }
46
57
 
47
58
  // src/providers/gemini.ts
48
59
  function mapGemini(chunk, state) {
@@ -54,28 +65,32 @@ function mapGemini(chunk, state) {
54
65
  const parts = candidate.content?.parts;
55
66
  if (Array.isArray(parts)) {
56
67
  for (const part of parts) {
68
+ if (!part || typeof part !== "object") {
69
+ continue;
70
+ }
57
71
  if (typeof part.text === "string" && part.text.length > 0) {
58
72
  events.push({
59
73
  type: part.thought === true ? "reasoning" : "text",
60
74
  text: part.text
61
75
  });
62
76
  }
63
- if (part.functionCall) {
77
+ const functionCall = part.functionCall;
78
+ if (functionCall && typeof functionCall === "object" && !Array.isArray(functionCall) && typeof functionCall.name === "string" && functionCall.name.length > 0) {
64
79
  const index = state.toolIndex++;
65
80
  events.push({
66
81
  type: "tool_call_start",
67
82
  index,
68
- name: part.functionCall.name
83
+ name: functionCall.name
69
84
  });
70
85
  events.push({
71
86
  type: "tool_call_delta",
72
87
  index,
73
- argumentsDelta: JSON.stringify(part.functionCall.args ?? {})
88
+ argumentsDelta: JSON.stringify(functionCall.args ?? {})
74
89
  });
75
90
  }
76
91
  }
77
92
  }
78
- if (candidate.finishReason) {
93
+ if (typeof candidate.finishReason === "string" && candidate.finishReason.length > 0) {
79
94
  events.push({ type: "finish", reason: candidate.finishReason });
80
95
  }
81
96
  return events;
@@ -84,39 +99,54 @@ function mapGemini(chunk, state) {
84
99
  // src/providers/openai.ts
85
100
  function mapOpenAI(chunk) {
86
101
  const events = [];
87
- const choice = chunk?.choices?.[0];
88
- if (!choice) {
102
+ const choices = Array.isArray(chunk?.choices) ? chunk.choices : [];
103
+ if (choices.length === 0) {
89
104
  return events;
90
105
  }
91
- const delta = choice.delta;
92
- if (delta) {
93
- const reasoning = delta.reasoning_content ?? delta.reasoning;
94
- if (typeof reasoning === "string" && reasoning.length > 0) {
95
- events.push({ type: "reasoning", text: reasoning });
96
- }
97
- if (typeof delta.content === "string" && delta.content.length > 0) {
98
- events.push({ type: "text", text: delta.content });
106
+ for (const choice of choices) {
107
+ if (!choice || typeof choice !== "object") {
108
+ continue;
99
109
  }
100
- if (Array.isArray(delta.tool_calls)) {
101
- for (const call of delta.tool_calls) {
102
- const index = typeof call.index === "number" ? call.index : 0;
103
- if (call.id !== void 0 || call.function?.name !== void 0) {
104
- events.push({
105
- type: "tool_call_start",
106
- index,
107
- id: call.id,
108
- name: call.function?.name
109
- });
110
- }
111
- const args = call.function?.arguments;
112
- if (typeof args === "string" && args.length > 0) {
113
- events.push({ type: "tool_call_delta", index, argumentsDelta: args });
110
+ const delta = choice.delta;
111
+ if (delta && typeof delta === "object") {
112
+ const reasoning = delta.reasoning_content ?? delta.reasoning;
113
+ if (typeof reasoning === "string" && reasoning.length > 0) {
114
+ events.push({ type: "reasoning", text: reasoning });
115
+ }
116
+ if (typeof delta.content === "string" && delta.content.length > 0) {
117
+ events.push({ type: "text", text: delta.content });
118
+ }
119
+ if (Array.isArray(delta.tool_calls)) {
120
+ for (const call of delta.tool_calls) {
121
+ if (!call || typeof call !== "object") {
122
+ continue;
123
+ }
124
+ const index = typeof call.index === "number" ? call.index : 0;
125
+ const fn = call.function && typeof call.function === "object" ? call.function : void 0;
126
+ const id = typeof call.id === "string" ? call.id : void 0;
127
+ const name = typeof fn?.name === "string" ? fn.name : void 0;
128
+ if (id !== void 0 || name !== void 0) {
129
+ events.push({
130
+ type: "tool_call_start",
131
+ index,
132
+ id,
133
+ name
134
+ });
135
+ }
136
+ const args = fn?.arguments;
137
+ if (typeof args === "string" && args.length > 0) {
138
+ events.push({
139
+ type: "tool_call_delta",
140
+ index,
141
+ argumentsDelta: args
142
+ });
143
+ }
114
144
  }
115
145
  }
116
146
  }
117
- }
118
- if (choice.finish_reason) {
119
- events.push({ type: "finish", reason: choice.finish_reason });
147
+ if (typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
148
+ events.push({ type: "finish", reason: choice.finish_reason });
149
+ }
120
150
  }
121
151
  return events;
122
152
  }
@@ -142,6 +172,28 @@ async function* decodeChunks(source) {
142
172
  async function* sseData(source) {
143
173
  let buffer = "";
144
174
  let dataLines = [];
175
+ const addLine = (line) => {
176
+ if (line[0] === ":") {
177
+ return;
178
+ }
179
+ const separator = line.indexOf(":");
180
+ const field = separator === -1 ? line : line.slice(0, separator);
181
+ let value = separator === -1 ? "" : line.slice(separator + 1);
182
+ if (value.startsWith(" ")) {
183
+ value = value.slice(1);
184
+ }
185
+ if (field === "data") {
186
+ dataLines.push(value);
187
+ }
188
+ };
189
+ const finishEvent = () => {
190
+ if (dataLines.length === 0) {
191
+ return void 0;
192
+ }
193
+ const data2 = dataLines.join("\n");
194
+ dataLines = [];
195
+ return data2;
196
+ };
145
197
  for await (const text of decodeChunks(source)) {
146
198
  buffer += text;
147
199
  let newline;
@@ -149,39 +201,39 @@ async function* sseData(source) {
149
201
  const line = buffer.slice(0, newline).replace(/\r$/, "");
150
202
  buffer = buffer.slice(newline + 1);
151
203
  if (line === "") {
152
- if (dataLines.length > 0) {
153
- yield dataLines.join("\n");
154
- dataLines = [];
204
+ const data2 = finishEvent();
205
+ if (data2 !== void 0) {
206
+ yield data2;
155
207
  }
156
208
  continue;
157
209
  }
158
- if (line[0] === ":") {
159
- continue;
160
- }
161
- if (line.startsWith("data:")) {
162
- dataLines.push(line.slice(5).replace(/^ /, ""));
163
- }
210
+ addLine(line);
164
211
  }
165
212
  }
166
213
  const last = buffer.replace(/\r$/, "");
167
- if (last.startsWith("data:")) {
168
- dataLines.push(last.slice(5).replace(/^ /, ""));
214
+ if (last !== "") {
215
+ addLine(last);
169
216
  }
170
- if (dataLines.length > 0) {
171
- yield dataLines.join("\n");
217
+ const data = finishEvent();
218
+ if (data !== void 0) {
219
+ yield data;
172
220
  }
173
221
  }
174
222
 
175
223
  // src/parse.ts
176
224
  async function* parseWith(source, map) {
177
225
  for await (const data of sseData(source)) {
178
- if (data === "[DONE]") {
226
+ if (isDone(data)) {
179
227
  return;
180
228
  }
181
229
  let payload;
182
230
  try {
183
231
  payload = JSON.parse(data);
184
- } catch {
232
+ } catch (error) {
233
+ const trimmed = data.trimStart();
234
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
235
+ yield malformedJsonEvent(error);
236
+ }
185
237
  continue;
186
238
  }
187
239
  for (const event of map(payload)) {
@@ -198,13 +250,17 @@ function parseAnthropicStream(source) {
198
250
  async function* parseGeminiStream(source) {
199
251
  const state = { toolIndex: 0 };
200
252
  for await (const data of sseData(source)) {
201
- if (data === "[DONE]") {
253
+ if (isDone(data)) {
202
254
  return;
203
255
  }
204
256
  let payload;
205
257
  try {
206
258
  payload = JSON.parse(data);
207
- } catch {
259
+ } catch (error) {
260
+ const trimmed = data.trimStart();
261
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
262
+ yield malformedJsonEvent(error);
263
+ }
208
264
  continue;
209
265
  }
210
266
  for (const event of mapGemini(payload, state)) {
@@ -222,6 +278,18 @@ function parseStream(source, provider) {
222
278
  return parseGeminiStream(source);
223
279
  }
224
280
  }
281
+ function malformedJsonEvent(error) {
282
+ return {
283
+ type: "error",
284
+ error: {
285
+ type: "malformed_json",
286
+ message: error instanceof Error ? error.message : String(error)
287
+ }
288
+ };
289
+ }
290
+ function isDone(data) {
291
+ return data.trim() === "[DONE]";
292
+ }
225
293
 
226
294
  // src/collect.ts
227
295
  async function collectStream(events) {
package/dist/index.js.map CHANGED
@@ -1 +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","../src/message.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 === 'thinking_delta' &&\n typeof delta.thinking === 'string'\n ) {\n events.push({ type: 'reasoning', text: delta.thinking });\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 // Gemini flags a thinking part with `thought: true`.\n events.push({\n type: part.thought === true ? 'reasoning' : 'text',\n text: part.text,\n });\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 // Reasoning models on OpenAI-compatible endpoints (e.g. DeepSeek R1) stream\n // their thinking in `reasoning_content` (some use `reasoning`).\n const reasoning = delta.reasoning_content ?? delta.reasoning;\n if (typeof reasoning === 'string' && reasoning.length > 0) {\n events.push({ type: 'reasoning', text: reasoning });\n }\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 reasoning = '';\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 'reasoning':\n reasoning += 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 reasoning,\n toolCalls: order.map((index) => byIndex.get(index)!),\n finishReason,\n };\n}\n","import type { CollectedMessage } from './types.ts';\n\n/**\n * An assistant message in OpenAI Chat Completions shape — the canonical \"hub\"\n * format. Append it to your message history to continue the conversation, or\n * pass it to `llm-messages` to port it to Anthropic or Gemini.\n */\nexport interface AssistantMessage {\n role: 'assistant';\n /** The assistant text, or `null` when the turn was only tool calls. */\n content: string | null;\n /** Present only when the turn produced tool calls. */\n tool_calls?: AssistantToolCall[];\n}\n\nexport interface AssistantToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string };\n}\n\n/**\n * Turn a {@link CollectedMessage} (from {@link collectStream}) into a standard\n * assistant message you can put back into a conversation.\n *\n * Output is the OpenAI Chat Completions shape, which is the format `llm-messages`\n * treats as canonical — so this composes directly with its `toAnthropic` /\n * `toGemini` converters. Tool calls without an id (e.g. from Gemini) get a\n * stable synthetic `call_<n>` id. Reasoning is intentionally omitted: it is not\n * part of the portable assistant message.\n *\n * @example\n * ```ts\n * const collected = await collectStream(parseOpenAIStream(res.body));\n * const message = toAssistantMessage(collected);\n * history.push(message); // or: toAnthropic([...history, message])\n * ```\n */\nexport function toAssistantMessage(\n collected: CollectedMessage,\n): AssistantMessage {\n const message: AssistantMessage = {\n role: 'assistant',\n content: collected.text.length > 0 ? collected.text : null,\n };\n\n if (collected.toolCalls.length > 0) {\n message.tool_calls = collected.toolCalls.map((call, position) => ({\n id: call.id ?? `call_${position}`,\n type: 'function',\n function: {\n name: call.name ?? '',\n arguments: call.arguments.length > 0 ? call.arguments : '{}',\n },\n }));\n }\n\n return message;\n}\n"],"mappings":"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","names":[]}
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","../src/message.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 const index = blockIndex(event.index);\n if (index === undefined) {\n break;\n }\n events.push({\n type: 'tool_call_start',\n index,\n id: typeof block.id === 'string' ? block.id : undefined,\n name: typeof block.name === 'string' ? block.name : undefined,\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 === 'thinking_delta' &&\n typeof delta.thinking === 'string'\n ) {\n events.push({ type: 'reasoning', text: delta.thinking });\n } else if (\n delta?.type === 'input_json_delta' &&\n typeof delta.partial_json === 'string'\n ) {\n const index = blockIndex(event.index);\n if (index === undefined) {\n break;\n }\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: delta.partial_json,\n });\n }\n break;\n }\n case 'message_delta': {\n const reason = event.delta?.stop_reason;\n if (typeof reason === 'string' && reason.length > 0) {\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\nfunction blockIndex(index: unknown): number | undefined {\n return typeof index === 'number' && Number.isInteger(index) && index >= 0\n ? index\n : undefined;\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 (!part || typeof part !== 'object') {\n continue;\n }\n\n if (typeof part.text === 'string' && part.text.length > 0) {\n // Gemini flags a thinking part with `thought: true`.\n events.push({\n type: part.thought === true ? 'reasoning' : 'text',\n text: part.text,\n });\n }\n const functionCall = part.functionCall;\n if (\n functionCall &&\n typeof functionCall === 'object' &&\n !Array.isArray(functionCall) &&\n typeof functionCall.name === 'string' &&\n functionCall.name.length > 0\n ) {\n const index = state.toolIndex++;\n events.push({\n type: 'tool_call_start',\n index,\n name: functionCall.name,\n });\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: JSON.stringify(functionCall.args ?? {}),\n });\n }\n }\n }\n\n if (\n typeof candidate.finishReason === 'string' &&\n candidate.finishReason.length > 0\n ) {\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 `choices[].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 choices = Array.isArray(chunk?.choices) ? chunk.choices : [];\n if (choices.length === 0) {\n return events;\n }\n\n for (const choice of choices) {\n if (!choice || typeof choice !== 'object') {\n continue;\n }\n\n const delta = choice.delta;\n if (delta && typeof delta === 'object') {\n // Reasoning models on OpenAI-compatible endpoints (e.g. DeepSeek R1)\n // stream their thinking in `reasoning_content` (some use `reasoning`).\n const reasoning = delta.reasoning_content ?? delta.reasoning;\n if (typeof reasoning === 'string' && reasoning.length > 0) {\n events.push({ type: 'reasoning', text: reasoning });\n }\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 if (!call || typeof call !== 'object') {\n continue;\n }\n\n const index = typeof call.index === 'number' ? call.index : 0;\n const fn =\n call.function && typeof call.function === 'object'\n ? call.function\n : undefined;\n const id = typeof call.id === 'string' ? call.id : undefined;\n const name = typeof fn?.name === 'string' ? fn.name : undefined;\n\n if (id !== undefined || name !== undefined) {\n events.push({\n type: 'tool_call_start',\n index,\n id,\n name,\n });\n }\n const args = fn?.arguments;\n if (typeof args === 'string' && args.length > 0) {\n events.push({\n type: 'tool_call_delta',\n index,\n argumentsDelta: args,\n });\n }\n }\n }\n }\n\n if (\n typeof choice.finish_reason === 'string' &&\n choice.finish_reason.length > 0\n ) {\n events.push({ type: 'finish', reason: choice.finish_reason });\n }\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 const addLine = (line: string): void => {\n if (line[0] === ':') {\n return; // comment\n }\n\n const separator = line.indexOf(':');\n const field = separator === -1 ? line : line.slice(0, separator);\n let value = separator === -1 ? '' : line.slice(separator + 1);\n if (value.startsWith(' ')) {\n value = value.slice(1);\n }\n\n if (field === 'data') {\n dataLines.push(value);\n }\n };\n\n const finishEvent = (): string | undefined => {\n if (dataLines.length === 0) {\n return undefined;\n }\n const data = dataLines.join('\\n');\n dataLines = [];\n return data;\n };\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 const data = finishEvent();\n if (data !== undefined) {\n yield data;\n }\n continue;\n }\n addLine(line);\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.replace(/\\r$/, '');\n if (last !== '') {\n addLine(last);\n }\n const data = finishEvent();\n if (data !== undefined) {\n yield data;\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 (isDone(data)) {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch (error) {\n const trimmed = data.trimStart();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n yield malformedJsonEvent(error);\n }\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 (isDone(data)) {\n return;\n }\n let payload: unknown;\n try {\n payload = JSON.parse(data);\n } catch (error) {\n const trimmed = data.trimStart();\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n yield malformedJsonEvent(error);\n }\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\nfunction malformedJsonEvent(error: unknown): StreamEvent {\n return {\n type: 'error',\n error: {\n type: 'malformed_json',\n message: error instanceof Error ? error.message : String(error),\n },\n };\n}\n\nfunction isDone(data: string): boolean {\n return data.trim() === '[DONE]';\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 reasoning = '';\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 'reasoning':\n reasoning += 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 reasoning,\n toolCalls: order.map((index) => byIndex.get(index)!),\n finishReason,\n };\n}\n","import type { CollectedMessage } from './types.ts';\n\n/**\n * An assistant message in OpenAI Chat Completions shape — the canonical \"hub\"\n * format. Append it to your message history to continue the conversation, or\n * pass it to `llm-messages` to port it to Anthropic or Gemini.\n */\nexport interface AssistantMessage {\n role: 'assistant';\n /** The assistant text, or `null` when the turn was only tool calls. */\n content: string | null;\n /** Present only when the turn produced tool calls. */\n tool_calls?: AssistantToolCall[];\n}\n\nexport interface AssistantToolCall {\n id: string;\n type: 'function';\n function: { name: string; arguments: string };\n}\n\n/**\n * Turn a {@link CollectedMessage} (from {@link collectStream}) into a standard\n * assistant message you can put back into a conversation.\n *\n * Output is the OpenAI Chat Completions shape, which is the format `llm-messages`\n * treats as canonical — so this composes directly with its `toAnthropic` /\n * `toGemini` converters. Tool calls without an id (e.g. from Gemini) get a\n * stable synthetic `call_<n>` id. Reasoning is intentionally omitted: it is not\n * part of the portable assistant message.\n *\n * @example\n * ```ts\n * const collected = await collectStream(parseOpenAIStream(res.body));\n * const message = toAssistantMessage(collected);\n * history.push(message); // or: toAnthropic([...history, message])\n * ```\n */\nexport function toAssistantMessage(\n collected: CollectedMessage,\n): AssistantMessage {\n const message: AssistantMessage = {\n role: 'assistant',\n content: collected.text.length > 0 ? collected.text : null,\n };\n\n if (collected.toolCalls.length > 0) {\n message.tool_calls = collected.toolCalls.map((call, position) => ({\n id: call.id ?? `call_${position}`,\n type: 'function',\n function: {\n name: call.name ?? '',\n arguments: call.arguments.length > 0 ? call.arguments : '{}',\n },\n }));\n }\n\n return message;\n}\n"],"mappings":"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","names":["data"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-sse",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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",
@@ -73,7 +73,7 @@
73
73
  "eslint": "^10.4.1",
74
74
  "prettier": "^3.4.2",
75
75
  "tsup": "^8.3.5",
76
- "typescript": "^5.7.2",
76
+ "typescript": "^6.0.3",
77
77
  "typescript-eslint": "^8.60.0",
78
78
  "vitest": "^4.1.8"
79
79
  }