diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index a69e8e3c..cc1ecb91 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -3,6 +3,7 @@ import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import { ChatOptions, getHeaders, LLMApi, LLMUsage } from "../api"; import Locale from "../../locales"; +import { fetchEventSource } from "@microsoft/fetch-event-source"; export class ChatGPTApi implements LLMApi { public ChatPath = "v1/chat/completions"; @@ -71,40 +72,20 @@ export class ChatGPTApi implements LLMApi { options.onFinish(responseText); }; - const res = await fetch(chatPath, chatPayload); - clearTimeout(reqestTimeoutId); - - if (res.status === 401) { - responseText += "\n\n" + Locale.Error.Unauthorized; - return finish(); - } - - if ( - !res.ok || - !res.headers.get("Content-Type")?.includes("stream") || - !res.body - ) { - return options.onError?.(new Error()); - } - - const reader = res.body.getReader(); - const decoder = new TextDecoder("utf-8"); - - while (true) { - const { done, value } = await reader.read(); - if (done) { - return finish(); - } - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split("data: "); - - for (const line of lines) { - const text = line.trim(); - if (line.startsWith("[DONE]")) { + fetchEventSource(chatPath, { + ...chatPayload, + async onopen(res) { + clearTimeout(reqestTimeoutId); + if (res.status === 401) { + responseText += "\n\n" + Locale.Error.Unauthorized; return finish(); } - if (text.length === 0) continue; + }, + onmessage(msg) { + if (msg.data === "[DONE]") { + return finish(); + } + const text = msg.data; try { const json = JSON.parse(text); const delta = json.choices[0].delta.content; @@ -113,10 +94,16 @@ export class ChatGPTApi implements LLMApi { options.onUpdate?.(responseText, delta); } } catch (e) { - console.error("[Request] parse error", text, chunk); + console.error("[Request] parse error", text, msg); } - } - } + }, + onclose() { + finish(); + }, + onerror(e) { + options.onError?.(e); + }, + }); } else { const res = await fetch(chatPath, chatPayload); clearTimeout(reqestTimeoutId); diff --git a/package.json b/package.json index 07ba977e..f9d3c3c7 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@hello-pangea/dnd": "^16.2.0", + "@microsoft/fetch-event-source": "^2.0.1", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", "emoji-picker-react": "^4.4.7", diff --git a/yarn.lock b/yarn.lock index 5240d7e7..e54a69e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,6 +1111,11 @@ dependencies: "@types/react" ">=16.0.0" +"@microsoft/fetch-event-source@^2.0.1": + version "2.0.1" + resolved "https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d" + integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA== + "@next/env@13.4.2": version "13.4.2" resolved "https://registry.npmmirror.com/@next/env/-/env-13.4.2.tgz#cf3ebfd523a33d8404c1216e02ac8d856a73170e"