2024-04-30 19:42:55 +00:00
|
|
|
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
|
|
|
|
import type { ParsedEvent } from 'eventsource-parser';
|
|
|
|
|
|
2024-04-20 19:03:52 +00:00
|
|
|
type TextStreamUpdate = {
|
|
|
|
|
done: boolean;
|
|
|
|
|
value: string;
|
|
|
|
|
};
|
|
|
|
|
|
2024-04-30 19:42:55 +00:00
|
|
|
// createOpenAITextStream takes a responseBody with a SSE response,
|
2024-04-20 19:03:52 +00:00
|
|
|
// and returns an async generator that emits delta updates with large deltas chunked into random sized chunks
|
|
|
|
|
export async function createOpenAITextStream(
|
2024-04-30 19:42:55 +00:00
|
|
|
responseBody: ReadableStream<Uint8Array>,
|
2024-04-21 09:45:07 +00:00
|
|
|
splitLargeDeltas: boolean
|
2024-04-20 19:03:52 +00:00
|
|
|
): Promise<AsyncGenerator<TextStreamUpdate>> {
|
2024-04-30 19:42:55 +00:00
|
|
|
const eventStream = responseBody
|
|
|
|
|
.pipeThrough(new TextDecoderStream())
|
|
|
|
|
.pipeThrough(new EventSourceParserStream())
|
|
|
|
|
.getReader();
|
|
|
|
|
let iterator = openAIStreamToIterator(eventStream);
|
2024-04-21 09:45:07 +00:00
|
|
|
if (splitLargeDeltas) {
|
|
|
|
|
iterator = streamLargeDeltasAsRandomChunks(iterator);
|
|
|
|
|
}
|
|
|
|
|
return iterator;
|
2024-04-20 19:03:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function* openAIStreamToIterator(
|
2024-04-30 19:42:55 +00:00
|
|
|
reader: ReadableStreamDefaultReader<ParsedEvent>
|
2024-04-20 19:03:52 +00:00
|
|
|
): AsyncGenerator<TextStreamUpdate> {
|
|
|
|
|
while (true) {
|
|
|
|
|
const { value, done } = await reader.read();
|
|
|
|
|
if (done) {
|
|
|
|
|
yield { done: true, value: '' };
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-04-30 19:42:55 +00:00
|
|
|
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);
|
2024-04-20 19:03:52 +00:00
|
|
|
|
2024-04-30 19:42:55 +00:00
|
|
|
yield { done: false, value: parsedData.choices?.[0]?.delta?.content ?? '' };
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Error extracting delta from SSE event:', e);
|
2024-04-20 19:03:52 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
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 };
|
2024-04-28 15:51:36 +00:00
|
|
|
// Do not sleep if the tab is hidden
|
|
|
|
|
// Timers are throttled to 1s in hidden tabs
|
|
|
|
|
if (document?.visibilityState !== 'hidden') {
|
|
|
|
|
await sleep(5);
|
|
|
|
|
}
|
2024-04-20 19:03:52 +00:00
|
|
|
content = content.slice(chunkSize);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|