feat(stream)!: decode SSE with the sse-stream crate, aligned with eventsource-parser - #213
Open
cunninghamcard-bit wants to merge 7 commits into
Open
cunninghamcard-bit wants to merge 7 commits into
cunninghamcard-bit wants to merge 7 commits into
Conversation
The AI SDK's parseJsonEventStream pipes through eventsource-parser. Replace the blank-line frame splitter with its line-based state machine: - \n, \r and \r\n line endings in any mix; a trailing \r waits for a possible \n in the next chunk - strip a UTF-8 BOM at the start of the stream - a field line without ':' has an empty value; empty event -> None; id containing U+0000 ignored; retry accepted only when all ASCII digits - exceeding the buffer limit (event data + partial line) is fatal and ends the stream, as maxBufferSize does upstream Kept from aimux: strict UTF-8 decoding after reassembly (an invalid line now drops only the event being built) and a default 1 MiB bound. extract_lines already matched upstream; the [DONE]-skipping JSON layer in aimux-provider-utils already matches parseJsonEventStream. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
Nothing in the workspace used NdjsonStream / NdjsonError and the AI SDK's provider-utils has no counterpart. Also move tokio to dev-dependencies (only the tests use it), drop the unused direct serde dependency, and update the crate description in README / PROJECT-OVERVIEW. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
A line that failed UTF-8 decoding reset the event, but the lines after it (up to the blank line) were then dispatched as a separate fragment event, producing a second downstream error. Mark the event poisoned until the next blank line so one bad event yields exactly one SseError::Utf8. Also update the aimux-stream description in CONTRIBUTING. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_015EYDYDWYcPsDmjBjFeuDVe
clippy::collapsible_match rejects the nested `if` under the CI `-D warnings` gate (stable 1.98); behaviour is unchanged. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
…s suite sse_test.rs was a behaviour list written from scratch, and its header claimed the AI SDK has no SSE parser tests. The parser behind parseJsonEventStream, eventsource-parser, ships its own suite. Port test/parse.test.ts and test/stream.test.ts (v3.1.1, MIT) case for case, driven by the same fixtures; test/multibyte.ts is checked in as JSON and read with include_str!. Cases with no Rust counterpart (onComment counts, reset(), onError payloads) are listed in the file header, as are the adaptations (retry is reported on the event, maxBufferSize maps to with_max_event_size). Four aimux-only tests remain: strict UTF-8, byte-level chunk splitting, transport errors, and the default limit. The four unit tests inside sse.rs are dropped; the port covers them. sha2 (already used by aimux-providers) is a dev-dependency for the upstream SHA-256 check on the 4.8M-character event. The tracker and argument-state tests were already case-for-case ports of the pinned upstream tests; only a comment is added. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Replace aimux-stream's hand-written SSE parser with a thin adapter over sse-stream 0.3 (WHATWG event-stream parsing), the way the AI SDK's parseJsonEventStream wraps eventsource-parser. The adapter keeps the AI SDK dispatch rules: only blocks with a data line are dispatched, an empty `event:` is None, and `retry` rides on its event. - No event size limit any more, as upstream: `with_max_event_size` and `SseError::FrameTooLarge` are removed. Real recordings of OpenAI Responses and Gemini image generation carry 2-3 MB events; the 1 MiB cap dropped them, and after the previous commit ended the stream on the first one. Both recordings now decode completely (15/15 and 5/5 events). - `SseError::Stream` carries the transport error as its source instead of a String; `Utf8` holds a `str::Utf8Error`; `Decode` covers the rest. Decoder errors end the stream after being reported. - The ported eventsource-parser suite runs unchanged against the adapter (29 cases); the 6 `maxBufferSize` cases go with the limit. - provider-utils: the `SseError::Stream` arm formats the source error. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
One entry for the final state of the branch: the per-commit changelog hunks described intermediate APIs that no longer exist. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS
4 of 5 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Split out of #204 (which keeps the tool-call tracker change) so the SSE fixes can be reviewed and merged on their own. Based on
master.aimux-stream'sSseStreamwas a hand-rolled parser with a 1 MiB per-event cap. The AI SDK'sparseJsonEventStreamwrapseventsource-parser; this PR makesSseStreamthe same thing: a thin adapter over thesse-streamcrate (WHATWG event-stream parsing,memchrfeature only).eventsource-parserdoes:\n,\rand\r\nline endings in any mix, a leading UTF-8 BOM is stripped, a field line without:is an empty value, an emptyevent:isNone, anidcontaining U+0000 is ignored,retrymust be all ASCII digits, and a block is dispatched only when it had adataline.with_max_event_sizeandSseError::FrameTooLargeare gone. This fixes the >1 MiB image-generation events recorded from OpenAI Responses and Gemini, which the cap truncated.SseError::Streamcarries the transport error as its source instead of aString;SseError::Utf8holds astr::Utf8Error;SseError::Decodeis added. Decoder errors (invalid UTF-8, transport failure) end the stream after being reported.NdjsonStream/NdjsonErrorare removed (unused, no AI SDK counterpart);tokioandserde_jsonbecome dev-dependencies, the unused directserdedependency is dropped.aimux-provider-utils/src/response_handler.rs, matchesSseError::Streamfor transport errors and turns every other error intoJsonParse.The tracker is untouched here; it is handled in #204.
Type of change
aimux-stream)Notes for reviewers
Tests are ported, not written.
sse_test.rsis a case-for-case port ofeventsource-parserv3.1.1'sparse.test.tsandstream.test.ts(MIT), driven by the same fixtures;test/multibyte.tsis checked in as JSON. What could not be ported (onCommentcounts,reset(),onErrorpayloads, themaxBufferSizecases, which have no counterpart now that there is no cap) is listed in the file header. aimux-only tests: strict UTF-8 as a terminal error, byte-level chunk splitting, transport errors.sha2(already used byaimux-providers) is a dev-dependency for the upstream SHA-256 check.Known, intentional differences from upstream: invalid UTF-8 is a strict, terminal
SseError::Utf8(upstream decodes lossily); Rusttrim()and JStrim()differ onU+FEFF/U+0085, not reachable with real wire data.Commit order: the parser rewrite, NDJSON removal, the UTF-8 error fix, a clippy fix (
collapsible_matchfails the CI-D warningsgate on stable), the test port, the switch tosse-stream, then one CHANGELOG entry describing the final API.Checklist
cargo fmt --all -- --checkcargo clippy -p aimux-stream -p aimux-provider-utils -p aimux-providers --all-targets -- -D warningscargo doc -p aimux-stream -p aimux-provider-utils --no-depswithRUSTDOCFLAGS=-D warningscargo test -p aimux-stream -p aimux-provider-utils -p aimux-providers— the existing chat/completions cassettes exercise the new parser end to end🤖 Generated with Claude Code
https://claude.ai/code/session_01WGwanvo9WLU9HWD7sRR8TS