Implements the approved simplification Agent Note: sse.ts now pipes the
response body through TextDecoderStream and EventSourceParserStream
(eventsource-parser/stream) and keeps only the DeepSeek protocol shim —
yield each event's data, terminate on [DONE], throw
LlmError('STREAM_CLOSED') on EOF without the sentinel. The SSE
spec-conformance tests are deleted; sse.spec.ts pins only the
[DONE]/STREAM_CLOSED/EOF contract, including the new spec-strict verdict
that an unterminated trailing event is truncation (the old parser
flushed it — a robustness nicety no real provider shape needs).
eventsource-parser@^3.1.0 becomes llm-deepseek's second runtime
dependency (already in the lockfile transitively via the MCP SDK).
Docs: the Agent Note moves proposed/ → implemented/ and is rewritten per
the lifecycle contract; the rejected NIH roll-up note's inbound links
follow. The twin-adapters note, dsh-llm LlmAdapter JSDoc (and its
type-equiv fences), cookbook, group/package READMEs, root AGENTS.md
layout line, sdk-helper comments, and the regenerated config catalog
drop the "hand-rolled fetch + SSE" claim in both languages; all eight
touched pairs re-recorded.
36 lines
1.5 KiB
TypeScript
36 lines
1.5 KiB
TypeScript
/**
|
|
* Decode an SSE byte stream into event `data` payloads. Framing — chunk
|
|
* reassembly, UTF-8/CRLF/BOM handling, comment and non-data field skipping,
|
|
* multi-`data:` joining — is `eventsource-parser`'s; this module keeps only
|
|
* the DeepSeek protocol: the literal `[DONE]` is yielded so the caller owns
|
|
* final flushing, and EOF before it raises {@link LlmError}. Framing is
|
|
* spec-strict: an event dispatches only on its blank-line terminator, so an
|
|
* unterminated tail at EOF is truncation, not a flushable payload.
|
|
*
|
|
* @module dsh-llm-deepseek/sse
|
|
*/
|
|
|
|
import { EventSourceParserStream } from 'eventsource-parser/stream'
|
|
import { LlmError } from '@deepseek-ai/dsh-llm'
|
|
|
|
/** The terminal payload DeepSeek (and OpenAI) send after the last chunk. */
|
|
export const DONE = '[DONE]'
|
|
|
|
/**
|
|
* Parse an SSE byte stream into data payloads. Yields `[DONE]` as the final
|
|
* value and returns; throws `LlmError('STREAM_CLOSED')` when the stream ends
|
|
* without it (truncated response — the model call cannot be trusted).
|
|
* @param stream - raw SSE bytes; reads may split anywhere, including mid-UTF-8 sequence.
|
|
* @returns each event's data payload in arrival order, the `[DONE]` sentinel last.
|
|
*/
|
|
export async function* parseSse(stream: ReadableStream<BufferSource>): AsyncGenerator<string> {
|
|
const events = stream
|
|
.pipeThrough(new TextDecoderStream())
|
|
.pipeThrough(new EventSourceParserStream())
|
|
for await (const { data } of events) {
|
|
yield data
|
|
if (data === DONE) return
|
|
}
|
|
throw new LlmError('SSE stream ended without [DONE]', 'STREAM_CLOSED')
|
|
}
|