matthoffner commited on
Commit
3406d52
·
1 Parent(s): 49f567f

Create fetch-sse.ts

Browse files
Files changed (1) hide show
  1. 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
+ }