"backend/apps/webui/main.py" did not exist on "9a95767062f0ec600fc9a001055789cc52dbb66c"
index.ts 3.23 KB
Newer Older
1
2
3
import { EventSourceParserStream } from 'eventsource-parser/stream';
import type { ParsedEvent } from 'eventsource-parser';

4
5
6
type TextStreamUpdate = {
	done: boolean;
	value: string;
7
8
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	citations?: any;
9
10
	// eslint-disable-next-line @typescript-eslint/no-explicit-any
	error?: any;
11
12
13
14
15
16
17
18
19
20
	usage?: ResponseUsage;
};

type ResponseUsage = {
	/** Including images and tools if any */
	prompt_tokens: number;
	/** The tokens generated */
	completion_tokens: number;
	/** Sum of the above two fields */
	total_tokens: number;
21
22
};

23
// createOpenAITextStream takes a responseBody with a SSE response,
24
25
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
export async function createOpenAITextStream(
26
	responseBody: ReadableStream<Uint8Array>,
27
	splitLargeDeltas: boolean
28
): Promise<AsyncGenerator<TextStreamUpdate>> {
29
30
31
32
33
	const eventStream = responseBody
		.pipeThrough(new TextDecoderStream())
		.pipeThrough(new EventSourceParserStream())
		.getReader();
	let iterator = openAIStreamToIterator(eventStream);
34
35
36
37
	if (splitLargeDeltas) {
		iterator = streamLargeDeltasAsRandomChunks(iterator);
	}
	return iterator;
38
39
40
}

async function* openAIStreamToIterator(
41
	reader: ReadableStreamDefaultReader<ParsedEvent>
42
43
44
45
46
47
48
): AsyncGenerator<TextStreamUpdate> {
	while (true) {
		const { value, done } = await reader.read();
		if (done) {
			yield { done: true, value: '' };
			break;
		}
49
50
51
52
53
54
55
56
57
58
59
60
		if (!value) {
			continue;
		}
		const data = value.data;
		if (data.startsWith('[DONE]')) {
			yield { done: true, value: '' };
			break;
		}

		try {
			const parsedData = JSON.parse(data);
			console.log(parsedData);
61

62
63
64
65
66
			if (parsedData.error) {
				yield { done: true, value: '', error: parsedData.error };
				break;
			}

67
68
69
70
71
			if (parsedData.citations) {
				yield { done: false, value: '', citations: parsedData.citations };
				continue;
			}

72
73
74
75
76
			yield {
				done: false,
				value: parsedData.choices?.[0]?.delta?.content ?? '',
				usage: parsedData.usage
			};
77
78
		} catch (e) {
			console.error('Error extracting delta from SSE event:', e);
79
80
81
82
83
84
85
86
87
88
89
90
91
92
		}
	}
}

// streamLargeDeltasAsRandomChunks will chunk large deltas (length > 5) into random sized chunks between 1-3 characters
// This is to simulate a more fluid streaming, even though some providers may send large chunks of text at once
async function* streamLargeDeltasAsRandomChunks(
	iterator: AsyncGenerator<TextStreamUpdate>
): AsyncGenerator<TextStreamUpdate> {
	for await (const textStreamUpdate of iterator) {
		if (textStreamUpdate.done) {
			yield textStreamUpdate;
			return;
		}
93
94
95
96
		if (textStreamUpdate.citations) {
			yield textStreamUpdate;
			continue;
		}
97
98
99
100
101
102
103
104
105
		let content = textStreamUpdate.value;
		if (content.length < 5) {
			yield { done: false, value: content };
			continue;
		}
		while (content != '') {
			const chunkSize = Math.min(Math.floor(Math.random() * 3) + 1, content.length);
			const chunk = content.slice(0, chunkSize);
			yield { done: false, value: chunk };
106
107
108
109
110
			// Do not sleep if the tab is hidden
			// Timers are throttled to 1s in hidden tabs
			if (document?.visibilityState !== 'hidden') {
				await sleep(5);
			}
111
112
113
114
115
116
			content = content.slice(chunkSize);
		}
	}
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));