Skip to content

Commit 05caa13

Browse files
openai responses instrumentation (#6583)
* openai responses instrumentation * clean up code * add streaming processing, fix span tag adding * make changes to conform with updated tool call tagging behavior tests * fix tags after merge master * remove unecessary stream chunk processing, refactor metadata tags * address comments, remove unecessary empty content tag handling * run linter * move const allowedParams to top of file * clean up * add test for openai response span * add streamed test, fix non-streamed response test * add cassettes * remove default to empty string for non-existent name case * lint * fix test * lint * Update packages/datadog-plugin-openai/src/stream-helpers.js Co-authored-by: Ruben Bridgewater <ruben@bridgewater.de> * Update packages/dd-trace/src/llmobs/plugins/openai.js Co-authored-by: Ruben Bridgewater <ruben@bridgewater.de> * clean up response chunk extracting * fix tests * lint remove expect statements --------- Co-authored-by: Ruben Bridgewater <ruben@bridgewater.de>
1 parent a28b6bb commit 05caa13

File tree

10 files changed

+849
-20
lines changed

10 files changed

+849
-20
lines changed

packages/datadog-instrumentations/src/openai.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ const V4_PACKAGE_SHIMS = [
2222
methods: ['create'],
2323
streamedResponse: true
2424
},
25+
{
26+
file: 'resources/responses/responses',
27+
targetClass: 'Responses',
28+
baseResource: 'responses',
29+
methods: ['create'],
30+
streamedResponse: true,
31+
versions: ['>=4.87.0']
32+
},
2533
{
2634
file: 'resources/embeddings',
2735
targetClass: 'Embeddings',

packages/datadog-plugin-openai/src/stream-helpers.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,33 @@ function constructChatCompletionResponseFromStreamedChunks (chunks, n) {
107107
})
108108
}
109109

110+
/**
111+
* Constructs the entire response from a stream of OpenAI responses chunks.
112+
* The responses API uses event-based streaming with delta chunks.
113+
* @param {Array<Record<string, any>>} chunks
114+
* @returns {Record<string, any>}
115+
*/
116+
function constructResponseResponseFromStreamedChunks (chunks) {
117+
// The responses API streams events with different types:
118+
// - response.output_text.delta: incremental text deltas
119+
// - response.output_text.done: complete text for a content part
120+
// - response.output_item.done: complete output item with role
121+
// - response.done/response.incomplete/response.completed: final response with output array and usage
122+
123+
// Find the last chunk with a complete response object (status: done, incomplete, or completed)
124+
const responseStatusSet = new Set(['done', 'incomplete', 'completed'])
125+
126+
for (let i = chunks.length - 1; i >= 0; i--) {
127+
const chunk = chunks[i]
128+
if (chunk.response && responseStatusSet.has(chunk.response.status)) {
129+
return chunk.response
130+
}
131+
}
132+
}
133+
110134
module.exports = {
111135
convertBuffersToObjects,
112136
constructCompletionResponseFromStreamedChunks,
113-
constructChatCompletionResponseFromStreamedChunks
137+
constructChatCompletionResponseFromStreamedChunks,
138+
constructResponseResponseFromStreamedChunks
114139
}

packages/datadog-plugin-openai/src/tracing.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const { MEASURED } = require('../../../ext/tags')
1111
const {
1212
convertBuffersToObjects,
1313
constructCompletionResponseFromStreamedChunks,
14-
constructChatCompletionResponseFromStreamedChunks
14+
constructChatCompletionResponseFromStreamedChunks,
15+
constructResponseResponseFromStreamedChunks
1516
} = require('./stream-helpers')
1617

1718
const { DD_MAJOR } = require('../../../version')
@@ -59,6 +60,8 @@ class OpenAiTracingPlugin extends TracingPlugin {
5960
response = constructCompletionResponseFromStreamedChunks(chunks, n)
6061
} else if (methodName === 'createChatCompletion') {
6162
response = constructChatCompletionResponseFromStreamedChunks(chunks, n)
63+
} else if (methodName === 'createResponse') {
64+
response = constructResponseResponseFromStreamedChunks(chunks)
6265
}
6366

6467
ctx.result = { data: response }
@@ -134,6 +137,10 @@ class OpenAiTracingPlugin extends TracingPlugin {
134137
case 'createEdit':
135138
createEditRequestExtraction(tags, payload, openaiStore)
136139
break
140+
141+
case 'createResponse':
142+
createResponseRequestExtraction(tags, payload, openaiStore)
143+
break
137144
}
138145

139146
span.addTags(tags)
@@ -313,6 +320,10 @@ function normalizeMethodName (methodName) {
313320
case 'embeddings.create':
314321
return 'createEmbedding'
315322

323+
// responses
324+
case 'responses.create':
325+
return 'createResponse'
326+
316327
// files
317328
case 'files.create':
318329
return 'createFile'
@@ -376,6 +387,16 @@ function createEditRequestExtraction (tags, payload, openaiStore) {
376387
openaiStore.instruction = instruction
377388
}
378389

390+
function createResponseRequestExtraction (tags, payload, openaiStore) {
391+
// Extract model information
392+
if (payload.model) {
393+
tags['openai.request.model'] = payload.model
394+
}
395+
396+
// Store the full payload for response extraction
397+
openaiStore.responseData = payload
398+
}
399+
379400
function retrieveModelRequestExtraction (tags, payload) {
380401
tags['openai.request.id'] = payload.id
381402
}
@@ -410,6 +431,10 @@ function responseDataExtractionByMethod (methodName, tags, body, openaiStore) {
410431
commonCreateResponseExtraction(tags, body, openaiStore, methodName)
411432
break
412433

434+
case 'createResponse':
435+
createResponseResponseExtraction(tags, body, openaiStore)
436+
break
437+
413438
case 'listFiles':
414439
case 'listFineTunes':
415440
case 'listFineTuneEvents':
@@ -513,6 +538,26 @@ function commonCreateResponseExtraction (tags, body, openaiStore, methodName) {
513538
openaiStore.choices = body.choices
514539
}
515540

541+
function createResponseResponseExtraction (tags, body, openaiStore) {
542+
// Extract response ID if available
543+
if (body.id) {
544+
tags['openai.response.id'] = body.id
545+
}
546+
547+
// Extract status if available
548+
if (body.status) {
549+
tags['openai.response.status'] = body.status
550+
}
551+
552+
// Extract model from response if available
553+
if (body.model) {
554+
tags['openai.response.model'] = body.model
555+
}
556+
557+
// Store the full response for potential future use
558+
openaiStore.response = body
559+
}
560+
516561
// The server almost always responds with JSON
517562
function coerceResponseBody (body, methodName) {
518563
switch (methodName) {

0 commit comments

Comments
 (0)