15
15
* limitations under the License.
16
16
*/
17
17
18
- import { CitationSource , GenerateContentCandidate , GenerateContentResponse , GenerateContentResult , StreamGenerateContentResult , } from './types/content' ;
18
+ import {
19
+ CitationSource ,
20
+ GenerateContentCandidate ,
21
+ GenerateContentResponse ,
22
+ GenerateContentResult ,
23
+ StreamGenerateContentResult ,
24
+ } from './types/content' ;
19
25
20
- // eslint-disable-next-line no-useless-escape
21
- const responseLineRE = / ^ d a t a \: ( .* ) \r \n / ;
26
+ const responseLineRE = / ^ d a t a : ( .* ) (?: \n \n | \r \r | \r \n \r \n ) / ;
22
27
23
- // TODO: set a better type for `reader`. Setting it to
24
- // `ReadableStreamDefaultReader` results in an error (diagnostic code 2304)
25
28
async function * generateResponseSequence (
26
- reader2 : any
29
+ stream : ReadableStream < GenerateContentResponse >
27
30
) : AsyncGenerator < GenerateContentResponse > {
31
+ const reader = stream . getReader ( ) ;
28
32
while ( true ) {
29
- const { value, done} = await reader2 . read ( ) ;
33
+ const { value, done} = await reader . read ( ) ;
30
34
if ( done ) {
31
35
break ;
32
36
}
@@ -35,55 +39,91 @@ async function* generateResponseSequence(
35
39
}
36
40
37
41
/**
38
- * Reads a raw stream from the fetch response and joins incomplete
42
+ * Process a response.body stream from the backend and return an
43
+ * iterator that provides one complete GenerateContentResponse at a time
44
+ * and a promise that resolves with a single aggregated
45
+ * GenerateContentResponse.
46
+ *
47
+ * @param response - Response from a fetch call
48
+ */
49
+ export function processStream (
50
+ response : Response | undefined
51
+ ) : StreamGenerateContentResult {
52
+ if ( response === undefined ) {
53
+ throw new Error ( 'Error processing stream because response === undefined' ) ;
54
+ }
55
+ if ( ! response . body ) {
56
+ throw new Error ( 'Error processing stream because response.body not found' ) ;
57
+ }
58
+ const inputStream = response . body ! . pipeThrough (
59
+ new TextDecoderStream ( 'utf8' , { fatal : true } )
60
+ ) ;
61
+ const responseStream =
62
+ getResponseStream < GenerateContentResponse > ( inputStream ) ;
63
+ const [ stream1 , stream2 ] = responseStream . tee ( ) ;
64
+ return {
65
+ stream : generateResponseSequence ( stream1 ) ,
66
+ response : getResponsePromise ( stream2 ) ,
67
+ } ;
68
+ }
69
+
70
+ async function getResponsePromise (
71
+ stream : ReadableStream < GenerateContentResponse >
72
+ ) : Promise < GenerateContentResponse > {
73
+ const allResponses : GenerateContentResponse [ ] = [ ] ;
74
+ const reader = stream . getReader ( ) ;
75
+ // eslint-disable-next-line no-constant-condition
76
+ while ( true ) {
77
+ const { done, value} = await reader . read ( ) ;
78
+ if ( done ) {
79
+ return aggregateResponses ( allResponses ) ;
80
+ }
81
+ allResponses . push ( value ) ;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Reads a raw stream from the fetch response and join incomplete
39
87
* chunks, returning a new stream that provides a single complete
40
88
* GenerateContentResponse in each iteration.
41
89
*/
42
- function readFromReader (
43
- reader : ReadableStreamDefaultReader
44
- ) : ReadableStream < GenerateContentResponse > {
45
- let currentText = '' ;
46
- const stream = new ReadableStream < GenerateContentResponse > ( {
90
+ export function getResponseStream < T > (
91
+ inputStream : ReadableStream < string >
92
+ ) : ReadableStream < T > {
93
+ const reader = inputStream . getReader ( ) ;
94
+ const stream = new ReadableStream < T > ( {
47
95
start ( controller ) {
96
+ let currentText = '' ;
48
97
return pump ( ) ;
49
98
function pump ( ) : Promise < ( ( ) => Promise < void > ) | undefined > {
50
- let streamReader ;
51
- try {
52
- streamReader = reader . read ( ) . then ( ( { value, done} ) => {
53
- if ( done ) {
54
- controller . close ( ) ;
99
+ return reader . read ( ) . then ( ( { value, done} ) => {
100
+ if ( done ) {
101
+ if ( currentText . trim ( ) ) {
102
+ controller . error ( new Error ( 'Failed to parse stream' ) ) ;
55
103
return ;
56
104
}
57
- const chunk = new TextDecoder ( ) . decode ( value ) ;
58
- currentText += chunk ;
59
- const match = currentText . match ( responseLineRE ) ;
60
- if ( match ) {
61
- let parsedResponse : GenerateContentResponse ;
62
- try {
63
- parsedResponse = JSON . parse (
64
- match [ 1 ]
65
- ) as GenerateContentResponse ;
66
- } catch ( e ) {
67
- throw new Error ( `Error parsing JSON response: "${ match [ 1 ] } "` ) ;
68
- }
69
- currentText = '' ;
70
- if ( 'candidates' in parsedResponse ) {
71
- controller . enqueue ( parsedResponse ) ;
72
- } else {
73
- console . warn (
74
- `No candidates in this response: ${ parsedResponse } `
75
- ) ;
76
- controller . enqueue ( {
77
- candidates : [ ] ,
78
- } ) ;
79
- }
105
+ controller . close ( ) ;
106
+ return ;
107
+ }
108
+
109
+ currentText += value ;
110
+ let match = currentText . match ( responseLineRE ) ;
111
+ let parsedResponse : T ;
112
+ while ( match ) {
113
+ try {
114
+ parsedResponse = JSON . parse ( match [ 1 ] ) as T ;
115
+ } catch ( e ) {
116
+ controller . error (
117
+ new Error ( `Error parsing JSON response: "${ match [ 1 ] } "` )
118
+ ) ;
119
+ return ;
80
120
}
81
- return pump ( ) ;
82
- } ) ;
83
- } catch ( e ) {
84
- throw new Error ( `Error reading from stream ${ e } .` ) ;
85
- }
86
- return streamReader ;
121
+ controller . enqueue ( parsedResponse ) ;
122
+ currentText = currentText . substring ( match [ 0 ] . length ) ;
123
+ match = currentText . match ( responseLineRE ) ;
124
+ }
125
+ return pump ( ) ;
126
+ } ) ;
87
127
}
88
128
} ,
89
129
} ) ;
@@ -121,20 +161,21 @@ function aggregateResponses(
121
161
} as GenerateContentCandidate ;
122
162
}
123
163
if ( response . candidates [ i ] . citationMetadata ) {
124
- if ( ! aggregatedResponse . candidates [ i ]
125
- . citationMetadata ?. citationSources ) {
164
+ if (
165
+ ! aggregatedResponse . candidates [ i ] . citationMetadata ?. citationSources
166
+ ) {
126
167
aggregatedResponse . candidates [ i ] . citationMetadata = {
127
168
citationSources : [ ] as CitationSource [ ] ,
128
169
} ;
129
170
}
130
171
131
-
132
- let existingMetadata = response . candidates [ i ] . citationMetadata ?? { } ;
172
+ const existingMetadata = response . candidates [ i ] . citationMetadata ?? { } ;
133
173
134
174
if ( aggregatedResponse . candidates [ i ] . citationMetadata ) {
135
175
aggregatedResponse . candidates [ i ] . citationMetadata ! . citationSources =
136
- aggregatedResponse . candidates [ i ]
137
- . citationMetadata ! . citationSources . concat ( existingMetadata ) ;
176
+ aggregatedResponse . candidates [
177
+ i
178
+ ] . citationMetadata ! . citationSources . concat ( existingMetadata ) ;
138
179
}
139
180
}
140
181
aggregatedResponse . candidates [ i ] . finishReason =
@@ -157,45 +198,6 @@ function aggregateResponses(
157
198
return aggregatedResponse ;
158
199
}
159
200
160
- // TODO: improve error handling throughout stream processing
161
- /**
162
- * Processes model responses from streamGenerateContent
163
- */
164
- export function processStream (
165
- response : Response | undefined
166
- ) : StreamGenerateContentResult {
167
- if ( response === undefined ) {
168
- throw new Error ( 'Error processing stream because response === undefined' ) ;
169
- }
170
- if ( ! response . body ) {
171
- throw new Error ( 'Error processing stream because response.body not found' ) ;
172
- }
173
- const reader = response . body . getReader ( ) ;
174
- const responseStream = readFromReader ( reader ) ;
175
- const [ stream1 , stream2 ] = responseStream . tee ( ) ;
176
- const reader1 = stream1 . getReader ( ) ;
177
- const reader2 = stream2 . getReader ( ) ;
178
- const allResponses : GenerateContentResponse [ ] = [ ] ;
179
- const responsePromise = new Promise < GenerateContentResponse > (
180
- // eslint-disable-next-line no-async-promise-executor
181
- async resolve => {
182
- // eslint-disable-next-line no-constant-condition
183
- while ( true ) {
184
- const { value, done} = await reader1 . read ( ) ;
185
- if ( done ) {
186
- resolve ( aggregateResponses ( allResponses ) ) ;
187
- return ;
188
- }
189
- allResponses . push ( value ) ;
190
- }
191
- }
192
- ) ;
193
- return {
194
- response : responsePromise ,
195
- stream : generateResponseSequence ( reader2 ) ,
196
- } ;
197
- }
198
-
199
201
/**
200
202
* Process model responses from generateContent
201
203
*/
0 commit comments