diff --git a/package-lock.json b/package-lock.json index 792060b6..7c2b495d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "axios": "^1.3.4", "esbuild-plugin-svg": "^0.1.0", + "eventsource-parser": "^1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, @@ -1465,6 +1466,14 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.0.0.tgz", + "integrity": "sha512-9jgfSCa3dmEme2ES3mPByGXfgZ87VbP97tng1G2nWwWx6bV2nYxm2AWCrbQjXToSe+yYlqaZNtxffR9IeQr95g==", + "engines": { + "node": ">=14.18" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", diff --git a/package.json b/package.json index 7a65491c..4b9aae9e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "axios": "^1.3.4", "esbuild-plugin-svg": "^0.1.0", + "eventsource-parser": "^1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" } diff --git a/src/openAiStream.ts b/src/openAiStream.ts new file mode 100644 index 00000000..3114f3c3 --- /dev/null +++ b/src/openAiStream.ts @@ -0,0 +1,103 @@ +import { + createParser, + ParsedEvent, + ReconnectInterval, +} from 'eventsource-parser'; +import { ChatMessage } from '@/sharedState'; + +export class OpenAIError extends Error { + type: string; + param: string; + code: string; + + constructor(message: string, type: string, param: string, code: string) { + super(message); + this.name = 'OpenAIError'; + this.type = type; + this.param = param; + this.code = code; + } +} + +export const OpenAIStream = async ( + model: string, + key: string, + messages: ChatMessage[], +) => { + const res = await fetch(`https://api.openai.com/v1/chat/completions`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`, + ...(process.env.OPENAI_ORGANIZATION && { + 'OpenAI-Organization': process.env.OPENAI_ORGANIZATION, + }), + }, + method: 'POST', + body: JSON.stringify({ + model, + messages: [ + { + role: 'system', + content: 'You are a helpful assistant.', + }, + ...messages, + ], + max_tokens: 1000, + temperature: 1, + stream: true, + }), + }); + + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + + if (res.status !== 200) { + const result = await res.json(); + if (result.error) { + throw new OpenAIError( + result.error.message, + result.error.type, + result.error.param, + result.error.code, + ); + } else { + throw new Error( + `OpenAI API returned an error: ${ + decoder.decode(result?.value) || result.statusText + }`, + ); + } + } + + const stream = new ReadableStream({ + async start(controller) { + const onParse = (event: ParsedEvent | ReconnectInterval) => { + if (event.type === 'event') { + const data = event.data; + + if (data === '[DONE]') { + controller.close(); + return; + } + + try { + const json = JSON.parse(data); + const text = json.choices[0].delta.content; + const queue = encoder.encode(text); + controller.enqueue(queue); + } catch (e) { + controller.error(e); + } + } + }; + + const parser = createParser(onParse); + + for await (const chunk of res.body as any) { + parser.feed(decoder.decode(chunk)); + } + }, + }); + + return stream; +};