llm-sse 0.4.2 → 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,29 @@ 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
+
19
+ ## [0.4.3] - 2026-06-05
20
+
21
+ ### Added
22
+
23
+ - Added a public cross-provider stream fixture corpus for OpenAI, Anthropic and
24
+ Gemini weather-tool calls.
25
+ - Added tests that parse the corpus and compare normalized `StreamEvent[]` and
26
+ collected message outputs, including byte-split stream boundaries.
27
+ - Published the `fixtures/` directory in the npm package for downstream parser
28
+ and agent-loop tests.
29
+
7
30
  ## [0.4.2] - 2026-06-04
8
31
 
9
32
  ### Changed
package/README.md CHANGED
@@ -9,6 +9,9 @@
9
9
 
10
10
  > Parse streaming responses from OpenAI, Anthropic, Gemini and OpenAI-compatible providers into one unified event format. Text, reasoning and tool-call deltas, handled. **Zero dependencies.**
11
11
 
12
+ Security posture is tracked in [docs/security-posture.md](./docs/security-posture.md),
13
+ including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.
14
+
12
15
  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.
13
16
 
14
17
  ```ts
@@ -33,6 +36,7 @@ for await (const event of parseOpenAIStream(res.body)) {
33
36
  - **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.
34
37
  - **Tool calls just accumulate.** Streamed JSON argument fragments carry an `index`; concatenate by index (or let `collectStream` do it) to get the full call.
35
38
  - **Correct SSE framing.** Robust to chunk boundaries splitting a line or event mid-way, CRLF, multi-line `data:` fields, comments and keep-alives.
39
+ - **Fixture-backed provider coverage.** Public OpenAI, Anthropic and Gemini `.sse` fixtures exercise text, reasoning, tool-call arguments and finish reasons.
36
40
  - **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.
37
41
  - **Zero dependencies**, ESM + CJS, fully typed.
38
42
 
@@ -99,10 +103,32 @@ const claudeBody = toAnthropic([...history, message]); // continue on Claude
99
103
 
100
104
  The underlying SSE parser, exported for advanced use: yields the `data` payload of each event as a string.
101
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
+
102
114
  ## Tool calls
103
115
 
104
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.
105
117
 
118
+ ## Fixture corpus
119
+
120
+ The package includes a small public fixture corpus under [`fixtures/`](./fixtures):
121
+
122
+ - `openai-weather-tool.sse`
123
+ - `anthropic-weather-tool.sse`
124
+ - `gemini-weather-tool.sse`
125
+ - expected normalized events and collected messages under `fixtures/expected/`
126
+
127
+ Each fixture describes the same semantic turn: reasoning, visible text, a
128
+ `get_weather` tool call, JSON arguments and provider-specific finish reason.
129
+ The tests parse the fixtures directly, including byte-split stream boundaries,
130
+ so contributors can change parsers with a stable cross-provider contract.
131
+
106
132
  ## Related
107
133
 
108
134
  - [`tool-schema`](https://www.npmjs.com/package/tool-schema) — convert a JSON Schema into OpenAI / Anthropic / Gemini / MCP tool schemas.
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":";AASO,SAAS,aAAa,OAA2B;AACtD,QAAM,SAAwB,CAAC;AAE/B,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK,uBAAuB;AAC1B,YAAM,QAAQ,MAAM;AACpB,UAAI,OAAO,SAAS,YAAY;AAC9B,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN,OAAO,MAAM,SAAS;AAAA,UACtB,IAAI,MAAM;AAAA,UACV,MAAM,MAAM;AAAA,QACd,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAAA,IACA,KAAK,uBAAuB;AAC1B,YAAM,QAAQ,MAAM;AACpB,UAAI,OAAO,SAAS,gBAAgB,OAAO,MAAM,SAAS,UAAU;AAClE,eAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,MAAM,KAAK,CAAC;AAAA,MAChD,WACE,OAAO,SAAS,oBAChB,OAAO,MAAM,aAAa,UAC1B;AACA,eAAO,KAAK,EAAE,MAAM,aAAa,MAAM,MAAM,SAAS,CAAC;AAAA,MACzD,WACE,OAAO,SAAS,sBAChB,OAAO,MAAM,iBAAiB,UAC9B;AACA,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN,OAAO,MAAM,SAAS;AAAA,UACtB,gBAAgB,MAAM;AAAA,QACxB,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAAA,IACA,KAAK,iBAAiB;AACpB,YAAM,SAAS,MAAM,OAAO;AAC5B,UAAI,QAAQ;AACV,eAAO,KAAK,EAAE,MAAM,UAAU,OAAO,CAAC;AAAA,MACxC;AACA;AAAA,IACF;AAAA,IACA,KAAK,SAAS;AACZ,aAAO,KAAK,EAAE,MAAM,SAAS,OAAO,MAAM,SAAS,MAAM,CAAC;AAC1D;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;;;AC7CO,SAAS,UAAU,OAAY,OAAmC;AACvE,QAAM,SAAwB,CAAC;AAC/B,QAAM,YAAY,OAAO,aAAa,CAAC;AACvC,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,UAAU,SAAS;AACjC,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,eAAW,QAAQ,OAAO;AACxB,UAAI,OAAO,KAAK,SAAS,YAAY,KAAK,KAAK,SAAS,GAAG;AAEzD,eAAO,KAAK;AAAA,UACV,MAAM,KAAK,YAAY,OAAO,cAAc;AAAA,UAC5C,MAAM,KAAK;AAAA,QACb,CAAC;AAAA,MACH;AACA,UAAI,KAAK,cAAc;AACrB,cAAM,QAAQ,MAAM;AACpB,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,MAAM,KAAK,aAAa;AAAA,QAC1B,CAAC;AACD,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,gBAAgB,KAAK,UAAU,KAAK,aAAa,QAAQ,CAAC,CAAC;AAAA,QAC7D,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,MAAI,UAAU,cAAc;AAC1B,WAAO,KAAK,EAAE,MAAM,UAAU,QAAQ,UAAU,aAAa,CAAC;AAAA,EAChE;AACA,SAAO;AACT;;;AC3CO,SAAS,UAAU,OAA2B;AACnD,QAAM,SAAwB,CAAC;AAC/B,QAAM,SAAS,OAAO,UAAU,CAAC;AACjC,MAAI,CAAC,QAAQ;AACX,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,OAAO;AACrB,MAAI,OAAO;AAGT,UAAM,YAAY,MAAM,qBAAqB,MAAM;AACnD,QAAI,OAAO,cAAc,YAAY,UAAU,SAAS,GAAG;AACzD,aAAO,KAAK,EAAE,MAAM,aAAa,MAAM,UAAU,CAAC;AAAA,IACpD;AACA,QAAI,OAAO,MAAM,YAAY,YAAY,MAAM,QAAQ,SAAS,GAAG;AACjE,aAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC;AAAA,IACnD;AACA,QAAI,MAAM,QAAQ,MAAM,UAAU,GAAG;AACnC,iBAAW,QAAQ,MAAM,YAAY;AACnC,cAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAC5D,YAAI,KAAK,OAAO,UAAa,KAAK,UAAU,SAAS,QAAW;AAC9D,iBAAO,KAAK;AAAA,YACV,MAAM;AAAA,YACN;AAAA,YACA,IAAI,KAAK;AAAA,YACT,MAAM,KAAK,UAAU;AAAA,UACvB,CAAC;AAAA,QACH;AACA,cAAM,OAAO,KAAK,UAAU;AAC5B,YAAI,OAAO,SAAS,YAAY,KAAK,SAAS,GAAG;AAC/C,iBAAO,KAAK,EAAE,MAAM,mBAAmB,OAAO,gBAAgB,KAAK,CAAC;AAAA,QACtE;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,OAAO,eAAe;AACxB,WAAO,KAAK,EAAE,MAAM,UAAU,QAAQ,OAAO,cAAc,CAAC;AAAA,EAC9D;AACA,SAAO;AACT;;;AC3CA,gBAAgB,aAAa,QAA6C;AACxE,QAAM,UAAU,IAAI,YAAY;AAChC,mBAAiB,SAAS,QAAQ;AAChC,QAAI,OAAO,UAAU,UAAU;AAC7B,YAAM;AAAA,IACR,OAAO;AACL,YAAM,OAAO,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;AACnD,UAAI,MAAM;AACR,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,QAAM,OAAO,QAAQ,OAAO;AAC5B,MAAI,MAAM;AACR,UAAM;AAAA,EACR;AACF;AAWA,gBAAuB,QAAQ,QAA6C;AAC1E,MAAI,SAAS;AACb,MAAI,YAAsB,CAAC;AAE3B,mBAAiB,QAAQ,aAAa,MAAM,GAAG;AAC7C,cAAU;AAEV,QAAI;AACJ,YAAQ,UAAU,OAAO,QAAQ,IAAI,OAAO,IAAI;AAC9C,YAAM,OAAO,OAAO,MAAM,GAAG,OAAO,EAAE,QAAQ,OAAO,EAAE;AACvD,eAAS,OAAO,MAAM,UAAU,CAAC;AAEjC,UAAI,SAAS,IAAI;AAEf,YAAI,UAAU,SAAS,GAAG;AACxB,gBAAM,UAAU,KAAK,IAAI;AACzB,sBAAY,CAAC;AAAA,QACf;AACA;AAAA,MACF;AACA,UAAI,KAAK,CAAC,MAAM,KAAK;AACnB;AAAA,MACF;AACA,UAAI,KAAK,WAAW,OAAO,GAAG;AAC5B,kBAAU,KAAK,KAAK,MAAM,CAAC,EAAE,QAAQ,MAAM,EAAE,CAAC;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,OAAO,QAAQ,OAAO,EAAE;AACrC,MAAI,KAAK,WAAW,OAAO,GAAG;AAC5B,cAAU,KAAK,KAAK,MAAM,CAAC,EAAE,QAAQ,MAAM,EAAE,CAAC;AAAA,EAChD;AACA,MAAI,UAAU,SAAS,GAAG;AACxB,UAAM,UAAU,KAAK,IAAI;AAAA,EAC3B;AACF;;;AChEA,gBAAgB,UACd,QACA,KAC6B;AAC7B,mBAAiB,QAAQ,QAAQ,MAAM,GAAG;AACxC,QAAI,SAAS,UAAU;AACrB;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,IAAI,OAAO,GAAG;AAChC,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAGO,SAAS,kBACd,QAC6B;AAC7B,SAAO,UAAU,QAAQ,SAAS;AACpC;AAGO,SAAS,qBACd,QAC6B;AAC7B,SAAO,UAAU,QAAQ,YAAY;AACvC;AAGA,gBAAuB,kBACrB,QAC6B;AAC7B,QAAM,QAAQ,EAAE,WAAW,EAAE;AAC7B,mBAAiB,QAAQ,QAAQ,MAAM,GAAG;AACxC,QAAI,SAAS,UAAU;AACrB;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,QAAQ;AACN;AAAA,IACF;AACA,eAAW,SAAS,UAAU,SAAS,KAAK,GAAG;AAC7C,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAGO,SAAS,YACd,QACA,UAC6B;AAC7B,UAAQ,UAAU;AAAA,IAChB,KAAK;AACH,aAAO,kBAAkB,MAAM;AAAA,IACjC,KAAK;AACH,aAAO,qBAAqB,MAAM;AAAA,IACpC,KAAK;AACH,aAAO,kBAAkB,MAAM;AAAA,EACnC;AACF;;;ACxDA,eAAsB,cACpB,QAC2B;AAC3B,MAAI,OAAO;AACX,MAAI,YAAY;AAChB,MAAI;AACJ,QAAM,UAAU,oBAAI,IAA+B;AACnD,QAAM,QAAkB,CAAC;AAEzB,QAAM,SAAS,CAAC,UAAqC;AACnD,QAAI,OAAO,QAAQ,IAAI,KAAK;AAC5B,QAAI,CAAC,MAAM;AACT,aAAO,EAAE,OAAO,WAAW,GAAG;AAC9B,cAAQ,IAAI,OAAO,IAAI;AACvB,YAAM,KAAK,KAAK;AAAA,IAClB;AACA,WAAO;AAAA,EACT;AAEA,mBAAiB,SAAS,QAAQ;AAChC,YAAQ,MAAM,MAAM;AAAA,MAClB,KAAK;AACH,gBAAQ,MAAM;AACd;AAAA,MACF,KAAK;AACH,qBAAa,MAAM;AACnB;AAAA,MACF,KAAK,mBAAmB;AACtB,cAAM,OAAO,OAAO,MAAM,KAAK;AAC/B,YAAI,MAAM,OAAO,QAAW;AAC1B,eAAK,KAAK,MAAM;AAAA,QAClB;AACA,YAAI,MAAM,SAAS,QAAW;AAC5B,eAAK,OAAO,MAAM;AAAA,QACpB;AACA;AAAA,MACF;AAAA,MACA,KAAK;AACH,eAAO,MAAM,KAAK,EAAE,aAAa,MAAM;AACvC;AAAA,MACF,KAAK;AACH,uBAAe,MAAM;AACrB;AAAA,MACF,KAAK;AACH;AAAA,IACJ;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,MAAM,IAAI,CAAC,UAAU,QAAQ,IAAI,KAAK,CAAE;AAAA,IACnD;AAAA,EACF;AACF;;;ACnCO,SAAS,mBACd,WACkB;AAClB,QAAM,UAA4B;AAAA,IAChC,MAAM;AAAA,IACN,SAAS,UAAU,KAAK,SAAS,IAAI,UAAU,OAAO;AAAA,EACxD;AAEA,MAAI,UAAU,UAAU,SAAS,GAAG;AAClC,YAAQ,aAAa,UAAU,UAAU,IAAI,CAAC,MAAM,cAAc;AAAA,MAChE,IAAI,KAAK,MAAM,QAAQ,QAAQ;AAAA,MAC/B,MAAM;AAAA,MACN,UAAU;AAAA,QACR,MAAM,KAAK,QAAQ;AAAA,QACnB,WAAW,KAAK,UAAU,SAAS,IAAI,KAAK,YAAY;AAAA,MAC1D;AAAA,IACF,EAAE;AAAA,EACJ;AAEA,SAAO;AACT;","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"]}
@@ -0,0 +1,16 @@
1
+ # Stream Fixture Corpus
2
+
3
+ This corpus contains small, deterministic Server-Sent Events streams for the
4
+ same semantic turn across providers:
5
+
6
+ - OpenAI / OpenAI-compatible chat completions
7
+ - Anthropic Messages streaming
8
+ - Gemini `streamGenerateContent?alt=sse`
9
+
10
+ Each `.sse` file is paired with expected normalized `StreamEvent[]` and
11
+ `CollectedMessage` JSON. The fixtures are safe for public CI: they contain no
12
+ API keys, no user data and no live provider responses.
13
+
14
+ Use them when changing parsers, comparing providers or writing downstream tests
15
+ that need stable examples of text, reasoning, tool-call arguments and finish
16
+ reasons.
@@ -0,0 +1,39 @@
1
+ event: message_start
2
+ data: {"type":"message_start","message":{"id":"msg_fixture_anthropic_weather","type":"message","role":"assistant","content":[]}}
3
+
4
+ event: content_block_start
5
+ data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}
6
+
7
+ event: content_block_delta
8
+ data: {"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"Need weather lookup. "}}
9
+
10
+ event: content_block_stop
11
+ data: {"type":"content_block_stop","index":0}
12
+
13
+ event: content_block_start
14
+ data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}
15
+
16
+ event: content_block_delta
17
+ data: {"type":"content_block_delta","index":1,"delta":{"type":"text_delta","text":"Let me check Santiago. "}}
18
+
19
+ event: content_block_stop
20
+ data: {"type":"content_block_stop","index":1}
21
+
22
+ event: content_block_start
23
+ data: {"type":"content_block_start","index":2,"content_block":{"type":"tool_use","id":"toolu_weather_1","name":"get_weather","input":{}}}
24
+
25
+ event: content_block_delta
26
+ data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"{\"city\":"}}
27
+
28
+ event: content_block_delta
29
+ data: {"type":"content_block_delta","index":2,"delta":{"type":"input_json_delta","partial_json":"\"Santiago\",\"units\":\"metric\"}"}}
30
+
31
+ event: content_block_stop
32
+ data: {"type":"content_block_stop","index":2}
33
+
34
+ event: message_delta
35
+ data: {"type":"message_delta","delta":{"stop_reason":"tool_use"}}
36
+
37
+ event: message_stop
38
+ data: {"type":"message_stop"}
39
+
@@ -0,0 +1,17 @@
1
+ [
2
+ { "type": "reasoning", "text": "Need weather lookup. " },
3
+ { "type": "text", "text": "Let me check Santiago. " },
4
+ {
5
+ "type": "tool_call_start",
6
+ "index": 2,
7
+ "id": "toolu_weather_1",
8
+ "name": "get_weather"
9
+ },
10
+ { "type": "tool_call_delta", "index": 2, "argumentsDelta": "{\"city\":" },
11
+ {
12
+ "type": "tool_call_delta",
13
+ "index": 2,
14
+ "argumentsDelta": "\"Santiago\",\"units\":\"metric\"}"
15
+ },
16
+ { "type": "finish", "reason": "tool_use" }
17
+ ]
@@ -0,0 +1,13 @@
1
+ {
2
+ "text": "Let me check Santiago. ",
3
+ "reasoning": "Need weather lookup. ",
4
+ "toolCalls": [
5
+ {
6
+ "index": 2,
7
+ "id": "toolu_weather_1",
8
+ "name": "get_weather",
9
+ "arguments": "{\"city\":\"Santiago\",\"units\":\"metric\"}"
10
+ }
11
+ ],
12
+ "finishReason": "tool_use"
13
+ }
@@ -0,0 +1,11 @@
1
+ [
2
+ { "type": "reasoning", "text": "Need weather lookup. " },
3
+ { "type": "text", "text": "Let me check Santiago. " },
4
+ { "type": "tool_call_start", "index": 0, "name": "get_weather" },
5
+ {
6
+ "type": "tool_call_delta",
7
+ "index": 0,
8
+ "argumentsDelta": "{\"city\":\"Santiago\",\"units\":\"metric\"}"
9
+ },
10
+ { "type": "finish", "reason": "STOP" }
11
+ ]
@@ -0,0 +1,12 @@
1
+ {
2
+ "text": "Let me check Santiago. ",
3
+ "reasoning": "Need weather lookup. ",
4
+ "toolCalls": [
5
+ {
6
+ "index": 0,
7
+ "name": "get_weather",
8
+ "arguments": "{\"city\":\"Santiago\",\"units\":\"metric\"}"
9
+ }
10
+ ],
11
+ "finishReason": "STOP"
12
+ }
@@ -0,0 +1,17 @@
1
+ [
2
+ { "type": "reasoning", "text": "Need weather lookup. " },
3
+ { "type": "text", "text": "Let me check Santiago. " },
4
+ {
5
+ "type": "tool_call_start",
6
+ "index": 0,
7
+ "id": "call_weather_1",
8
+ "name": "get_weather"
9
+ },
10
+ { "type": "tool_call_delta", "index": 0, "argumentsDelta": "{\"city\":" },
11
+ {
12
+ "type": "tool_call_delta",
13
+ "index": 0,
14
+ "argumentsDelta": "\"Santiago\",\"units\":\"metric\"}"
15
+ },
16
+ { "type": "finish", "reason": "tool_calls" }
17
+ ]
@@ -0,0 +1,13 @@
1
+ {
2
+ "text": "Let me check Santiago. ",
3
+ "reasoning": "Need weather lookup. ",
4
+ "toolCalls": [
5
+ {
6
+ "index": 0,
7
+ "id": "call_weather_1",
8
+ "name": "get_weather",
9
+ "arguments": "{\"city\":\"Santiago\",\"units\":\"metric\"}"
10
+ }
11
+ ],
12
+ "finishReason": "tool_calls"
13
+ }
@@ -0,0 +1,6 @@
1
+ data: {"candidates":[{"content":{"role":"model","parts":[{"text":"Need weather lookup. ","thought":true}]}}]}
2
+
3
+ data: {"candidates":[{"content":{"role":"model","parts":[{"text":"Let me check Santiago. "}]}}]}
4
+
5
+ data: {"candidates":[{"content":{"role":"model","parts":[{"functionCall":{"name":"get_weather","args":{"city":"Santiago","units":"metric"}}}]},"finishReason":"STOP"}]}
6
+
@@ -0,0 +1,14 @@
1
+ data: {"id":"chatcmpl_fixture_openai_weather","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"Need weather lookup. "}}]}
2
+
3
+ data: {"id":"chatcmpl_fixture_openai_weather","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Let me check Santiago. "}}]}
4
+
5
+ data: {"id":"chatcmpl_fixture_openai_weather","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_weather_1","type":"function","function":{"name":"get_weather","arguments":""}}]}}]}
6
+
7
+ data: {"id":"chatcmpl_fixture_openai_weather","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]}}]}
8
+
9
+ data: {"id":"chatcmpl_fixture_openai_weather","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"\"Santiago\",\"units\":\"metric\"}"}}]}}]}
10
+
11
+ data: {"id":"chatcmpl_fixture_openai_weather","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]}
12
+
13
+ data: [DONE]
14
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-sse",
3
- "version": "0.4.2",
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",
@@ -47,6 +47,7 @@
47
47
  },
48
48
  "files": [
49
49
  "dist",
50
+ "fixtures",
50
51
  "README.md",
51
52
  "LICENSE",
52
53
  "CHANGELOG.md"
@@ -72,7 +73,7 @@
72
73
  "eslint": "^10.4.1",
73
74
  "prettier": "^3.4.2",
74
75
  "tsup": "^8.3.5",
75
- "typescript": "^5.7.2",
76
+ "typescript": "^6.0.3",
76
77
  "typescript-eslint": "^8.60.0",
77
78
  "vitest": "^4.1.8"
78
79
  }