char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Aleksander Grygierui: Stores split refactor (#27240)521a64cd0

char/aliceai-t5-moe
2.9 KiB104 linesraw
1import {
2	SSE_DATA_PREFIX,
3	SSE_DONE_MARKER,
4	SSE_LINE_SEPARATOR,
5	SSE_RECORD_SEPARATOR
6} from '$lib/constants';
7
8/**
9 * Minimal SSE-with-JSON stream iterator.
10 *
11 * Yields one event per `\n\n`-separated record. Each event payload is the
12 * decoded `data:` field after JSON-parsing. A `[DONE]` sentinel terminates
13 * the stream early. Malformed records - any record whose `data:` payload
14 * fails `JSON.parse` - are skipped silently: usually a transient mid-stream
15 * fault that the caller should not have to special-case, and the noise of
16 * logging every occurrence on long-running streams outweighs the diagnostic
17 * value.
18 *
19 * Less ambitious than ChatService.handleStreamResponse (no resume, no byte
20 * offset tracking) - suitable for one-shot streams like `/tools?stream=true`
21 * where the consumer just reads chunks until done.
22 */
23
24export interface SseJsonEvent<T = unknown> {
25	data: T;
26}
27
28/**
29 * Splits a raw SSE byte buffer into complete records on the blank-line
30 * boundary, returning the leftover partial record separately. Shared by the
31 * record-based consumers (parseSseJsonStream, models.service).
32 */
33export function splitSseRecords(buffer: string): { records: string[]; rest: string } {
34	const parts = buffer.split(SSE_RECORD_SEPARATOR);
35
36	return { records: parts.slice(0, -1), rest: parts[parts.length - 1] ?? '' };
37}
38
39/**
40 * Extracts the joined `data:` payload from one SSE record (the data lines
41 * concatenated with a newline), or an empty string when the record carries
42 * no data lines. Used by models.service to parse status envelopes.
43 */
44export function extractSseDataPayload(record: string): string {
45	return record
46		.split(SSE_LINE_SEPARATOR)
47		.filter((line) => line.startsWith(SSE_DATA_PREFIX))
48		.map((line) => line.slice(SSE_DATA_PREFIX.length).trim())
49		.join(SSE_LINE_SEPARATOR);
50}
51
52export async function* parseSseJsonStream<T = unknown>(
53	response: Response,
54	signal?: AbortSignal
55): AsyncGenerator<SseJsonEvent<T>> {
56	const reader = response.body?.getReader();
57
58	if (!reader) return;
59
60	const decoder = new TextDecoder();
61
62	let buffer = '';
63
64	try {
65		while (true) {
66			if (signal?.aborted) return;
67
68			const { done, value } = await reader.read();
69
70			if (done) break;
71
72			buffer += decoder.decode(value, { stream: true });
73			const { records, rest } = splitSseRecords(buffer);
74
75			buffer = rest;
76
77			for (const record of records) {
78				if (!record) continue;
79
80				for (const line of record.split(SSE_LINE_SEPARATOR)) {
81					if (!line.startsWith(SSE_DATA_PREFIX)) continue;
82
83					const payload = line.slice(SSE_DATA_PREFIX.length).trim();
84
85					if (payload === SSE_DONE_MARKER) return;
86
87					if (!payload) continue;
88
89					try {
90						yield { data: JSON.parse(payload) as T };
91					} catch {
92						// Skip silently per the function contract above.
93					}
94				}
95			}
96		}
97	} finally {
98		try {
99			reader.releaseLock();
100		} catch (error) {
101			console.error('[sse] failed to release reader lock:', error);
102		}
103	}
104}