llm-sse 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/CODE_OF_CONDUCT.md +57 -0
- package/CONTRIBUTING.md +65 -0
- package/GOVERNANCE.md +71 -0
- package/README.md +99 -9
- package/ROADMAP.md +64 -0
- package/SECURITY.md +24 -0
- package/SUPPORT.md +38 -0
- package/dist/index.cjs +78 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -8
- package/dist/index.d.ts +11 -8
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -1
- package/docs/security-posture.md +20 -0
- package/fixtures/expected/openai-responses-weather-tool.events.json +17 -0
- package/fixtures/expected/openai-responses-weather-tool.message.json +13 -0
- package/fixtures/openai-responses-weather-tool.sse +17 -0
- package/package.json +16 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,29 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-07-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `parseOpenAIResponsesStream(source)` and the `openai-responses` dispatcher
|
|
14
|
+
target for typed OpenAI Responses API streams.
|
|
15
|
+
- Normalization for Responses API text, reasoning summaries, function-call
|
|
16
|
+
arguments, refusal text, completion, incomplete responses, and errors.
|
|
17
|
+
- A public Responses API weather-tool fixture with normalized event and
|
|
18
|
+
collected-message expectations.
|
|
19
|
+
- Public roadmap, governance, support channels, and a documented path from
|
|
20
|
+
contributor to maintainer.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Project documentation now links contribution, security, governance, support,
|
|
25
|
+
roadmap, and release resources from the README.
|
|
26
|
+
- The published tarball now includes those public project documents and is
|
|
27
|
+
installation-tested before release.
|
|
28
|
+
- Development tooling overrides `esbuild` to a patched release so a clean
|
|
29
|
+
install passes `npm audit` without changing the package's zero-dependency
|
|
30
|
+
runtime.
|
|
31
|
+
|
|
9
32
|
## [0.4.9] - 2026-06-29
|
|
10
33
|
|
|
11
34
|
### Changed
|
|
@@ -117,6 +140,16 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
117
140
|
and `Uint8Array` or string sources.
|
|
118
141
|
- Zero runtime dependencies; ESM + CJS builds with type declarations.
|
|
119
142
|
|
|
143
|
+
[0.5.0]: https://github.com/slegarraga/llm-sse/releases/tag/v0.5.0
|
|
144
|
+
[0.4.9]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.9
|
|
145
|
+
[0.4.8]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.8
|
|
146
|
+
[0.4.7]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.7
|
|
147
|
+
[0.4.6]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.6
|
|
148
|
+
[0.4.5]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.5
|
|
149
|
+
[0.4.4]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.4
|
|
150
|
+
[0.4.3]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.3
|
|
151
|
+
[0.4.2]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.2
|
|
152
|
+
[0.4.1]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.1
|
|
120
153
|
[0.4.0]: https://github.com/slegarraga/llm-sse/releases/tag/v0.4.0
|
|
121
154
|
[0.3.0]: https://github.com/slegarraga/llm-sse/releases/tag/v0.3.0
|
|
122
155
|
[0.2.0]: https://github.com/slegarraga/llm-sse/releases/tag/v0.2.0
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
+
orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment include:
|
|
18
|
+
|
|
19
|
+
- Demonstrating empathy and kindness toward other people
|
|
20
|
+
- Being respectful of differing opinions, viewpoints, and experiences
|
|
21
|
+
- Giving and gracefully accepting constructive feedback
|
|
22
|
+
- Accepting responsibility and apologizing to those affected by our mistakes
|
|
23
|
+
- Focusing on what is best for the overall community
|
|
24
|
+
|
|
25
|
+
Examples of unacceptable behavior include:
|
|
26
|
+
|
|
27
|
+
- The use of sexualized language or imagery, and sexual attention or advances
|
|
28
|
+
- Trolling, insulting or derogatory comments, and personal or political attacks
|
|
29
|
+
- Public or private harassment
|
|
30
|
+
- Publishing others' private information without their explicit permission
|
|
31
|
+
- Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
+
professional setting
|
|
33
|
+
|
|
34
|
+
## Enforcement Responsibilities
|
|
35
|
+
|
|
36
|
+
Project maintainers are responsible for clarifying and enforcing our standards of
|
|
37
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
38
|
+
response to any behavior that they deem inappropriate, threatening, offensive, or
|
|
39
|
+
harmful.
|
|
40
|
+
|
|
41
|
+
## Scope
|
|
42
|
+
|
|
43
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
44
|
+
an individual is officially representing the community in public spaces.
|
|
45
|
+
|
|
46
|
+
## Enforcement
|
|
47
|
+
|
|
48
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
49
|
+
reported to the maintainers at **sebastian@0a.cl**. All complaints will be
|
|
50
|
+
reviewed and investigated promptly and fairly. All maintainers are obligated to
|
|
51
|
+
respect the privacy and security of the reporter of any incident.
|
|
52
|
+
|
|
53
|
+
## Attribution
|
|
54
|
+
|
|
55
|
+
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
|
|
56
|
+
version 2.1, available at
|
|
57
|
+
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Contributing to llm-sse
|
|
2
|
+
|
|
3
|
+
Thanks for taking the time to contribute. This project aims to be a small,
|
|
4
|
+
dependable, zero-dependency building block, so the bar for changes is clarity
|
|
5
|
+
and correctness over breadth.
|
|
6
|
+
|
|
7
|
+
## Getting started
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
git clone https://github.com/slegarraga/llm-sse.git
|
|
11
|
+
cd llm-sse
|
|
12
|
+
npm install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Development workflow
|
|
16
|
+
|
|
17
|
+
Every change should keep the full check suite green:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm run typecheck # tsc --noEmit
|
|
21
|
+
npm run lint # eslint
|
|
22
|
+
npm test # vitest
|
|
23
|
+
npm run build # tsup (ESM + CJS + types)
|
|
24
|
+
npm run format # prettier --write
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Run `npm run test:watch` while developing.
|
|
28
|
+
|
|
29
|
+
## Pull requests
|
|
30
|
+
|
|
31
|
+
1. Fork the repo and create a branch from `main` (e.g. `fix/gemini-tool-index`).
|
|
32
|
+
2. Add or update tests. New behaviour without a test will not be merged.
|
|
33
|
+
3. Make sure `typecheck`, `lint`, `test` and `build` all pass.
|
|
34
|
+
4. Keep the public API surface small and documented with JSDoc.
|
|
35
|
+
5. Open a pull request describing the provider stream shape you are handling.
|
|
36
|
+
|
|
37
|
+
## Commit messages
|
|
38
|
+
|
|
39
|
+
This project uses [Conventional Commits](https://www.conventionalcommits.org/).
|
|
40
|
+
Examples:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
feat(anthropic): handle thinking_delta blocks
|
|
44
|
+
fix(sse): join multi-line data fields per spec
|
|
45
|
+
docs: clarify the tool-call delta model
|
|
46
|
+
test: cover chunk boundaries splitting an event
|
|
47
|
+
chore: bump dev dependencies
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The type drives the next version bump (`fix` -> patch, `feat` -> minor, a
|
|
51
|
+
`!` or `BREAKING CHANGE` footer -> major).
|
|
52
|
+
|
|
53
|
+
## Reporting bugs
|
|
54
|
+
|
|
55
|
+
Open an issue with a minimal reproduction: the raw stream bytes (or the `data:`
|
|
56
|
+
lines) from the provider, which provider produced them, and the `StreamEvent`s
|
|
57
|
+
you expected. A failing test case is the most useful form a bug report can take.
|
|
58
|
+
|
|
59
|
+
## Scope and philosophy
|
|
60
|
+
|
|
61
|
+
- Zero runtime dependencies. A dependency needs an exceptional justification.
|
|
62
|
+
- Parsing is total: malformed or non-JSON `data:` lines are skipped, never
|
|
63
|
+
thrown, so one bad keep-alive can't break a stream.
|
|
64
|
+
- Event mapping is grounded in official provider documentation. When you add or
|
|
65
|
+
change a rule, link the source in the PR.
|
package/GOVERNANCE.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Governance
|
|
2
|
+
|
|
3
|
+
`llm-sse` is an independent open-source project. Governance is designed to keep
|
|
4
|
+
the parser small and dependable while making it possible for trusted
|
|
5
|
+
contributors to take real ownership.
|
|
6
|
+
|
|
7
|
+
## Current maintainer
|
|
8
|
+
|
|
9
|
+
- [Sebastian Legarraga](https://github.com/slegarraga) — project direction,
|
|
10
|
+
releases, security response, and repository administration.
|
|
11
|
+
|
|
12
|
+
The project currently has a single maintainer. Expanding the maintainer group is
|
|
13
|
+
an explicit goal, not a prerequisite contributors must wait for.
|
|
14
|
+
|
|
15
|
+
## How decisions are made
|
|
16
|
+
|
|
17
|
+
- Bug fixes and small, backward-compatible improvements are decided through
|
|
18
|
+
pull-request review.
|
|
19
|
+
- New provider formats, normalized event variants, and breaking changes should
|
|
20
|
+
start with a public issue or discussion.
|
|
21
|
+
- Decisions prioritize primary provider documentation, reproducible fixtures,
|
|
22
|
+
interoperability, security, and maintenance cost.
|
|
23
|
+
- If reasonable contributors disagree, the maintainer records the tradeoff and
|
|
24
|
+
decision publicly rather than moving the discussion to a private channel.
|
|
25
|
+
|
|
26
|
+
## Roles
|
|
27
|
+
|
|
28
|
+
### Contributor
|
|
29
|
+
|
|
30
|
+
Anyone who reports a reproducible issue, improves documentation, supplies a
|
|
31
|
+
sanitized fixture, or lands a code change.
|
|
32
|
+
|
|
33
|
+
### Reviewer
|
|
34
|
+
|
|
35
|
+
A contributor who understands an area of the project and regularly helps review
|
|
36
|
+
changes or triage issues. Reviewers may be listed in the repository and invited
|
|
37
|
+
to relevant reviews.
|
|
38
|
+
|
|
39
|
+
### Maintainer
|
|
40
|
+
|
|
41
|
+
A trusted reviewer with sustained, constructive involvement who can merge
|
|
42
|
+
changes, manage issues, and help prepare releases. Maintainers are expected to:
|
|
43
|
+
|
|
44
|
+
- uphold the code of conduct and security policy;
|
|
45
|
+
- require tests and primary-source evidence for provider behavior changes;
|
|
46
|
+
- disclose conflicts of interest;
|
|
47
|
+
- avoid merging their own substantial changes without another review when a
|
|
48
|
+
second maintainer is available;
|
|
49
|
+
- preserve the project's zero-dependency and compatibility commitments.
|
|
50
|
+
|
|
51
|
+
Maintainer access is offered based on demonstrated judgment, reliability,
|
|
52
|
+
project need, and mutual agreement. It is never exchanged for sponsorship,
|
|
53
|
+
stars, or a fixed number of pull requests.
|
|
54
|
+
|
|
55
|
+
## Releases
|
|
56
|
+
|
|
57
|
+
Releases follow Semantic Versioning. The release workflow reruns type checking,
|
|
58
|
+
linting, tests, and the build before publishing with npm provenance. Breaking
|
|
59
|
+
changes require a migration note.
|
|
60
|
+
|
|
61
|
+
## Security and conduct
|
|
62
|
+
|
|
63
|
+
Security reports follow [SECURITY.md](./SECURITY.md). Community behavior follows
|
|
64
|
+
[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). A maintainer involved in a conduct
|
|
65
|
+
or security concern should recuse themselves when another trusted reviewer is
|
|
66
|
+
available.
|
|
67
|
+
|
|
68
|
+
## Changes to governance
|
|
69
|
+
|
|
70
|
+
Governance changes use the same public pull-request process as code changes and
|
|
71
|
+
should explain the problem they solve.
|
package/README.md
CHANGED
|
@@ -9,16 +9,64 @@
|
|
|
9
9
|
[](./LICENSE)
|
|
10
10
|
[](./package.json)
|
|
11
11
|
|
|
12
|
-
> Zero-dependency SSE parser that turns OpenAI, Anthropic, Gemini and
|
|
12
|
+
> Zero-dependency SSE parser that turns OpenAI Responses, OpenAI Chat Completions, Anthropic, Gemini and OpenAI-compatible streams into one unified event shape: text deltas, reasoning, tool-call fragments and finish reasons, handled the same way regardless of provider.
|
|
13
13
|
|
|
14
14
|
Security posture is tracked in [docs/security-posture.md](https://github.com/slegarraga/llm-sse/blob/main/docs/security-posture.md),
|
|
15
15
|
including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.
|
|
16
16
|
|
|
17
|
-
Each
|
|
17
|
+
Each API streams differently. OpenAI Responses sends typed `response.*` events,
|
|
18
|
+
OpenAI Chat Completions sends `choices[].delta` chunks, Anthropic sends typed
|
|
19
|
+
`content_block_*` / `message_*` events, and Gemini sends
|
|
20
|
+
`candidates[].content.parts`. `llm-sse` turns all of them into the same small
|
|
21
|
+
set of events, so your streaming UI or agent loop stays provider-agnostic.
|
|
22
|
+
|
|
23
|
+
## Project
|
|
24
|
+
|
|
25
|
+
- [Roadmap](./ROADMAP.md): compatibility, conformance, and sustainability priorities
|
|
26
|
+
- [Contributing](./CONTRIBUTING.md): local setup, tests, and pull-request expectations
|
|
27
|
+
- [Governance](./GOVERNANCE.md): decision process and the path to reviewer or maintainer
|
|
28
|
+
- [Support](./SUPPORT.md): where to ask questions or report reproducible bugs
|
|
29
|
+
- [Security](./SECURITY.md): private vulnerability reporting and supported versions
|
|
30
|
+
- [Changelog](./CHANGELOG.md): release history and migration notes
|
|
31
|
+
- [Code of Conduct](./CODE_OF_CONDUCT.md): community standards
|
|
18
32
|
|
|
19
33
|
## Quickstart
|
|
20
34
|
|
|
21
|
-
### OpenAI
|
|
35
|
+
### OpenAI Responses API
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { parseOpenAIResponsesStream } from 'llm-sse';
|
|
39
|
+
|
|
40
|
+
const res = await fetch('https://api.openai.com/v1/responses', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${key}`,
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model: process.env.OPENAI_MODEL,
|
|
48
|
+
input: 'What is the weather in Santiago?',
|
|
49
|
+
tools,
|
|
50
|
+
stream: true,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
for await (const event of parseOpenAIResponsesStream(res.body)) {
|
|
55
|
+
if (event.type === 'text') process.stdout.write(event.text);
|
|
56
|
+
if (event.type === 'tool_call_delta') {
|
|
57
|
+
process.stderr.write(event.argumentsDelta);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The parser follows the official
|
|
63
|
+
[Responses streaming event reference](https://platform.openai.com/docs/api-reference/responses-streaming),
|
|
64
|
+
including output text, reasoning summaries, function-call arguments,
|
|
65
|
+
refusal text, completion, incomplete responses, and errors. Refusal deltas use
|
|
66
|
+
the normalized `text` channel so provider-agnostic UIs do not drop the model's
|
|
67
|
+
response.
|
|
68
|
+
|
|
69
|
+
### OpenAI Chat Completions
|
|
22
70
|
|
|
23
71
|
```ts
|
|
24
72
|
import { parseOpenAIStream } from 'llm-sse';
|
|
@@ -29,7 +77,11 @@ const res = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
|
29
77
|
Authorization: `Bearer ${key}`,
|
|
30
78
|
'content-type': 'application/json',
|
|
31
79
|
},
|
|
32
|
-
body: JSON.stringify({
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
model: process.env.OPENAI_MODEL,
|
|
82
|
+
messages,
|
|
83
|
+
stream: true,
|
|
84
|
+
}),
|
|
33
85
|
});
|
|
34
86
|
|
|
35
87
|
for await (const event of parseOpenAIStream(res.body)) {
|
|
@@ -112,16 +164,29 @@ for await (const event of parseOpenAIStream(res.body)) {
|
|
|
112
164
|
|
|
113
165
|
## Why
|
|
114
166
|
|
|
115
|
-
- **One event shape,
|
|
167
|
+
- **One event shape, four streaming formats.** `text`, `reasoning`, `tool_call_start`, `tool_call_delta`, `finish`, and `error`, whether the bytes came from OpenAI Responses, OpenAI Chat Completions, Anthropic, or Gemini.
|
|
116
168
|
- **Tool calls just accumulate.** Streamed JSON argument fragments carry an `index`; concatenate by index (or let `collectStream` do it) to get the full call.
|
|
117
169
|
- **Correct SSE framing.** Robust to chunk boundaries splitting a line or event mid-way, CRLF, multi-line `data:` fields, comments and keep-alives.
|
|
118
170
|
- **Fixture-backed provider coverage.** Public OpenAI, Anthropic and Gemini `.sse` fixtures exercise text, reasoning, tool-call arguments and finish reasons.
|
|
119
171
|
- **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.
|
|
120
172
|
- **Zero dependencies**, ESM + CJS, fully typed.
|
|
121
173
|
|
|
174
|
+
| Streaming source | Parser | Text | Reasoning | Function calls | Finish and errors |
|
|
175
|
+
| ------------------------------ | ---------------------------- | :--: | :---------------: | :------------: | :---------------: |
|
|
176
|
+
| OpenAI Responses API | `parseOpenAIResponsesStream` | ✓ | ✓ | ✓ | ✓ |
|
|
177
|
+
| OpenAI Chat Completions | `parseOpenAIStream` | ✓ | compatible fields | ✓ | ✓ |
|
|
178
|
+
| Anthropic Messages | `parseAnthropicStream` | ✓ | ✓ | ✓ | ✓ |
|
|
179
|
+
| Gemini `streamGenerateContent` | `parseGeminiStream` | ✓ | ✓ | ✓ | ✓ |
|
|
180
|
+
|
|
122
181
|
## Why not the provider SDK?
|
|
123
182
|
|
|
124
|
-
The official SDKs
|
|
183
|
+
The official SDKs each ship their own streaming abstraction, so combining APIs
|
|
184
|
+
means learning multiple event models and writing multiple adapter paths in your
|
|
185
|
+
agent or UI. `llm-sse` is a thin, zero-dependency layer that normalizes the wire
|
|
186
|
+
format only. You keep your own fetch, retry, and authentication logic. If you
|
|
187
|
+
use one provider exclusively, its SDK helpers may be sufficient; this library is
|
|
188
|
+
most useful when you need provider portability, a minimal footprint, or control
|
|
189
|
+
over the HTTP layer.
|
|
125
190
|
|
|
126
191
|
## Install
|
|
127
192
|
|
|
@@ -131,6 +196,13 @@ npm install llm-sse
|
|
|
131
196
|
|
|
132
197
|
## API
|
|
133
198
|
|
|
199
|
+
### `parseOpenAIResponsesStream(source)`
|
|
200
|
+
|
|
201
|
+
Parses typed OpenAI Responses API SSE events. It maps
|
|
202
|
+
`response.output_text.delta`, reasoning deltas, function-call item and argument
|
|
203
|
+
events, and terminal response states into normalized events. The Responses
|
|
204
|
+
`output_index` is used as the normalized tool-call index.
|
|
205
|
+
|
|
134
206
|
### `parseOpenAIStream(source)` · `parseAnthropicStream(source)` · `parseGeminiStream(source)`
|
|
135
207
|
|
|
136
208
|
Each takes a `source` (`AsyncIterable<Uint8Array | string>`, `fetch().body` satisfies this) and returns an `AsyncGenerator<StreamEvent>`.
|
|
@@ -141,7 +213,8 @@ Each takes a `source` (`AsyncIterable<Uint8Array | string>`, `fetch().body` sati
|
|
|
141
213
|
|
|
142
214
|
### `parseStream(source, provider)`
|
|
143
215
|
|
|
144
|
-
Same thing, dispatching on `provider`
|
|
216
|
+
Same thing, dispatching on `provider`
|
|
217
|
+
(`'openai-responses' | 'openai' | 'anthropic' | 'gemini'`).
|
|
145
218
|
|
|
146
219
|
### `StreamEvent`
|
|
147
220
|
|
|
@@ -155,7 +228,9 @@ type StreamEvent =
|
|
|
155
228
|
| { type: 'error'; error: unknown };
|
|
156
229
|
```
|
|
157
230
|
|
|
158
|
-
> `reasoning` carries
|
|
231
|
+
> `reasoning` carries OpenAI reasoning text or summaries, Anthropic extended
|
|
232
|
+
> thinking, and Gemini thought parts separately from `text`, so you can render
|
|
233
|
+
> it in its own affordance or drop it.
|
|
159
234
|
|
|
160
235
|
### `collectStream(events)`
|
|
161
236
|
|
|
@@ -271,13 +346,19 @@ const result2 = await complete('openai', openaiResponse);
|
|
|
271
346
|
|
|
272
347
|
## Tool calls
|
|
273
348
|
|
|
274
|
-
All
|
|
349
|
+
All supported APIs are normalized to the same pattern: a `tool_call_start`
|
|
350
|
+
(with `index`, and `id` / `name` when available) followed by one or more
|
|
351
|
+
`tool_call_delta`s whose `argumentsDelta` strings concatenate into the call's
|
|
352
|
+
JSON arguments. OpenAI Responses uses its stable `output_index`; Chat
|
|
353
|
+
Completions and Anthropic stream argument fragments; Gemini sends arguments
|
|
354
|
+
whole in a single delta. `collectStream` joins them for you.
|
|
275
355
|
|
|
276
356
|
## Fixture corpus
|
|
277
357
|
|
|
278
358
|
The package includes a small public fixture corpus under [`fixtures/`](https://github.com/slegarraga/llm-sse/tree/main/fixtures):
|
|
279
359
|
|
|
280
360
|
- `openai-weather-tool.sse`
|
|
361
|
+
- `openai-responses-weather-tool.sse`
|
|
281
362
|
- `anthropic-weather-tool.sse`
|
|
282
363
|
- `gemini-weather-tool.sse`
|
|
283
364
|
- expected normalized events and collected messages under `fixtures/expected/`
|
|
@@ -287,6 +368,15 @@ Each fixture describes the same semantic turn: reasoning, visible text, a
|
|
|
287
368
|
The tests parse the fixtures directly, including byte-split stream boundaries,
|
|
288
369
|
so contributors can change parsers with a stable cross-provider contract.
|
|
289
370
|
|
|
371
|
+
## Community
|
|
372
|
+
|
|
373
|
+
Contributions are welcome, especially sanitized provider fixtures, reproducible
|
|
374
|
+
edge cases, interoperability examples, and review help. Start with
|
|
375
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md), use
|
|
376
|
+
[Discussions](https://github.com/slegarraga/llm-sse/discussions) for design
|
|
377
|
+
questions, and see [GOVERNANCE.md](./GOVERNANCE.md) for how sustained
|
|
378
|
+
contributors can become reviewers or maintainers.
|
|
379
|
+
|
|
290
380
|
## Related
|
|
291
381
|
|
|
292
382
|
- [`json-from-llm`](https://www.npmjs.com/package/json-from-llm): extract valid JSON from an LLM response, even inside reasoning tags, fenced blocks or prose
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
`llm-sse` exists to make provider streaming predictable without requiring a
|
|
4
|
+
provider SDK or adding runtime dependencies. The roadmap prioritizes correctness,
|
|
5
|
+
interoperability, and evidence-backed support over adding providers by name.
|
|
6
|
+
|
|
7
|
+
## Current priorities
|
|
8
|
+
|
|
9
|
+
### 1. Streaming conformance
|
|
10
|
+
|
|
11
|
+
- Keep the public fixture corpus aligned with documented OpenAI Responses,
|
|
12
|
+
OpenAI Chat Completions, Anthropic Messages, and Gemini event shapes.
|
|
13
|
+
- Add regression fixtures for real provider edge cases after removing secrets
|
|
14
|
+
and user content.
|
|
15
|
+
- Preserve byte-boundary, line-ending, malformed-event, and multi-tool coverage.
|
|
16
|
+
|
|
17
|
+
### 2. Provider portability
|
|
18
|
+
|
|
19
|
+
- Keep normalized text, reasoning, tool-call, finish, and error events stable.
|
|
20
|
+
- Document unavoidable information loss between provider formats.
|
|
21
|
+
- Improve composition with
|
|
22
|
+
[`llm-messages`](https://github.com/slegarraga/llm-messages),
|
|
23
|
+
[`tool-schema`](https://github.com/slegarraga/tool-schema), and
|
|
24
|
+
[`llm-errors`](https://github.com/slegarraga/llm-errors).
|
|
25
|
+
|
|
26
|
+
### 3. Maintainer workflows
|
|
27
|
+
|
|
28
|
+
- Make provider-behavior changes reviewable through small fixtures and explicit
|
|
29
|
+
links to primary documentation.
|
|
30
|
+
- Automate compatibility reports, release notes, and routine issue triage while
|
|
31
|
+
keeping release decisions human-reviewed.
|
|
32
|
+
- Publish enough project context for contributors to reproduce and own an area
|
|
33
|
+
of the parser.
|
|
34
|
+
|
|
35
|
+
### 4. Sustainable community
|
|
36
|
+
|
|
37
|
+
- Grow usage through real integrations and examples rather than synthetic stars
|
|
38
|
+
or low-value promotion.
|
|
39
|
+
- Welcome additional reviewers and maintainers through the path described in
|
|
40
|
+
[GOVERNANCE.md](./GOVERNANCE.md).
|
|
41
|
+
- Track breaking proposals in public issues before implementation.
|
|
42
|
+
|
|
43
|
+
## API credit use
|
|
44
|
+
|
|
45
|
+
If the project receives sponsored API credits, they will be used for opt-in
|
|
46
|
+
conformance checks against official provider endpoints, fixture refreshes,
|
|
47
|
+
regression reproduction, and maintainer automation. Secrets and user prompts
|
|
48
|
+
will not be committed. Pull requests will continue to run deterministically
|
|
49
|
+
against sanitized offline fixtures.
|
|
50
|
+
|
|
51
|
+
## Non-goals
|
|
52
|
+
|
|
53
|
+
- Replacing full provider SDKs.
|
|
54
|
+
- Managing authentication, retries, billing, or network requests.
|
|
55
|
+
- Claiming compatibility with an endpoint without a documented event format or
|
|
56
|
+
reproducible fixture.
|
|
57
|
+
- Adding runtime dependencies for convenience.
|
|
58
|
+
|
|
59
|
+
## Propose an item
|
|
60
|
+
|
|
61
|
+
Open a [GitHub Discussion](https://github.com/slegarraga/llm-sse/discussions)
|
|
62
|
+
for design exploration or a
|
|
63
|
+
[feature request](https://github.com/slegarraga/llm-sse/issues/new?template=feature_request.yml)
|
|
64
|
+
for a scoped, testable change.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
The latest published `0.x` release receives security fixes.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please report security issues privately rather than opening a public issue.
|
|
10
|
+
|
|
11
|
+
- Use GitHub's [private vulnerability reporting](https://github.com/slegarraga/llm-sse/security/advisories/new), or
|
|
12
|
+
- Email **sebastian@0a.cl** with the details.
|
|
13
|
+
|
|
14
|
+
Include a description, a reproduction, and the impact. You can expect an initial
|
|
15
|
+
response within a few days. Once a fix is released, we are happy to credit you in
|
|
16
|
+
the advisory unless you prefer to remain anonymous.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
`llm-sse` has zero runtime dependencies and performs only in-memory parsing
|
|
21
|
+
of streamed text: it does not make network requests, read or write files, or
|
|
22
|
+
execute code from its input. The most relevant risks are denial of service from
|
|
23
|
+
pathological input (for example, unbounded buffering on a stream with no event
|
|
24
|
+
delimiters). Reports along those lines are welcome.
|
package/SUPPORT.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Support
|
|
2
|
+
|
|
3
|
+
`llm-sse` is maintained in public on a best-effort basis.
|
|
4
|
+
|
|
5
|
+
## Questions and design discussions
|
|
6
|
+
|
|
7
|
+
Use [GitHub Discussions](https://github.com/slegarraga/llm-sse/discussions) for
|
|
8
|
+
integration questions, design exploration, and help choosing the right parser.
|
|
9
|
+
|
|
10
|
+
## Bugs
|
|
11
|
+
|
|
12
|
+
Open a
|
|
13
|
+
[bug report](https://github.com/slegarraga/llm-sse/issues/new?template=bug_report.yml)
|
|
14
|
+
with:
|
|
15
|
+
|
|
16
|
+
- the provider and API surface;
|
|
17
|
+
- a minimal, sanitized sequence of SSE `data:` payloads;
|
|
18
|
+
- the normalized events you expected and received;
|
|
19
|
+
- the `llm-sse`, Node.js, and runtime versions;
|
|
20
|
+
- a reproduction or failing test when possible.
|
|
21
|
+
|
|
22
|
+
Please do not include API keys, private prompts, personal data, or proprietary
|
|
23
|
+
model output.
|
|
24
|
+
|
|
25
|
+
## Security
|
|
26
|
+
|
|
27
|
+
Do not open a public issue for a vulnerability. Follow
|
|
28
|
+
[SECURITY.md](./SECURITY.md) to use GitHub private vulnerability reporting or
|
|
29
|
+
the listed security email.
|
|
30
|
+
|
|
31
|
+
## Scope
|
|
32
|
+
|
|
33
|
+
The project can help with parsing and normalization behavior. Provider account
|
|
34
|
+
access, billing, rate limits, model availability, and general SDK support belong
|
|
35
|
+
with the relevant provider.
|
|
36
|
+
|
|
37
|
+
No response-time guarantee is offered, but well-scoped reproductions and pull
|
|
38
|
+
requests are prioritized.
|
package/dist/index.cjs
CHANGED
|
@@ -23,6 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
collectStream: () => collectStream,
|
|
24
24
|
parseAnthropicStream: () => parseAnthropicStream,
|
|
25
25
|
parseGeminiStream: () => parseGeminiStream,
|
|
26
|
+
parseOpenAIResponsesStream: () => parseOpenAIResponsesStream,
|
|
26
27
|
parseOpenAIStream: () => parseOpenAIStream,
|
|
27
28
|
parseStream: () => parseStream,
|
|
28
29
|
sseData: () => sseData,
|
|
@@ -183,6 +184,77 @@ function mapOpenAI(chunk) {
|
|
|
183
184
|
return events;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
// src/providers/openai-responses.ts
|
|
188
|
+
function mapOpenAIResponses(event) {
|
|
189
|
+
if (!event || typeof event !== "object") {
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
switch (event.type) {
|
|
193
|
+
case "response.output_text.delta":
|
|
194
|
+
return stringDelta(event.delta, "text");
|
|
195
|
+
// The normalized model has one user-visible text channel. Preserve refusal
|
|
196
|
+
// text there so a provider-agnostic UI never drops the model's response.
|
|
197
|
+
case "response.refusal.delta":
|
|
198
|
+
return stringDelta(event.delta, "text");
|
|
199
|
+
case "response.reasoning_summary_text.delta":
|
|
200
|
+
case "response.reasoning_text.delta":
|
|
201
|
+
return stringDelta(event.delta, "reasoning");
|
|
202
|
+
case "response.output_item.added": {
|
|
203
|
+
const item = event.item;
|
|
204
|
+
if (!item || typeof item !== "object" || item.type !== "function_call") {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
const index = numericIndex(event.output_index);
|
|
208
|
+
const id = typeof item.call_id === "string" ? item.call_id : typeof item.id === "string" ? item.id : void 0;
|
|
209
|
+
const name = typeof item.name === "string" ? item.name : void 0;
|
|
210
|
+
return [{ type: "tool_call_start", index, id, name }];
|
|
211
|
+
}
|
|
212
|
+
case "response.function_call_arguments.delta":
|
|
213
|
+
if (typeof event.delta !== "string" || event.delta.length === 0) {
|
|
214
|
+
return [];
|
|
215
|
+
}
|
|
216
|
+
return [
|
|
217
|
+
{
|
|
218
|
+
type: "tool_call_delta",
|
|
219
|
+
index: numericIndex(event.output_index),
|
|
220
|
+
argumentsDelta: event.delta
|
|
221
|
+
}
|
|
222
|
+
];
|
|
223
|
+
case "response.completed":
|
|
224
|
+
return [{ type: "finish", reason: "completed" }];
|
|
225
|
+
case "response.incomplete": {
|
|
226
|
+
const reason = event.response?.incomplete_details?.reason;
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
type: "finish",
|
|
230
|
+
reason: typeof reason === "string" ? reason : "incomplete"
|
|
231
|
+
}
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
case "response.failed":
|
|
235
|
+
return [
|
|
236
|
+
{
|
|
237
|
+
type: "error",
|
|
238
|
+
error: event.response?.error ?? {
|
|
239
|
+
type: "response_failed",
|
|
240
|
+
message: "The OpenAI response failed."
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{ type: "finish", reason: "failed" }
|
|
244
|
+
];
|
|
245
|
+
case "error":
|
|
246
|
+
return [{ type: "error", error: event }];
|
|
247
|
+
default:
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function numericIndex(value) {
|
|
252
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
253
|
+
}
|
|
254
|
+
function stringDelta(value, type) {
|
|
255
|
+
return typeof value === "string" && value.length > 0 ? [{ type, text: value }] : [];
|
|
256
|
+
}
|
|
257
|
+
|
|
186
258
|
// src/sse.ts
|
|
187
259
|
async function* decodeChunks(source) {
|
|
188
260
|
const decoder = new TextDecoder();
|
|
@@ -302,6 +374,9 @@ async function* parseWith(source, map) {
|
|
|
302
374
|
function parseOpenAIStream(source) {
|
|
303
375
|
return parseWith(source, mapOpenAI);
|
|
304
376
|
}
|
|
377
|
+
function parseOpenAIResponsesStream(source) {
|
|
378
|
+
return parseWith(source, mapOpenAIResponses);
|
|
379
|
+
}
|
|
305
380
|
function parseAnthropicStream(source) {
|
|
306
381
|
return parseWith(source, mapAnthropic);
|
|
307
382
|
}
|
|
@@ -330,6 +405,8 @@ function parseStream(source, provider) {
|
|
|
330
405
|
switch (provider) {
|
|
331
406
|
case "openai":
|
|
332
407
|
return parseOpenAIStream(source);
|
|
408
|
+
case "openai-responses":
|
|
409
|
+
return parseOpenAIResponsesStream(source);
|
|
333
410
|
case "anthropic":
|
|
334
411
|
return parseAnthropicStream(source);
|
|
335
412
|
case "gemini":
|
|
@@ -424,6 +501,7 @@ function toAssistantMessage(collected) {
|
|
|
424
501
|
collectStream,
|
|
425
502
|
parseAnthropicStream,
|
|
426
503
|
parseGeminiStream,
|
|
504
|
+
parseOpenAIResponsesStream,
|
|
427
505
|
parseOpenAIStream,
|
|
428
506
|
parseStream,
|
|
429
507
|
sseData,
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 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 // The SSE spec strips one leading BOM. TextDecoder already does this for\n // byte chunks; this covers sources that yield strings.\n let atStart = true;\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 const processLine = (line: string): string | undefined => {\n if (line === '') {\n return finishEvent();\n }\n addLine(line);\n return undefined;\n };\n\n for await (const text of decodeChunks(source)) {\n if (atStart && text.length > 0) {\n atStart = false;\n buffer += text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n } else {\n buffer += text;\n }\n\n while (buffer.length > 0) {\n const cr = buffer.indexOf('\\r');\n const lf = buffer.indexOf('\\n');\n let newline: number;\n\n if (cr === -1) {\n newline = lf;\n } else if (lf === -1) {\n newline = cr;\n } else {\n newline = Math.min(cr, lf);\n }\n\n if (newline === -1) {\n break;\n }\n\n const newlineChar = buffer[newline];\n if (newlineChar === '\\r' && newline === buffer.length - 1) {\n break;\n }\n\n const line = buffer.slice(0, newline);\n const nextIsLf = newlineChar === '\\r' && buffer[newline + 1] === '\\n';\n buffer = buffer.slice(newline + (nextIsLf ? 2 : 1));\n\n const data = processLine(line);\n if (data !== undefined) {\n yield data;\n }\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer;\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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACSO,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,cAAM,QAAQ,WAAW,MAAM,KAAK;AACpC,YAAI,UAAU,QAAW;AACvB;AAAA,QACF;AACA,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,IAAI,OAAO,MAAM,OAAO,WAAW,MAAM,KAAK;AAAA,UAC9C,MAAM,OAAO,MAAM,SAAS,WAAW,MAAM,OAAO;AAAA,QACtD,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,cAAM,QAAQ,WAAW,MAAM,KAAK;AACpC,YAAI,UAAU,QAAW;AACvB;AAAA,QACF;AACA,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,gBAAgB,MAAM;AAAA,QACxB,CAAC;AAAA,MACH;AACA;AAAA,IACF;AAAA,IACA,KAAK,iBAAiB;AACpB,YAAM,SAAS,MAAM,OAAO;AAC5B,UAAI,OAAO,WAAW,YAAY,OAAO,SAAS,GAAG;AACnD,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;AAEA,SAAS,WAAW,OAAoC;AACtD,SAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,KAAK,SAAS,IACpE,QACA;AACN;;;AC3DO,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,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC;AAAA,MACF;AAEA,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,YAAM,eAAe,KAAK;AAC1B,UACE,gBACA,OAAO,iBAAiB,YACxB,CAAC,MAAM,QAAQ,YAAY,KAC3B,OAAO,aAAa,SAAS,YAC7B,aAAa,KAAK,SAAS,GAC3B;AACA,cAAM,QAAQ,MAAM;AACpB,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,MAAM,aAAa;AAAA,QACrB,CAAC;AACD,eAAO,KAAK;AAAA,UACV,MAAM;AAAA,UACN;AAAA,UACA,gBAAgB,KAAK,UAAU,aAAa,QAAQ,CAAC,CAAC;AAAA,QACxD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,MACE,OAAO,UAAU,iBAAiB,YAClC,UAAU,aAAa,SAAS,GAChC;AACA,WAAO,KAAK,EAAE,MAAM,UAAU,QAAQ,UAAU,aAAa,CAAC;AAAA,EAChE;AACA,SAAO;AACT;;;ACzDO,SAAS,UAAU,OAA2B;AACnD,QAAM,SAAwB,CAAC;AAC/B,QAAM,UAAU,MAAM,QAAQ,OAAO,OAAO,IAAI,MAAM,UAAU,CAAC;AACjE,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO;AAAA,EACT;AAEA,aAAW,UAAU,SAAS;AAC5B,QAAI,CAAC,UAAU,OAAO,WAAW,UAAU;AACzC;AAAA,IACF;AAEA,UAAM,QAAQ,OAAO;AACrB,QAAI,SAAS,OAAO,UAAU,UAAU;AAGtC,YAAM,YAAY,MAAM,qBAAqB,MAAM;AACnD,UAAI,OAAO,cAAc,YAAY,UAAU,SAAS,GAAG;AACzD,eAAO,KAAK,EAAE,MAAM,aAAa,MAAM,UAAU,CAAC;AAAA,MACpD;AACA,UAAI,OAAO,MAAM,YAAY,YAAY,MAAM,QAAQ,SAAS,GAAG;AACjE,eAAO,KAAK,EAAE,MAAM,QAAQ,MAAM,MAAM,QAAQ,CAAC;AAAA,MACnD;AACA,UAAI,MAAM,QAAQ,MAAM,UAAU,GAAG;AACnC,mBAAW,QAAQ,MAAM,YAAY;AACnC,cAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;AACrC;AAAA,UACF;AAEA,gBAAM,QAAQ,OAAO,KAAK,UAAU,WAAW,KAAK,QAAQ;AAC5D,gBAAM,KACJ,KAAK,YAAY,OAAO,KAAK,aAAa,WACtC,KAAK,WACL;AACN,gBAAM,KAAK,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AACnD,gBAAM,OAAO,OAAO,IAAI,SAAS,WAAW,GAAG,OAAO;AAEtD,cAAI,OAAO,UAAa,SAAS,QAAW;AAC1C,mBAAO,KAAK;AAAA,cACV,MAAM;AAAA,cACN;AAAA,cACA;AAAA,cACA;AAAA,YACF,CAAC;AAAA,UACH;AACA,gBAAM,OAAO,IAAI;AACjB,cAAI,OAAO,SAAS,YAAY,KAAK,SAAS,GAAG;AAC/C,mBAAO,KAAK;AAAA,cACV,MAAM;AAAA,cACN;AAAA,cACA,gBAAgB;AAAA,YAClB,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QACE,OAAO,OAAO,kBAAkB,YAChC,OAAO,cAAc,SAAS,GAC9B;AACA,aAAO,KAAK,EAAE,MAAM,UAAU,QAAQ,OAAO,cAAc,CAAC;AAAA,IAC9D;AAAA,EACF;AACA,SAAO;AACT;;;ACnEA,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;AAG3B,MAAI,UAAU;AAEd,QAAM,UAAU,CAAC,SAAuB;AACtC,QAAI,KAAK,CAAC,MAAM,KAAK;AACnB;AAAA,IACF;AAEA,UAAM,YAAY,KAAK,QAAQ,GAAG;AAClC,UAAM,QAAQ,cAAc,KAAK,OAAO,KAAK,MAAM,GAAG,SAAS;AAC/D,QAAI,QAAQ,cAAc,KAAK,KAAK,KAAK,MAAM,YAAY,CAAC;AAC5D,QAAI,MAAM,WAAW,GAAG,GAAG;AACzB,cAAQ,MAAM,MAAM,CAAC;AAAA,IACvB;AAEA,QAAI,UAAU,QAAQ;AACpB,gBAAU,KAAK,KAAK;AAAA,IACtB;AAAA,EACF;AAEA,QAAM,cAAc,MAA0B;AAC5C,QAAI,UAAU,WAAW,GAAG;AAC1B,aAAO;AAAA,IACT;AACA,UAAMA,QAAO,UAAU,KAAK,IAAI;AAChC,gBAAY,CAAC;AACb,WAAOA;AAAA,EACT;AAEA,QAAM,cAAc,CAAC,SAAqC;AACxD,QAAI,SAAS,IAAI;AACf,aAAO,YAAY;AAAA,IACrB;AACA,YAAQ,IAAI;AACZ,WAAO;AAAA,EACT;AAEA,mBAAiB,QAAQ,aAAa,MAAM,GAAG;AAC7C,QAAI,WAAW,KAAK,SAAS,GAAG;AAC9B,gBAAU;AACV,gBAAU,KAAK,WAAW,CAAC,MAAM,QAAS,KAAK,MAAM,CAAC,IAAI;AAAA,IAC5D,OAAO;AACL,gBAAU;AAAA,IACZ;AAEA,WAAO,OAAO,SAAS,GAAG;AACxB,YAAM,KAAK,OAAO,QAAQ,IAAI;AAC9B,YAAM,KAAK,OAAO,QAAQ,IAAI;AAC9B,UAAI;AAEJ,UAAI,OAAO,IAAI;AACb,kBAAU;AAAA,MACZ,WAAW,OAAO,IAAI;AACpB,kBAAU;AAAA,MACZ,OAAO;AACL,kBAAU,KAAK,IAAI,IAAI,EAAE;AAAA,MAC3B;AAEA,UAAI,YAAY,IAAI;AAClB;AAAA,MACF;AAEA,YAAM,cAAc,OAAO,OAAO;AAClC,UAAI,gBAAgB,QAAQ,YAAY,OAAO,SAAS,GAAG;AACzD;AAAA,MACF;AAEA,YAAM,OAAO,OAAO,MAAM,GAAG,OAAO;AACpC,YAAM,WAAW,gBAAgB,QAAQ,OAAO,UAAU,CAAC,MAAM;AACjE,eAAS,OAAO,MAAM,WAAW,WAAW,IAAI,EAAE;AAElD,YAAMA,QAAO,YAAY,IAAI;AAC7B,UAAIA,UAAS,QAAW;AACtB,cAAMA;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,OAAO,SAAS,IAAI,IAAI,OAAO,MAAM,GAAG,EAAE,IAAI;AAC3D,MAAI,SAAS,IAAI;AACf,YAAQ,IAAI;AAAA,EACd;AACA,QAAM,OAAO,YAAY;AACzB,MAAI,SAAS,QAAW;AACtB,UAAM;AAAA,EACR;AACF;;;ACtHA,gBAAgB,UACd,QACA,KAC6B;AAC7B,mBAAiB,QAAQ,QAAQ,MAAM,GAAG;AACxC,QAAI,OAAO,IAAI,GAAG;AAChB;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,UAAU,KAAK,UAAU;AAC/B,UAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GAAG;AACtD,cAAM,mBAAmB,KAAK;AAAA,MAChC;AACA;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,OAAO,IAAI,GAAG;AAChB;AAAA,IACF;AACA,QAAI;AACJ,QAAI;AACF,gBAAU,KAAK,MAAM,IAAI;AAAA,IAC3B,SAAS,OAAO;AACd,YAAM,UAAU,KAAK,UAAU;AAC/B,UAAI,QAAQ,WAAW,GAAG,KAAK,QAAQ,WAAW,GAAG,GAAG;AACtD,cAAM,mBAAmB,KAAK;AAAA,MAChC;AACA;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;AAEA,SAAS,mBAAmB,OAA6B;AACvD,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAChE;AAAA,EACF;AACF;AAEA,SAAS,OAAO,MAAuB;AACrC,SAAO,KAAK,KAAK,MAAM;AACzB;;;AC9EA,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":["data"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/providers/openai.ts","../src/providers/openai-responses.ts","../src/sse.ts","../src/parse.ts","../src/collect.ts","../src/message.ts"],"sourcesContent":["export {\n parseStream,\n parseOpenAIStream,\n parseOpenAIResponsesStream,\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 { StreamEvent } from '../types.ts';\n\n/**\n * Map one OpenAI Responses API streaming event into normalized events.\n *\n * Responses streams are typed events rather than Chat Completions\n * `choices[].delta` chunks. Text, reasoning summaries, function calls,\n * completion state and errors are normalized here while unrelated built-in\n * tool events pass through without producing an event.\n *\n * @see https://platform.openai.com/docs/api-reference/responses-streaming\n */\nexport function mapOpenAIResponses(event: any): StreamEvent[] {\n if (!event || typeof event !== 'object') {\n return [];\n }\n\n switch (event.type) {\n case 'response.output_text.delta':\n return stringDelta(event.delta, 'text');\n\n // The normalized model has one user-visible text channel. Preserve refusal\n // text there so a provider-agnostic UI never drops the model's response.\n case 'response.refusal.delta':\n return stringDelta(event.delta, 'text');\n\n case 'response.reasoning_summary_text.delta':\n case 'response.reasoning_text.delta':\n return stringDelta(event.delta, 'reasoning');\n\n case 'response.output_item.added': {\n const item = event.item;\n if (!item || typeof item !== 'object' || item.type !== 'function_call') {\n return [];\n }\n\n const index = numericIndex(event.output_index);\n const id =\n typeof item.call_id === 'string'\n ? item.call_id\n : typeof item.id === 'string'\n ? item.id\n : undefined;\n const name = typeof item.name === 'string' ? item.name : undefined;\n\n return [{ type: 'tool_call_start', index, id, name }];\n }\n\n case 'response.function_call_arguments.delta':\n if (typeof event.delta !== 'string' || event.delta.length === 0) {\n return [];\n }\n return [\n {\n type: 'tool_call_delta',\n index: numericIndex(event.output_index),\n argumentsDelta: event.delta,\n },\n ];\n\n case 'response.completed':\n return [{ type: 'finish', reason: 'completed' }];\n\n case 'response.incomplete': {\n const reason = event.response?.incomplete_details?.reason;\n return [\n {\n type: 'finish',\n reason: typeof reason === 'string' ? reason : 'incomplete',\n },\n ];\n }\n\n case 'response.failed':\n return [\n {\n type: 'error',\n error: event.response?.error ?? {\n type: 'response_failed',\n message: 'The OpenAI response failed.',\n },\n },\n { type: 'finish', reason: 'failed' },\n ];\n\n case 'error':\n return [{ type: 'error', error: event }];\n\n default:\n return [];\n }\n}\n\nfunction numericIndex(value: unknown): number {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0\n ? value\n : 0;\n}\n\nfunction stringDelta(\n value: unknown,\n type: 'text' | 'reasoning',\n): StreamEvent[] {\n return typeof value === 'string' && value.length > 0\n ? [{ type, text: value }]\n : [];\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 // The SSE spec strips one leading BOM. TextDecoder already does this for\n // byte chunks; this covers sources that yield strings.\n let atStart = true;\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 const processLine = (line: string): string | undefined => {\n if (line === '') {\n return finishEvent();\n }\n addLine(line);\n return undefined;\n };\n\n for await (const text of decodeChunks(source)) {\n if (atStart && text.length > 0) {\n atStart = false;\n buffer += text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n } else {\n buffer += text;\n }\n\n while (buffer.length > 0) {\n const cr = buffer.indexOf('\\r');\n const lf = buffer.indexOf('\\n');\n let newline: number;\n\n if (cr === -1) {\n newline = lf;\n } else if (lf === -1) {\n newline = cr;\n } else {\n newline = Math.min(cr, lf);\n }\n\n if (newline === -1) {\n break;\n }\n\n const newlineChar = buffer[newline];\n if (newlineChar === '\\r' && newline === buffer.length - 1) {\n break;\n }\n\n const line = buffer.slice(0, newline);\n const nextIsLf = newlineChar === '\\r' && buffer[newline + 1] === '\\n';\n buffer = buffer.slice(newline + (nextIsLf ? 2 : 1));\n\n const data = processLine(line);\n if (data !== undefined) {\n yield data;\n }\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer;\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 { mapOpenAIResponses } from './providers/openai-responses.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 OpenAI Responses API stream into normalized events. */\nexport function parseOpenAIResponsesStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n return parseWith(source, mapOpenAIResponses);\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 'openai-responses':\n return parseOpenAIResponsesStream(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.d.cts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
/** The provider whose streaming format is being parsed. */
|
|
2
|
-
type Provider = 'openai' | 'anthropic' | 'gemini';
|
|
1
|
+
/** The provider/API whose streaming format is being parsed. */
|
|
2
|
+
type Provider = 'openai' | 'openai-responses' | 'anthropic' | 'gemini';
|
|
3
3
|
/**
|
|
4
4
|
* A single normalized event emitted while parsing a provider stream.
|
|
5
5
|
*
|
|
6
|
-
* The
|
|
7
|
-
* chunks,
|
|
8
|
-
* sends `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The providers stream very differently — OpenAI Chat Completions sends
|
|
7
|
+
* `choices[].delta` chunks, OpenAI Responses sends typed `response.*` events,
|
|
8
|
+
* Anthropic sends `content_block_*` / `message_*` events, and Gemini sends
|
|
9
|
+
* `candidates[].content.parts` — but they all reduce to the same handful of
|
|
10
|
+
* things: text arrived, reasoning arrived, a tool call started, tool-call
|
|
11
|
+
* arguments arrived, the turn finished, or the provider reported an error.
|
|
11
12
|
*/
|
|
12
13
|
type StreamEvent =
|
|
13
14
|
/** A chunk of assistant text. */
|
|
@@ -74,6 +75,8 @@ type ChunkSource = AsyncIterable<Uint8Array | string>;
|
|
|
74
75
|
|
|
75
76
|
/** Parse an OpenAI Chat Completions stream into normalized events. */
|
|
76
77
|
declare function parseOpenAIStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
|
|
78
|
+
/** Parse an OpenAI Responses API stream into normalized events. */
|
|
79
|
+
declare function parseOpenAIResponsesStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
|
|
77
80
|
/** Parse an Anthropic Messages stream into normalized events. */
|
|
78
81
|
declare function parseAnthropicStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
|
|
79
82
|
/** Parse a Gemini `streamGenerateContent` (SSE) stream into normalized events. */
|
|
@@ -146,4 +149,4 @@ declare function toAssistantMessage(collected: CollectedMessage): AssistantMessa
|
|
|
146
149
|
*/
|
|
147
150
|
declare function sseData(source: ChunkSource): AsyncGenerator<string>;
|
|
148
151
|
|
|
149
|
-
export { type AssistantMessage, type AssistantToolCall, type ChunkSource, type CollectedMessage, type CollectedToolCall, type Provider, type StreamEvent, collectStream, parseAnthropicStream, parseGeminiStream, parseOpenAIStream, parseStream, sseData, toAssistantMessage };
|
|
152
|
+
export { type AssistantMessage, type AssistantToolCall, type ChunkSource, type CollectedMessage, type CollectedToolCall, type Provider, type StreamEvent, collectStream, parseAnthropicStream, parseGeminiStream, parseOpenAIResponsesStream, parseOpenAIStream, parseStream, sseData, toAssistantMessage };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
/** The provider whose streaming format is being parsed. */
|
|
2
|
-
type Provider = 'openai' | 'anthropic' | 'gemini';
|
|
1
|
+
/** The provider/API whose streaming format is being parsed. */
|
|
2
|
+
type Provider = 'openai' | 'openai-responses' | 'anthropic' | 'gemini';
|
|
3
3
|
/**
|
|
4
4
|
* A single normalized event emitted while parsing a provider stream.
|
|
5
5
|
*
|
|
6
|
-
* The
|
|
7
|
-
* chunks,
|
|
8
|
-
* sends `
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* The providers stream very differently — OpenAI Chat Completions sends
|
|
7
|
+
* `choices[].delta` chunks, OpenAI Responses sends typed `response.*` events,
|
|
8
|
+
* Anthropic sends `content_block_*` / `message_*` events, and Gemini sends
|
|
9
|
+
* `candidates[].content.parts` — but they all reduce to the same handful of
|
|
10
|
+
* things: text arrived, reasoning arrived, a tool call started, tool-call
|
|
11
|
+
* arguments arrived, the turn finished, or the provider reported an error.
|
|
11
12
|
*/
|
|
12
13
|
type StreamEvent =
|
|
13
14
|
/** A chunk of assistant text. */
|
|
@@ -74,6 +75,8 @@ type ChunkSource = AsyncIterable<Uint8Array | string>;
|
|
|
74
75
|
|
|
75
76
|
/** Parse an OpenAI Chat Completions stream into normalized events. */
|
|
76
77
|
declare function parseOpenAIStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
|
|
78
|
+
/** Parse an OpenAI Responses API stream into normalized events. */
|
|
79
|
+
declare function parseOpenAIResponsesStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
|
|
77
80
|
/** Parse an Anthropic Messages stream into normalized events. */
|
|
78
81
|
declare function parseAnthropicStream(source: ChunkSource): AsyncGenerator<StreamEvent>;
|
|
79
82
|
/** Parse a Gemini `streamGenerateContent` (SSE) stream into normalized events. */
|
|
@@ -146,4 +149,4 @@ declare function toAssistantMessage(collected: CollectedMessage): AssistantMessa
|
|
|
146
149
|
*/
|
|
147
150
|
declare function sseData(source: ChunkSource): AsyncGenerator<string>;
|
|
148
151
|
|
|
149
|
-
export { type AssistantMessage, type AssistantToolCall, type ChunkSource, type CollectedMessage, type CollectedToolCall, type Provider, type StreamEvent, collectStream, parseAnthropicStream, parseGeminiStream, parseOpenAIStream, parseStream, sseData, toAssistantMessage };
|
|
152
|
+
export { type AssistantMessage, type AssistantToolCall, type ChunkSource, type CollectedMessage, type CollectedToolCall, type Provider, type StreamEvent, collectStream, parseAnthropicStream, parseGeminiStream, parseOpenAIResponsesStream, parseOpenAIStream, parseStream, sseData, toAssistantMessage };
|
package/dist/index.js
CHANGED
|
@@ -151,6 +151,77 @@ function mapOpenAI(chunk) {
|
|
|
151
151
|
return events;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
// src/providers/openai-responses.ts
|
|
155
|
+
function mapOpenAIResponses(event) {
|
|
156
|
+
if (!event || typeof event !== "object") {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
switch (event.type) {
|
|
160
|
+
case "response.output_text.delta":
|
|
161
|
+
return stringDelta(event.delta, "text");
|
|
162
|
+
// The normalized model has one user-visible text channel. Preserve refusal
|
|
163
|
+
// text there so a provider-agnostic UI never drops the model's response.
|
|
164
|
+
case "response.refusal.delta":
|
|
165
|
+
return stringDelta(event.delta, "text");
|
|
166
|
+
case "response.reasoning_summary_text.delta":
|
|
167
|
+
case "response.reasoning_text.delta":
|
|
168
|
+
return stringDelta(event.delta, "reasoning");
|
|
169
|
+
case "response.output_item.added": {
|
|
170
|
+
const item = event.item;
|
|
171
|
+
if (!item || typeof item !== "object" || item.type !== "function_call") {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
const index = numericIndex(event.output_index);
|
|
175
|
+
const id = typeof item.call_id === "string" ? item.call_id : typeof item.id === "string" ? item.id : void 0;
|
|
176
|
+
const name = typeof item.name === "string" ? item.name : void 0;
|
|
177
|
+
return [{ type: "tool_call_start", index, id, name }];
|
|
178
|
+
}
|
|
179
|
+
case "response.function_call_arguments.delta":
|
|
180
|
+
if (typeof event.delta !== "string" || event.delta.length === 0) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
return [
|
|
184
|
+
{
|
|
185
|
+
type: "tool_call_delta",
|
|
186
|
+
index: numericIndex(event.output_index),
|
|
187
|
+
argumentsDelta: event.delta
|
|
188
|
+
}
|
|
189
|
+
];
|
|
190
|
+
case "response.completed":
|
|
191
|
+
return [{ type: "finish", reason: "completed" }];
|
|
192
|
+
case "response.incomplete": {
|
|
193
|
+
const reason = event.response?.incomplete_details?.reason;
|
|
194
|
+
return [
|
|
195
|
+
{
|
|
196
|
+
type: "finish",
|
|
197
|
+
reason: typeof reason === "string" ? reason : "incomplete"
|
|
198
|
+
}
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
case "response.failed":
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
type: "error",
|
|
205
|
+
error: event.response?.error ?? {
|
|
206
|
+
type: "response_failed",
|
|
207
|
+
message: "The OpenAI response failed."
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{ type: "finish", reason: "failed" }
|
|
211
|
+
];
|
|
212
|
+
case "error":
|
|
213
|
+
return [{ type: "error", error: event }];
|
|
214
|
+
default:
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function numericIndex(value) {
|
|
219
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
|
|
220
|
+
}
|
|
221
|
+
function stringDelta(value, type) {
|
|
222
|
+
return typeof value === "string" && value.length > 0 ? [{ type, text: value }] : [];
|
|
223
|
+
}
|
|
224
|
+
|
|
154
225
|
// src/sse.ts
|
|
155
226
|
async function* decodeChunks(source) {
|
|
156
227
|
const decoder = new TextDecoder();
|
|
@@ -270,6 +341,9 @@ async function* parseWith(source, map) {
|
|
|
270
341
|
function parseOpenAIStream(source) {
|
|
271
342
|
return parseWith(source, mapOpenAI);
|
|
272
343
|
}
|
|
344
|
+
function parseOpenAIResponsesStream(source) {
|
|
345
|
+
return parseWith(source, mapOpenAIResponses);
|
|
346
|
+
}
|
|
273
347
|
function parseAnthropicStream(source) {
|
|
274
348
|
return parseWith(source, mapAnthropic);
|
|
275
349
|
}
|
|
@@ -298,6 +372,8 @@ function parseStream(source, provider) {
|
|
|
298
372
|
switch (provider) {
|
|
299
373
|
case "openai":
|
|
300
374
|
return parseOpenAIStream(source);
|
|
375
|
+
case "openai-responses":
|
|
376
|
+
return parseOpenAIResponsesStream(source);
|
|
301
377
|
case "anthropic":
|
|
302
378
|
return parseAnthropicStream(source);
|
|
303
379
|
case "gemini":
|
|
@@ -391,6 +467,7 @@ export {
|
|
|
391
467
|
collectStream,
|
|
392
468
|
parseAnthropicStream,
|
|
393
469
|
parseGeminiStream,
|
|
470
|
+
parseOpenAIResponsesStream,
|
|
394
471
|
parseOpenAIStream,
|
|
395
472
|
parseStream,
|
|
396
473
|
sseData,
|
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 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 // The SSE spec strips one leading BOM. TextDecoder already does this for\n // byte chunks; this covers sources that yield strings.\n let atStart = true;\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 const processLine = (line: string): string | undefined => {\n if (line === '') {\n return finishEvent();\n }\n addLine(line);\n return undefined;\n };\n\n for await (const text of decodeChunks(source)) {\n if (atStart && text.length > 0) {\n atStart = false;\n buffer += text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n } else {\n buffer += text;\n }\n\n while (buffer.length > 0) {\n const cr = buffer.indexOf('\\r');\n const lf = buffer.indexOf('\\n');\n let newline: number;\n\n if (cr === -1) {\n newline = lf;\n } else if (lf === -1) {\n newline = cr;\n } else {\n newline = Math.min(cr, lf);\n }\n\n if (newline === -1) {\n break;\n }\n\n const newlineChar = buffer[newline];\n if (newlineChar === '\\r' && newline === buffer.length - 1) {\n break;\n }\n\n const line = buffer.slice(0, newline);\n const nextIsLf = newlineChar === '\\r' && buffer[newline + 1] === '\\n';\n buffer = buffer.slice(newline + (nextIsLf ? 2 : 1));\n\n const data = processLine(line);\n if (data !== undefined) {\n yield data;\n }\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer;\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"]}
|
|
1
|
+
{"version":3,"sources":["../src/providers/anthropic.ts","../src/providers/gemini.ts","../src/providers/openai.ts","../src/providers/openai-responses.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 { StreamEvent } from '../types.ts';\n\n/**\n * Map one OpenAI Responses API streaming event into normalized events.\n *\n * Responses streams are typed events rather than Chat Completions\n * `choices[].delta` chunks. Text, reasoning summaries, function calls,\n * completion state and errors are normalized here while unrelated built-in\n * tool events pass through without producing an event.\n *\n * @see https://platform.openai.com/docs/api-reference/responses-streaming\n */\nexport function mapOpenAIResponses(event: any): StreamEvent[] {\n if (!event || typeof event !== 'object') {\n return [];\n }\n\n switch (event.type) {\n case 'response.output_text.delta':\n return stringDelta(event.delta, 'text');\n\n // The normalized model has one user-visible text channel. Preserve refusal\n // text there so a provider-agnostic UI never drops the model's response.\n case 'response.refusal.delta':\n return stringDelta(event.delta, 'text');\n\n case 'response.reasoning_summary_text.delta':\n case 'response.reasoning_text.delta':\n return stringDelta(event.delta, 'reasoning');\n\n case 'response.output_item.added': {\n const item = event.item;\n if (!item || typeof item !== 'object' || item.type !== 'function_call') {\n return [];\n }\n\n const index = numericIndex(event.output_index);\n const id =\n typeof item.call_id === 'string'\n ? item.call_id\n : typeof item.id === 'string'\n ? item.id\n : undefined;\n const name = typeof item.name === 'string' ? item.name : undefined;\n\n return [{ type: 'tool_call_start', index, id, name }];\n }\n\n case 'response.function_call_arguments.delta':\n if (typeof event.delta !== 'string' || event.delta.length === 0) {\n return [];\n }\n return [\n {\n type: 'tool_call_delta',\n index: numericIndex(event.output_index),\n argumentsDelta: event.delta,\n },\n ];\n\n case 'response.completed':\n return [{ type: 'finish', reason: 'completed' }];\n\n case 'response.incomplete': {\n const reason = event.response?.incomplete_details?.reason;\n return [\n {\n type: 'finish',\n reason: typeof reason === 'string' ? reason : 'incomplete',\n },\n ];\n }\n\n case 'response.failed':\n return [\n {\n type: 'error',\n error: event.response?.error ?? {\n type: 'response_failed',\n message: 'The OpenAI response failed.',\n },\n },\n { type: 'finish', reason: 'failed' },\n ];\n\n case 'error':\n return [{ type: 'error', error: event }];\n\n default:\n return [];\n }\n}\n\nfunction numericIndex(value: unknown): number {\n return typeof value === 'number' && Number.isInteger(value) && value >= 0\n ? value\n : 0;\n}\n\nfunction stringDelta(\n value: unknown,\n type: 'text' | 'reasoning',\n): StreamEvent[] {\n return typeof value === 'string' && value.length > 0\n ? [{ type, text: value }]\n : [];\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 // The SSE spec strips one leading BOM. TextDecoder already does this for\n // byte chunks; this covers sources that yield strings.\n let atStart = true;\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 const processLine = (line: string): string | undefined => {\n if (line === '') {\n return finishEvent();\n }\n addLine(line);\n return undefined;\n };\n\n for await (const text of decodeChunks(source)) {\n if (atStart && text.length > 0) {\n atStart = false;\n buffer += text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;\n } else {\n buffer += text;\n }\n\n while (buffer.length > 0) {\n const cr = buffer.indexOf('\\r');\n const lf = buffer.indexOf('\\n');\n let newline: number;\n\n if (cr === -1) {\n newline = lf;\n } else if (lf === -1) {\n newline = cr;\n } else {\n newline = Math.min(cr, lf);\n }\n\n if (newline === -1) {\n break;\n }\n\n const newlineChar = buffer[newline];\n if (newlineChar === '\\r' && newline === buffer.length - 1) {\n break;\n }\n\n const line = buffer.slice(0, newline);\n const nextIsLf = newlineChar === '\\r' && buffer[newline + 1] === '\\n';\n buffer = buffer.slice(newline + (nextIsLf ? 2 : 1));\n\n const data = processLine(line);\n if (data !== undefined) {\n yield data;\n }\n }\n }\n\n // A final event may arrive without a trailing blank line.\n const last = buffer.endsWith('\\r') ? buffer.slice(0, -1) : buffer;\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 { mapOpenAIResponses } from './providers/openai-responses.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 OpenAI Responses API stream into normalized events. */\nexport function parseOpenAIResponsesStream(\n source: ChunkSource,\n): AsyncGenerator<StreamEvent> {\n return parseWith(source, mapOpenAIResponses);\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 'openai-responses':\n return parseOpenAIResponsesStream(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,20 @@
|
|
|
1
|
+
# Security posture
|
|
2
|
+
|
|
3
|
+
`llm-sse` is a zero-runtime-dependency package, but the repository still tracks
|
|
4
|
+
supply-chain controls as part of maintenance.
|
|
5
|
+
|
|
6
|
+
Current controls:
|
|
7
|
+
|
|
8
|
+
- MIT license and public security policy.
|
|
9
|
+
- GitHub vulnerability alerts and Dependabot security updates.
|
|
10
|
+
- Secret scanning and push protection.
|
|
11
|
+
- CI on Node 20 and 22.
|
|
12
|
+
- CodeQL analysis for JavaScript/TypeScript.
|
|
13
|
+
- OpenSSF Scorecard workflow with published results.
|
|
14
|
+
- Pinned GitHub Actions and least-privilege workflow permissions.
|
|
15
|
+
- Branch ruleset for `main` requiring CI and CodeQL checks before merge.
|
|
16
|
+
- CODEOWNERS for maintainer review visibility.
|
|
17
|
+
|
|
18
|
+
Security reports should not be opened as public issues. Use the process in
|
|
19
|
+
[SECURITY.md](../SECURITY.md) or contact
|
|
20
|
+
[sebastian@0a.cl](mailto:sebastian@0a.cl).
|
|
@@ -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": "call_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": "completed" }
|
|
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": "call_weather_1",
|
|
8
|
+
"name": "get_weather",
|
|
9
|
+
"arguments": "{\"city\":\"Santiago\",\"units\":\"metric\"}"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"finishReason": "completed"
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
event: response.reasoning_summary_text.delta
|
|
2
|
+
data: {"type":"response.reasoning_summary_text.delta","item_id":"rs_fixture_weather","output_index":0,"summary_index":0,"delta":"Need weather lookup. ","sequence_number":1}
|
|
3
|
+
|
|
4
|
+
event: response.output_text.delta
|
|
5
|
+
data: {"type":"response.output_text.delta","item_id":"msg_fixture_weather","output_index":1,"content_index":0,"delta":"Let me check Santiago. ","sequence_number":2}
|
|
6
|
+
|
|
7
|
+
event: response.output_item.added
|
|
8
|
+
data: {"type":"response.output_item.added","output_index":2,"item":{"id":"fc_fixture_weather","type":"function_call","call_id":"call_weather_1","name":"get_weather","arguments":"","status":"in_progress"},"sequence_number":3}
|
|
9
|
+
|
|
10
|
+
event: response.function_call_arguments.delta
|
|
11
|
+
data: {"type":"response.function_call_arguments.delta","item_id":"fc_fixture_weather","output_index":2,"delta":"{\"city\":","sequence_number":4}
|
|
12
|
+
|
|
13
|
+
event: response.function_call_arguments.delta
|
|
14
|
+
data: {"type":"response.function_call_arguments.delta","item_id":"fc_fixture_weather","output_index":2,"delta":"\"Santiago\",\"units\":\"metric\"}","sequence_number":5}
|
|
15
|
+
|
|
16
|
+
event: response.completed
|
|
17
|
+
data: {"type":"response.completed","response":{"id":"resp_fixture_weather","status":"completed"},"sequence_number":6}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "llm-sse",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Parse
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Parse OpenAI Responses and Chat Completions, Anthropic, Gemini and OpenAI-compatible SSE streams into one unified event format. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai",
|
|
7
7
|
"anthropic",
|
|
@@ -48,7 +48,14 @@
|
|
|
48
48
|
"files": [
|
|
49
49
|
"dist",
|
|
50
50
|
"fixtures",
|
|
51
|
+
"docs",
|
|
51
52
|
"README.md",
|
|
53
|
+
"ROADMAP.md",
|
|
54
|
+
"GOVERNANCE.md",
|
|
55
|
+
"SUPPORT.md",
|
|
56
|
+
"CONTRIBUTING.md",
|
|
57
|
+
"CODE_OF_CONDUCT.md",
|
|
58
|
+
"SECURITY.md",
|
|
52
59
|
"LICENSE",
|
|
53
60
|
"CHANGELOG.md"
|
|
54
61
|
],
|
|
@@ -58,15 +65,21 @@
|
|
|
58
65
|
"sideEffects": false,
|
|
59
66
|
"scripts": {
|
|
60
67
|
"build": "tsup",
|
|
68
|
+
"check": "npm run format:check && npm run typecheck && npm run lint && npm test && npm run build && npm run pack:check && npm run pack:smoke",
|
|
61
69
|
"typecheck": "tsc --noEmit",
|
|
62
70
|
"test": "vitest run",
|
|
63
71
|
"test:watch": "vitest",
|
|
64
72
|
"lint": "eslint .",
|
|
65
73
|
"format": "prettier --write .",
|
|
66
74
|
"format:check": "prettier --check .",
|
|
67
|
-
"
|
|
75
|
+
"pack:check": "npm pack --dry-run --foreground-scripts=false",
|
|
76
|
+
"pack:smoke": "node scripts/pack-smoke.mjs",
|
|
77
|
+
"prepublishOnly": "npm run check",
|
|
68
78
|
"prepare": "npm run build"
|
|
69
79
|
},
|
|
80
|
+
"overrides": {
|
|
81
|
+
"esbuild": "0.28.1"
|
|
82
|
+
},
|
|
70
83
|
"devDependencies": {
|
|
71
84
|
"@eslint/js": "^10.0.1",
|
|
72
85
|
"@types/node": "^26.0.1",
|