@@ -15,13 +15,15 @@ const V4_PACKAGE_SHIMS = [
15
15
file : 'resources/chat/completions.js' ,
16
16
targetClass : 'Completions' ,
17
17
baseResource : 'chat.completions' ,
18
- methods : [ 'create' ]
18
+ methods : [ 'create' ] ,
19
+ streamedResponse : true
19
20
} ,
20
21
{
21
22
file : 'resources/completions.js' ,
22
23
targetClass : 'Completions' ,
23
24
baseResource : 'completions' ,
24
- methods : [ 'create' ]
25
+ methods : [ 'create' ] ,
26
+ streamedResponse : true
25
27
} ,
26
28
{
27
29
file : 'resources/embeddings.js' ,
@@ -141,9 +143,130 @@ addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, expor
141
143
return exports
142
144
} )
143
145
146
+ function addStreamedChunk ( content , chunk ) {
147
+ return content . choices . map ( ( oldChoice , choiceIdx ) => {
148
+ const newChoice = oldChoice
149
+ const chunkChoice = chunk . choices [ choiceIdx ]
150
+ if ( ! oldChoice . finish_reason ) {
151
+ newChoice . finish_reason = chunkChoice . finish_reason
152
+ }
153
+
154
+ // delta exists on chat completions
155
+ const delta = chunkChoice . delta
156
+
157
+ if ( delta ) {
158
+ const content = delta . content
159
+ if ( content ) {
160
+ if ( newChoice . delta . content ) { // we don't want to append to undefined
161
+ newChoice . delta . content += content
162
+ } else {
163
+ newChoice . delta . content = content
164
+ }
165
+ }
166
+ } else {
167
+ const text = chunkChoice . text
168
+ if ( text ) {
169
+ if ( newChoice . text ) {
170
+ newChoice . text += text
171
+ } else {
172
+ newChoice . text = text
173
+ }
174
+ }
175
+ }
176
+
177
+ // tools only exist on chat completions
178
+ const tools = delta && chunkChoice . delta . tool_calls
179
+
180
+ if ( tools ) {
181
+ newChoice . delta . tool_calls = tools . map ( ( newTool , toolIdx ) => {
182
+ const oldTool = oldChoice . delta . tool_calls [ toolIdx ]
183
+
184
+ if ( oldTool ) {
185
+ oldTool . function . arguments += newTool . function . arguments
186
+ }
187
+
188
+ return oldTool
189
+ } )
190
+ }
191
+
192
+ return newChoice
193
+ } )
194
+ }
195
+
196
+ function buffersToJSON ( chunks = [ ] ) {
197
+ return Buffer
198
+ . concat ( chunks ) // combine the buffers
199
+ . toString ( ) // stringify
200
+ . split ( / (? = d a t a : ) / ) // split on "data:"
201
+ . map ( chunk => chunk . split ( '\n' ) . join ( '' ) ) // remove newlines
202
+ . map ( chunk => chunk . substring ( 6 ) ) // remove 'data: ' from the front
203
+ . slice ( 0 , - 1 ) // remove the last [DONE] message
204
+ . map ( JSON . parse ) // parse all of the returned objects
205
+ }
206
+
207
+ /**
208
+ * For streamed responses, we need to accumulate all of the content in
209
+ * the chunks, and let the combined content be the final response.
210
+ * This way, spans look the same as when not streamed.
211
+ */
212
+ function wrapStreamIterator ( response , options ) {
213
+ let processChunksAsBuffers = false
214
+ const chunks = [ ]
215
+ return function ( itr ) {
216
+ return function ( ) {
217
+ const iterator = itr . apply ( this , arguments )
218
+ shimmer . wrap ( iterator , 'next' , next => function ( ) {
219
+ return next . apply ( this , arguments )
220
+ . then ( res => {
221
+ const { done, value : chunk } = res
222
+
223
+ if ( chunk ) {
224
+ chunks . push ( chunk )
225
+ if ( chunk instanceof Buffer ) {
226
+ // this operation should be safe
227
+ // if one chunk is a buffer (versus a plain object), the rest should be as well
228
+ processChunksAsBuffers = true
229
+ }
230
+ }
231
+
232
+ if ( done ) {
233
+ let content = chunks . filter ( chunk => chunk != null ) // filter null or undefined values
234
+
235
+ if ( chunks ) {
236
+ if ( processChunksAsBuffers ) {
237
+ content = buffersToJSON ( content )
238
+ }
239
+
240
+ content = content . reduce ( ( content , chunk ) => {
241
+ content . choices = addStreamedChunk ( content , chunk )
242
+ return content
243
+ } )
244
+ }
245
+
246
+ finishCh . publish ( {
247
+ headers : response . headers ,
248
+ body : content ,
249
+ path : response . url ,
250
+ method : options . method
251
+ } )
252
+ }
253
+
254
+ return res
255
+ } )
256
+ . catch ( err => {
257
+ errorCh . publish ( { err } )
258
+
259
+ throw err
260
+ } )
261
+ } )
262
+ return iterator
263
+ }
264
+ }
265
+ }
266
+
144
267
for ( const shim of V4_PACKAGE_SHIMS ) {
145
- const { file, targetClass, baseResource, methods } = shim
146
- addHook ( { name : 'openai' , file, versions : shim . versions || [ '>=4' ] } , exports => {
268
+ const { file, targetClass, baseResource, methods, versions , streamedResponse } = shim
269
+ addHook ( { name : 'openai' , file, versions : versions || [ '>=4' ] } , exports => {
147
270
const targetPrototype = exports [ targetClass ] . prototype
148
271
149
272
for ( const methodName of methods ) {
@@ -152,6 +275,11 @@ for (const shim of V4_PACKAGE_SHIMS) {
152
275
return methodFn . apply ( this , arguments )
153
276
}
154
277
278
+ // The OpenAI library lets you set `stream: true` on the options arg to any method
279
+ // However, we only want to handle streamed responses in specific cases
280
+ // chat.completions and completions
281
+ const stream = streamedResponse && arguments [ arguments . length - 1 ] ?. stream
282
+
155
283
const client = this . _client || this . client
156
284
157
285
startCh . publish ( {
@@ -170,12 +298,22 @@ for (const shim of V4_PACKAGE_SHIMS) {
170
298
// the original response is wrapped in a promise, so we need to unwrap it
171
299
. then ( body => Promise . all ( [ this . responsePromise , body ] ) )
172
300
. then ( ( [ { response, options } , body ] ) => {
173
- finishCh . publish ( {
174
- headers : response . headers ,
175
- body,
176
- path : response . url ,
177
- method : options . method
178
- } )
301
+ if ( stream ) {
302
+ if ( body . iterator ) {
303
+ shimmer . wrap ( body , 'iterator' , wrapStreamIterator ( response , options ) )
304
+ } else {
305
+ shimmer . wrap (
306
+ body . response . body , Symbol . asyncIterator , wrapStreamIterator ( response , options )
307
+ )
308
+ }
309
+ } else {
310
+ finishCh . publish ( {
311
+ headers : response . headers ,
312
+ body,
313
+ path : response . url ,
314
+ method : options . method
315
+ } )
316
+ }
179
317
180
318
return body
181
319
} )
0 commit comments