Spaces:
Paused
Paused
Commit
·
3406d52
1
Parent(s):
49f567f
Create fetch-sse.ts
Browse files- app/editor/fetch-sse.ts +119 -0
app/editor/fetch-sse.ts
ADDED
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createParser } from 'eventsource-parser'
|
2 |
+
|
3 |
+
export async function* streamAsyncIterable<T>(stream: ReadableStream<T>) {
|
4 |
+
const reader = stream.getReader()
|
5 |
+
try {
|
6 |
+
while (true) {
|
7 |
+
const { done, value } = await reader.read()
|
8 |
+
if (done) {
|
9 |
+
return
|
10 |
+
}
|
11 |
+
yield value
|
12 |
+
}
|
13 |
+
} finally {
|
14 |
+
reader.releaseLock()
|
15 |
+
}
|
16 |
+
}
|
17 |
+
|
18 |
+
export class llmError extends Error {
|
19 |
+
statusCode?: number;
|
20 |
+
statusText?: string;
|
21 |
+
isFinal?: boolean;
|
22 |
+
accountId?: string;
|
23 |
+
cause?: any;
|
24 |
+
|
25 |
+
constructor(message: string, cause?: any) {
|
26 |
+
super(message);
|
27 |
+
if (cause) {
|
28 |
+
this.cause = cause;
|
29 |
+
}
|
30 |
+
// Set the prototype explicitly.
|
31 |
+
Object.setPrototypeOf(this, llmError.prototype);
|
32 |
+
}
|
33 |
+
}
|
34 |
+
|
35 |
+
export async function fetchSSE(
|
36 |
+
url: string,
|
37 |
+
options: Parameters<typeof fetch>[1] & {
|
38 |
+
onMessage: (data: string) => void
|
39 |
+
onError?: (error: any) => void
|
40 |
+
},
|
41 |
+
) {
|
42 |
+
const { onMessage, onError, ...fetchOptions } = options
|
43 |
+
const res = await fetch(url, fetchOptions)
|
44 |
+
if (!res.ok) {
|
45 |
+
let reason: string
|
46 |
+
|
47 |
+
try {
|
48 |
+
reason = await res.text()
|
49 |
+
} catch (err) {
|
50 |
+
reason = res.statusText
|
51 |
+
}
|
52 |
+
|
53 |
+
|
54 |
+
const msg = `llm error ${res.status}: ${reason}`
|
55 |
+
const error = new llmError(msg, { cause: res })
|
56 |
+
error.statusCode = res.status
|
57 |
+
error.statusText = res.statusText
|
58 |
+
throw error
|
59 |
+
}
|
60 |
+
|
61 |
+
const parser = createParser((event) => {
|
62 |
+
if (event.type === 'event') {
|
63 |
+
|
64 |
+
onMessage(event.data)
|
65 |
+
}
|
66 |
+
})
|
67 |
+
|
68 |
+
// handle special response errors
|
69 |
+
const feed = (chunk: string) => {
|
70 |
+
let response = null
|
71 |
+
|
72 |
+
try {
|
73 |
+
response = JSON.parse(chunk)
|
74 |
+
} catch {
|
75 |
+
// ignore
|
76 |
+
}
|
77 |
+
|
78 |
+
if (response?.detail?.type === 'invalid_request_error') {
|
79 |
+
const msg = `llm error ${response.detail.message}: ${response.detail.code} (${response.detail.type})`
|
80 |
+
const error = new llmError(msg, { cause: response })
|
81 |
+
error.statusCode = response.detail.code
|
82 |
+
error.statusText = response.detail.message
|
83 |
+
|
84 |
+
if (onError) {
|
85 |
+
onError(error)
|
86 |
+
} else {
|
87 |
+
console.error(error)
|
88 |
+
}
|
89 |
+
|
90 |
+
// don't feed to the event parser
|
91 |
+
return
|
92 |
+
}
|
93 |
+
|
94 |
+
|
95 |
+
parser.feed(chunk)
|
96 |
+
}
|
97 |
+
|
98 |
+
if (!res?.body?.getReader) {
|
99 |
+
// Vercel polyfills `fetch` with `node-fetch`, which doesn't conform to
|
100 |
+
// web standards, so this is a workaround...
|
101 |
+
const body: NodeJS.ReadableStream = res.body as any
|
102 |
+
|
103 |
+
if (!body.on || !body.read) {
|
104 |
+
throw new llmError('unsupported "fetch" implementation')
|
105 |
+
}
|
106 |
+
|
107 |
+
body.on('readable', () => {
|
108 |
+
let chunk: string | Buffer
|
109 |
+
while (null !== (chunk = body.read())) {
|
110 |
+
feed(chunk.toString())
|
111 |
+
}
|
112 |
+
})
|
113 |
+
} else {
|
114 |
+
for await (const chunk of streamAsyncIterable(res.body)) {
|
115 |
+
const str = new TextDecoder().decode(chunk)
|
116 |
+
feed(str)
|
117 |
+
}
|
118 |
+
}
|
119 |
+
}
|