@@ -5,6 +5,7 @@ const shimmer = require('../../datadog-shimmer')
5
5
6
6
const dc = require ( 'dc-polyfill' )
7
7
const ch = dc . tracingChannel ( 'apm:openai:request' )
8
+ const onStreamedChunkCh = dc . channel ( 'apm:openai:request:chunk' )
8
9
9
10
const V4_PACKAGE_SHIMS = [
10
11
{
@@ -160,119 +161,24 @@ addHook({ name: 'openai', file: 'dist/api.js', versions: ['>=3.0.0 <4'] }, expor
160
161
return exports
161
162
} )
162
163
163
- function addStreamedChunk ( content , chunk ) {
164
- content . usage = chunk . usage // add usage if it was specified to be returned
165
- for ( const choice of chunk . choices ) {
166
- const choiceIdx = choice . index
167
- const oldChoice = content . choices . find ( choice => choice ?. index === choiceIdx )
168
- if ( oldChoice ) {
169
- if ( ! oldChoice . finish_reason ) {
170
- oldChoice . finish_reason = choice . finish_reason
171
- }
172
-
173
- // delta exists on chat completions
174
- const delta = choice . delta
175
-
176
- if ( delta ) {
177
- const content = delta . content
178
- if ( content ) {
179
- if ( oldChoice . delta . content ) { // we don't want to append to undefined
180
- oldChoice . delta . content += content
181
- } else {
182
- oldChoice . delta . content = content
183
- }
184
- }
185
- } else {
186
- const text = choice . text
187
- if ( text ) {
188
- if ( oldChoice . text ) {
189
- oldChoice . text += text
190
- } else {
191
- oldChoice . text = text
192
- }
193
- }
194
- }
195
-
196
- // tools only exist on chat completions
197
- const tools = delta && choice . delta . tool_calls
198
-
199
- if ( tools ) {
200
- oldChoice . delta . tool_calls = tools . map ( ( newTool , toolIdx ) => {
201
- const oldTool = oldChoice . delta . tool_calls ?. [ toolIdx ]
202
-
203
- if ( oldTool ) {
204
- oldTool . function . arguments += newTool . function . arguments
205
- return oldTool
206
- }
207
-
208
- return newTool
209
- } )
210
- }
211
- } else {
212
- // we don't know which choices arrive in which order
213
- content . choices [ choiceIdx ] = choice
214
- }
215
- }
216
- }
217
-
218
- function convertBufferstoObjects ( chunks ) {
219
- return Buffer
220
- . concat ( chunks ) // combine the buffers
221
- . toString ( ) // stringify
222
- . split ( / (? = d a t a : ) / ) // split on "data:"
223
- . map ( chunk => chunk . replaceAll ( '\n' , '' ) . slice ( 6 ) ) // remove newlines and 'data: ' from the front
224
- . slice ( 0 , - 1 ) // remove the last [DONE] message
225
- . map ( JSON . parse ) // parse all of the returned objects
226
- }
227
-
228
164
/**
229
165
* For streamed responses, we need to accumulate all of the content in
230
166
* the chunks, and let the combined content be the final response.
231
167
* This way, spans look the same as when not streamed.
232
168
*/
233
- function wrapStreamIterator ( response , options , n , ctx ) {
234
- let processChunksAsBuffers = false
235
- let chunks = [ ]
169
+ function wrapStreamIterator ( response , options , ctx ) {
236
170
return function ( itr ) {
237
171
return function ( ) {
238
172
const iterator = itr . apply ( this , arguments )
239
173
shimmer . wrap ( iterator , 'next' , next => function ( ) {
240
174
return next . apply ( this , arguments )
241
175
. then ( res => {
242
176
const { done, value : chunk } = res
243
-
244
- if ( chunk ) {
245
- chunks . push ( chunk )
246
- // TODO(BridgeAR): It likely depends on the options being passed
247
- // through if the stream returns buffers or not. By reading that,
248
- // we don't have to do the instanceof check anymore, which is
249
- // relatively expensive.
250
- if ( chunk instanceof Buffer ) {
251
- // this operation should be safe
252
- // if one chunk is a buffer (versus a plain object), the rest should be as well
253
- processChunksAsBuffers = true
254
- }
255
- }
177
+ onStreamedChunkCh . publish ( { ctx, chunk, done } )
256
178
257
179
if ( done ) {
258
- let body = { }
259
- if ( processChunksAsBuffers ) {
260
- chunks = convertBufferstoObjects ( chunks )
261
- }
262
-
263
- if ( chunks . length ) {
264
- // Define the initial body having all the content outside of choices from the first chunk
265
- // this will include import data like created, id, model, etc.
266
- body = { ...chunks [ 0 ] , choices : Array . from ( { length : n } ) }
267
- // Start from the first chunk, and add its choices into the body
268
- for ( const chunk_ of chunks ) {
269
- addStreamedChunk ( body , chunk_ )
270
- }
271
- }
272
-
273
180
finish ( ctx , {
274
181
headers : response . headers ,
275
- data : body ,
276
182
request : {
277
183
path : response . url ,
278
184
method : options . method
@@ -312,17 +218,6 @@ for (const extension of extensions) {
312
218
// chat.completions and completions
313
219
const stream = streamedResponse && getOption ( arguments , 'stream' , false )
314
220
315
- // we need to compute how many prompts we are sending in streamed cases for completions
316
- // not applicable for chat completiond
317
- let n
318
- if ( stream ) {
319
- n = getOption ( arguments , 'n' , 1 )
320
- const prompt = getOption ( arguments , 'prompt' )
321
- if ( Array . isArray ( prompt ) && typeof prompt [ 0 ] !== 'number' ) {
322
- n *= prompt . length
323
- }
324
- }
325
-
326
221
const client = this . _client || this . client
327
222
328
223
const ctx = {
@@ -348,7 +243,7 @@ for (const extension of extensions) {
348
243
const parsedPromise = origApiPromParse . apply ( this , arguments )
349
244
. then ( body => Promise . all ( [ this . responsePromise , body ] ) )
350
245
351
- return handleUnwrappedAPIPromise ( parsedPromise , ctx , stream , n )
246
+ return handleUnwrappedAPIPromise ( parsedPromise , ctx , stream )
352
247
} )
353
248
354
249
return unwrappedPromise
@@ -361,7 +256,7 @@ for (const extension of extensions) {
361
256
const parsedPromise = origApiPromParse . apply ( this , arguments )
362
257
. then ( body => Promise . all ( [ this . responsePromise , body ] ) )
363
258
364
- return handleUnwrappedAPIPromise ( parsedPromise , ctx , stream , n )
259
+ return handleUnwrappedAPIPromise ( parsedPromise , ctx , stream )
365
260
} )
366
261
367
262
ch . end . publish ( ctx )
@@ -375,15 +270,15 @@ for (const extension of extensions) {
375
270
}
376
271
}
377
272
378
- function handleUnwrappedAPIPromise ( apiProm , ctx , stream , n ) {
273
+ function handleUnwrappedAPIPromise ( apiProm , ctx , stream ) {
379
274
return apiProm
380
275
. then ( ( [ { response, options } , body ] ) => {
381
276
if ( stream ) {
382
277
if ( body . iterator ) {
383
- shimmer . wrap ( body , 'iterator' , wrapStreamIterator ( response , options , n , ctx ) )
278
+ shimmer . wrap ( body , 'iterator' , wrapStreamIterator ( response , options , ctx ) )
384
279
} else {
385
280
shimmer . wrap (
386
- body . response . body , Symbol . asyncIterator , wrapStreamIterator ( response , options , n , ctx )
281
+ body . response . body , Symbol . asyncIterator , wrapStreamIterator ( response , options , ctx )
387
282
)
388
283
}
389
284
} else {
@@ -412,7 +307,11 @@ function finish (ctx, response, error) {
412
307
ch . error . publish ( ctx )
413
308
}
414
309
415
- ctx . result = response
310
+ // for successful streamed responses, we've already set the result on ctx.body,
311
+ // so we don't want to override it here
312
+ ctx . result ??= { }
313
+ Object . assign ( ctx . result , response )
314
+
416
315
ch . asyncEnd . publish ( ctx )
417
316
}
418
317
0 commit comments