dd-trace 5.108.0 → 5.109.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +22 -1
- package/package.json +2 -1
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/next.js +34 -0
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/vitest.js +60 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-vitest/src/index.js +4 -1
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -0
- package/packages/dd-trace/src/config/supported-configurations.json +24 -2
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Returns the value as a string, JSON-stringifying it when it is not already a string.
|
|
5
|
-
* Returns the value unchanged when it is `null` or `undefined`.
|
|
6
|
-
*
|
|
7
|
-
* @param {unknown} value
|
|
8
|
-
* @returns {string|undefined|null}
|
|
9
|
-
*/
|
|
10
|
-
function stringifyIfNeeded (value) {
|
|
11
|
-
if (value == null) return value
|
|
12
|
-
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
3
|
const FILE_FALLBACK = '[file]'
|
|
16
4
|
const IMAGE_FALLBACK = '[image]'
|
|
17
5
|
|
|
@@ -39,138 +27,30 @@ const OPENAI_RESPONSE_TOOL_OUTPUT_TYPES = new Set([
|
|
|
39
27
|
])
|
|
40
28
|
|
|
41
29
|
/**
|
|
42
|
-
* Returns a
|
|
30
|
+
* Returns the value as a string, JSON-stringifying it when it is not already a string.
|
|
31
|
+
* Returns the value unchanged when it is `null` or `undefined`.
|
|
43
32
|
*
|
|
44
33
|
* @param {unknown} value
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
function stringifyOrEmpty (value) {
|
|
48
|
-
return stringifyIfNeeded(value) ?? ''
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Converts a LanguageModelV2FilePart with an image mediaType to an AI guard style image_url content part.
|
|
53
|
-
*
|
|
54
|
-
* @param {{type: 'file', data: URL|string|Uint8Array, mediaType: string}} part
|
|
55
|
-
* @returns {{type: 'image_url', image_url: {url: string}}|undefined}
|
|
34
|
+
* @returns {string|undefined|null}
|
|
56
35
|
*/
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (data instanceof URL) {
|
|
61
|
-
return { type: 'image_url', image_url: { url: data.toString() } }
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (typeof data === 'string') {
|
|
65
|
-
if (data.startsWith('http') || data.startsWith('data:')) {
|
|
66
|
-
return { type: 'image_url', image_url: { url: data } }
|
|
67
|
-
}
|
|
68
|
-
return { type: 'image_url', image_url: { url: `data:${mediaType};base64,${data}` } }
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (data instanceof Uint8Array) {
|
|
72
|
-
return { type: 'image_url', image_url: { url: `data:${mediaType};base64,${Buffer.from(data).toString('base64')}` } }
|
|
73
|
-
}
|
|
36
|
+
function stringifyIfNeeded (value) {
|
|
37
|
+
if (value == null) return value
|
|
38
|
+
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
74
39
|
}
|
|
75
40
|
|
|
76
41
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* Vercel AI v2 prompt entries use content arrays with typed parts (e.g. { type: 'text', text },
|
|
80
|
-
* { type: 'file', data, mediaType }). This function converts them to AI guard style messages.
|
|
81
|
-
* When file parts with image media types are present, the content is an array of text and
|
|
82
|
-
* image_url parts; otherwise it is a plain string.
|
|
42
|
+
* Returns a stringified value, falling back to an empty string for absent values.
|
|
83
43
|
*
|
|
84
|
-
* @param {
|
|
85
|
-
* @returns {
|
|
44
|
+
* @param {unknown} value
|
|
45
|
+
* @returns {string}
|
|
86
46
|
*/
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const messages = []
|
|
91
|
-
for (const msg of prompt) {
|
|
92
|
-
switch (msg.role) {
|
|
93
|
-
case 'system':
|
|
94
|
-
messages.push({ role: 'system', content: typeof msg.content === 'string' ? msg.content : '' })
|
|
95
|
-
break
|
|
96
|
-
|
|
97
|
-
case 'user': {
|
|
98
|
-
if (!Array.isArray(msg.content)) break
|
|
99
|
-
|
|
100
|
-
const contentParts = []
|
|
101
|
-
for (const part of msg.content) {
|
|
102
|
-
if (part.type === 'text') {
|
|
103
|
-
contentParts.push({ type: 'text', text: part.text })
|
|
104
|
-
} else if (part.type === 'file' && part.mediaType?.startsWith('image/')) {
|
|
105
|
-
const converted = convertFilePartToImageUrl(part)
|
|
106
|
-
if (converted) contentParts.push(converted)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (contentParts.length === 0) break
|
|
111
|
-
|
|
112
|
-
const hasImages = contentParts.some(p => p.type === 'image_url')
|
|
113
|
-
if (hasImages) {
|
|
114
|
-
messages.push({ role: 'user', content: contentParts })
|
|
115
|
-
} else {
|
|
116
|
-
messages.push({ role: 'user', content: contentParts.map(p => p.text).join('\n') })
|
|
117
|
-
}
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
case 'assistant': {
|
|
122
|
-
const textParts = []
|
|
123
|
-
const toolCalls = []
|
|
124
|
-
if (!Array.isArray(msg.content)) break
|
|
125
|
-
|
|
126
|
-
for (const part of msg.content) {
|
|
127
|
-
if (part.type === 'text') {
|
|
128
|
-
textParts.push(part.text)
|
|
129
|
-
} else if (part.type === 'tool-call') {
|
|
130
|
-
toolCalls.push({
|
|
131
|
-
id: part.toolCallId,
|
|
132
|
-
function: {
|
|
133
|
-
name: part.toolName,
|
|
134
|
-
arguments: stringifyIfNeeded(part.args ?? part.input),
|
|
135
|
-
},
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (toolCalls.length > 0) {
|
|
141
|
-
messages.push({ role: 'assistant', tool_calls: toolCalls })
|
|
142
|
-
} else if (textParts.length > 0) {
|
|
143
|
-
messages.push({ role: 'assistant', content: textParts.join('\n') })
|
|
144
|
-
}
|
|
145
|
-
break
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
case 'tool': {
|
|
149
|
-
if (!Array.isArray(msg.content)) break
|
|
150
|
-
|
|
151
|
-
for (const part of msg.content) {
|
|
152
|
-
if (part.type === 'tool-result') {
|
|
153
|
-
messages.push({
|
|
154
|
-
role: 'tool',
|
|
155
|
-
tool_call_id: part.toolCallId,
|
|
156
|
-
content: stringifyIfNeeded(part.result ?? part.output),
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
break
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return messages
|
|
47
|
+
function stringifyOrEmpty (value) {
|
|
48
|
+
return stringifyIfNeeded(value) ?? ''
|
|
165
49
|
}
|
|
166
50
|
|
|
167
51
|
/**
|
|
168
52
|
* Converts OpenAI chat-completions messages to the message format expected by AI Guard.
|
|
169
53
|
*
|
|
170
|
-
* Modern `tool_calls` messages already match the expected shape. Deprecated chat
|
|
171
|
-
* completions `function_call` and `function` role messages are normalized to the
|
|
172
|
-
* equivalent tool-call shape so AI Guard can classify them as tool interactions.
|
|
173
|
-
*
|
|
174
54
|
* @param {Array<object>} messages
|
|
175
55
|
* @returns {Array<object>|undefined}
|
|
176
56
|
*/
|
|
@@ -217,55 +97,36 @@ function normalizeOpenAIChatMessage (message) {
|
|
|
217
97
|
}
|
|
218
98
|
|
|
219
99
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* @param {Array<object>} inputMessages - The input messages already in AI guard style format
|
|
223
|
-
* @param {Array<{toolCallId: string, toolName: string, args?: unknown, input?: unknown}>} toolCalls
|
|
224
|
-
* @returns {Array<object>}
|
|
225
|
-
*/
|
|
226
|
-
function buildToolCallOutputMessages (inputMessages, toolCalls) {
|
|
227
|
-
return [
|
|
228
|
-
...inputMessages,
|
|
229
|
-
{
|
|
230
|
-
role: 'assistant',
|
|
231
|
-
tool_calls: toolCalls.map(tc => ({
|
|
232
|
-
id: tc.toolCallId,
|
|
233
|
-
function: {
|
|
234
|
-
name: tc.toolName,
|
|
235
|
-
arguments: stringifyIfNeeded(tc.args ?? tc.input),
|
|
236
|
-
},
|
|
237
|
-
})),
|
|
238
|
-
},
|
|
239
|
-
]
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Builds OpenAI-style output messages for the assistant's text response.
|
|
100
|
+
* Extracts OpenAI input messages from a `chat.completions.create` call.
|
|
244
101
|
*
|
|
245
|
-
* @param {
|
|
246
|
-
* @
|
|
247
|
-
* @returns {Array<object>}
|
|
102
|
+
* @param {object} callArgs - First argument passed to the wrapped method
|
|
103
|
+
* @returns {Array<object>|undefined}
|
|
248
104
|
*/
|
|
249
|
-
function
|
|
250
|
-
return
|
|
251
|
-
...inputMessages,
|
|
252
|
-
{ role: 'assistant', content: text },
|
|
253
|
-
]
|
|
105
|
+
function getChatCompletionsInputMessages (callArgs) {
|
|
106
|
+
return normalizeOpenAIChatMessages(callArgs?.messages)
|
|
254
107
|
}
|
|
255
108
|
|
|
256
109
|
/**
|
|
257
|
-
*
|
|
110
|
+
* Extracts OpenAI output messages from a `chat.completions.create` parsed body.
|
|
258
111
|
*
|
|
259
|
-
* @param {
|
|
260
|
-
* @param {Array<{type: string}>} content - Vercel AI content array from doGenerate/doStream result
|
|
112
|
+
* @param {object} body - Parsed response body
|
|
261
113
|
* @returns {Array<object>}
|
|
262
114
|
*/
|
|
263
|
-
function
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
115
|
+
function getChatCompletionsOutputMessages (body) {
|
|
116
|
+
const eligible = []
|
|
117
|
+
const choices = Array.isArray(body?.choices) ? body.choices : []
|
|
118
|
+
for (const choice of choices) {
|
|
119
|
+
const message = choice?.message
|
|
120
|
+
if (
|
|
121
|
+
message?.content != null ||
|
|
122
|
+
message?.tool_calls?.length ||
|
|
123
|
+
message?.refusal != null ||
|
|
124
|
+
message?.function_call != null
|
|
125
|
+
) {
|
|
126
|
+
eligible.push(message)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return normalizeOpenAIChatMessages(eligible) ?? []
|
|
269
130
|
}
|
|
270
131
|
|
|
271
132
|
/**
|
|
@@ -294,11 +155,6 @@ function convertOpenAIResponseItemsToMessages (items, defaultRole) {
|
|
|
294
155
|
/**
|
|
295
156
|
* Converts OpenAI reusable prompt variables to user messages for AI Guard.
|
|
296
157
|
*
|
|
297
|
-
* The reusable prompt template body is not available on the request, but its
|
|
298
|
-
* variables are user/application-provided content that OpenAI substitutes into
|
|
299
|
-
* the prompt. Screening them closes prompt-only `responses.create({ prompt })`
|
|
300
|
-
* calls and prompt variables used alongside `input`.
|
|
301
|
-
*
|
|
302
158
|
* @param {{variables?: Record<string, string|object>|null}|undefined|null} prompt
|
|
303
159
|
* @returns {Array<object>}
|
|
304
160
|
*/
|
|
@@ -315,14 +171,55 @@ function convertOpenAIResponsePromptToMessages (prompt) {
|
|
|
315
171
|
}
|
|
316
172
|
|
|
317
173
|
/**
|
|
318
|
-
*
|
|
174
|
+
* Extracts OpenAI input messages from a `responses.create` call.
|
|
319
175
|
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
176
|
+
* @param {object} callArgs - First argument passed to the wrapped method
|
|
177
|
+
* @returns {Array<object>|undefined}
|
|
178
|
+
*/
|
|
179
|
+
function getResponsesInputMessages (callArgs) {
|
|
180
|
+
const messages = [
|
|
181
|
+
...convertOpenAIResponseItemsToMessages(callArgs?.input, 'user'),
|
|
182
|
+
...convertOpenAIResponsePromptToMessages(callArgs?.prompt),
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
const instructions = typeof callArgs?.instructions === 'string' && callArgs.instructions.length
|
|
186
|
+
? callArgs.instructions
|
|
187
|
+
: undefined
|
|
188
|
+
if (!instructions) return messages.length ? messages : undefined
|
|
189
|
+
|
|
190
|
+
const first = messages[0]
|
|
191
|
+
if (first && (first.role === 'developer' || first.role === 'system')) {
|
|
192
|
+
const merged = { role: 'developer', content: mergeInstructionsWithContent(instructions, first.content) }
|
|
193
|
+
return [merged, ...messages.slice(1)]
|
|
194
|
+
}
|
|
195
|
+
return [{ role: 'developer', content: instructions }, ...messages]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Merges Responses API instructions with an existing leading developer/system content value.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} instructions
|
|
202
|
+
* @param {string|Array<object>|undefined} content
|
|
203
|
+
* @returns {string|Array<object>}
|
|
204
|
+
*/
|
|
205
|
+
function mergeInstructionsWithContent (instructions, content) {
|
|
206
|
+
if (Array.isArray(content)) return [{ type: 'text', text: instructions }, ...content]
|
|
207
|
+
if (typeof content === 'string' && content.length) return `${instructions}\n\n${content}`
|
|
208
|
+
return instructions
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extracts OpenAI output messages from a `responses.create` parsed body.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} body - Parsed response body
|
|
215
|
+
* @returns {Array<object>}
|
|
216
|
+
*/
|
|
217
|
+
function getResponsesOutputMessages (body) {
|
|
218
|
+
return convertOpenAIResponseItemsToMessages(body?.output, 'assistant')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Converts one OpenAI reusable prompt variable value to message content.
|
|
326
223
|
*
|
|
327
224
|
* @param {string|object} value
|
|
328
225
|
* @returns {string|Array<{type: string, text?: string, image_url?: {url: string}}>|undefined}
|
|
@@ -366,10 +263,6 @@ function openAIResponseItemToMessage (item, defaultRole) {
|
|
|
366
263
|
/**
|
|
367
264
|
* Converts a Responses API tool-call item to one or more chat-style messages.
|
|
368
265
|
*
|
|
369
|
-
* Most tool-call items represent only the assistant's tool request. MCP and
|
|
370
|
-
* image-generation items can also carry tool output on the same item, so include
|
|
371
|
-
* a linked tool message when output-like fields are present.
|
|
372
|
-
*
|
|
373
266
|
* @param {object} item
|
|
374
267
|
* @returns {object|Array<object>}
|
|
375
268
|
*/
|
|
@@ -478,13 +371,12 @@ function openAIResponseFileContentPart (part) {
|
|
|
478
371
|
}
|
|
479
372
|
|
|
480
373
|
module.exports = {
|
|
481
|
-
convertVercelPromptToMessages,
|
|
482
|
-
convertFilePartToImageUrl,
|
|
483
374
|
normalizeOpenAIChatMessages,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
buildOutputMessages,
|
|
375
|
+
getChatCompletionsInputMessages,
|
|
376
|
+
getChatCompletionsOutputMessages,
|
|
487
377
|
convertOpenAIResponseItemsToMessages,
|
|
488
378
|
convertOpenAIResponsePromptToMessages,
|
|
379
|
+
getResponsesInputMessages,
|
|
380
|
+
getResponsesOutputMessages,
|
|
489
381
|
openAIResponseContentToMessageContent,
|
|
490
382
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the value as a string, JSON-stringifying it when it is not already a string.
|
|
5
|
+
* Returns the value unchanged when it is `null` or `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {string|undefined|null}
|
|
9
|
+
*/
|
|
10
|
+
function stringifyIfNeeded (value) {
|
|
11
|
+
if (value == null) return value
|
|
12
|
+
return typeof value === 'string' ? value : JSON.stringify(value)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts a LanguageModelV2FilePart with an image mediaType to an AI Guard style image_url content part.
|
|
17
|
+
*
|
|
18
|
+
* @param {{type: 'file', data: URL|string|Uint8Array, mediaType: string}} part
|
|
19
|
+
* @returns {{type: 'image_url', image_url: {url: string}}|undefined}
|
|
20
|
+
*/
|
|
21
|
+
function convertFilePartToImageUrl (part) {
|
|
22
|
+
const { data, mediaType } = part
|
|
23
|
+
|
|
24
|
+
if (data instanceof URL) {
|
|
25
|
+
return { type: 'image_url', image_url: { url: data.toString() } }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof data === 'string') {
|
|
29
|
+
if (data.startsWith('http') || data.startsWith('data:')) {
|
|
30
|
+
return { type: 'image_url', image_url: { url: data } }
|
|
31
|
+
}
|
|
32
|
+
return { type: 'image_url', image_url: { url: `data:${mediaType};base64,${data}` } }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (data instanceof Uint8Array) {
|
|
36
|
+
return { type: 'image_url', image_url: { url: `data:${mediaType};base64,${Buffer.from(data).toString('base64')}` } }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Converts a LanguageModelV2Prompt to the AI Guard style message format.
|
|
42
|
+
*
|
|
43
|
+
* @param {Array<{role: string, content: string|Array<{type: string}>}>} prompt
|
|
44
|
+
* @returns {Array<{role: string, content?: string|Array<{type: string}>, tool_calls?: Array, tool_call_id?: string}>}
|
|
45
|
+
*/
|
|
46
|
+
function convertVercelPromptToMessages (prompt) {
|
|
47
|
+
if (!Array.isArray(prompt)) return []
|
|
48
|
+
|
|
49
|
+
const messages = []
|
|
50
|
+
for (const msg of prompt) {
|
|
51
|
+
switch (msg.role) {
|
|
52
|
+
case 'system':
|
|
53
|
+
messages.push({ role: 'system', content: typeof msg.content === 'string' ? msg.content : '' })
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
case 'user': {
|
|
57
|
+
if (!Array.isArray(msg.content)) break
|
|
58
|
+
|
|
59
|
+
const contentParts = []
|
|
60
|
+
for (const part of msg.content) {
|
|
61
|
+
if (part.type === 'text') {
|
|
62
|
+
contentParts.push({ type: 'text', text: part.text })
|
|
63
|
+
} else if (part.type === 'file' && part.mediaType?.startsWith('image/')) {
|
|
64
|
+
const converted = convertFilePartToImageUrl(part)
|
|
65
|
+
if (converted) contentParts.push(converted)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (contentParts.length === 0) break
|
|
70
|
+
|
|
71
|
+
const hasImages = contentParts.some(p => p.type === 'image_url')
|
|
72
|
+
if (hasImages) {
|
|
73
|
+
messages.push({ role: 'user', content: contentParts })
|
|
74
|
+
} else {
|
|
75
|
+
messages.push({ role: 'user', content: contentParts.map(p => p.text).join('\n') })
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'assistant': {
|
|
81
|
+
const textParts = []
|
|
82
|
+
const toolCalls = []
|
|
83
|
+
if (!Array.isArray(msg.content)) break
|
|
84
|
+
|
|
85
|
+
for (const part of msg.content) {
|
|
86
|
+
if (part.type === 'text') {
|
|
87
|
+
textParts.push(part.text)
|
|
88
|
+
} else if (part.type === 'tool-call') {
|
|
89
|
+
toolCalls.push({
|
|
90
|
+
id: part.toolCallId,
|
|
91
|
+
function: {
|
|
92
|
+
name: part.toolName,
|
|
93
|
+
arguments: stringifyIfNeeded(part.args ?? part.input),
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (toolCalls.length > 0) {
|
|
100
|
+
messages.push({ role: 'assistant', tool_calls: toolCalls })
|
|
101
|
+
} else if (textParts.length > 0) {
|
|
102
|
+
messages.push({ role: 'assistant', content: textParts.join('\n') })
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'tool': {
|
|
108
|
+
if (!Array.isArray(msg.content)) break
|
|
109
|
+
|
|
110
|
+
for (const part of msg.content) {
|
|
111
|
+
if (part.type === 'tool-result') {
|
|
112
|
+
messages.push({
|
|
113
|
+
role: 'tool',
|
|
114
|
+
tool_call_id: part.toolCallId,
|
|
115
|
+
content: stringifyIfNeeded(part.result ?? part.output),
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
break
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return messages
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Converts LLM output tool calls to AI Guard style message format.
|
|
128
|
+
*
|
|
129
|
+
* @param {Array<object>} inputMessages - The input messages already in AI Guard style format
|
|
130
|
+
* @param {Array<{toolCallId: string, toolName: string, args?: unknown, input?: unknown}>} toolCalls
|
|
131
|
+
* @returns {Array<object>}
|
|
132
|
+
*/
|
|
133
|
+
function buildToolCallOutputMessages (inputMessages, toolCalls) {
|
|
134
|
+
return [
|
|
135
|
+
...inputMessages,
|
|
136
|
+
{
|
|
137
|
+
role: 'assistant',
|
|
138
|
+
tool_calls: toolCalls.map(tc => ({
|
|
139
|
+
id: tc.toolCallId,
|
|
140
|
+
function: {
|
|
141
|
+
name: tc.toolName,
|
|
142
|
+
arguments: stringifyIfNeeded(tc.args ?? tc.input),
|
|
143
|
+
},
|
|
144
|
+
})),
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Builds OpenAI-style output messages for the assistant's text response.
|
|
151
|
+
*
|
|
152
|
+
* @param {Array<object>} inputMessages - The input messages already in AI Guard style format
|
|
153
|
+
* @param {string} text - The assistant's text response
|
|
154
|
+
* @returns {Array<object>}
|
|
155
|
+
*/
|
|
156
|
+
function buildTextOutputMessages (inputMessages, text) {
|
|
157
|
+
return [
|
|
158
|
+
...inputMessages,
|
|
159
|
+
{ role: 'assistant', content: text },
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parses a Vercel AI content array and dispatches to the appropriate output message builder.
|
|
165
|
+
* Returns `[]` when no assistant tool calls or text content were extractable.
|
|
166
|
+
*
|
|
167
|
+
* @param {Array<object>} inputMessages - The input messages already in AI Guard style format
|
|
168
|
+
* @param {Array<{type: string}>} content - Vercel AI content array from doGenerate/doStream result
|
|
169
|
+
* @returns {Array<object>}
|
|
170
|
+
*/
|
|
171
|
+
function buildOutputMessages (inputMessages, content) {
|
|
172
|
+
const toolCalls = content.filter(c => c.type === 'tool-call')
|
|
173
|
+
if (toolCalls.length) return buildToolCallOutputMessages(inputMessages, toolCalls)
|
|
174
|
+
const text = content.filter(c => c.type === 'text').map(c => c.text).join('\n')
|
|
175
|
+
if (text) return buildTextOutputMessages(inputMessages, text)
|
|
176
|
+
return []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
convertVercelPromptToMessages,
|
|
181
|
+
convertFilePartToImageUrl,
|
|
182
|
+
buildToolCallOutputMessages,
|
|
183
|
+
buildTextOutputMessages,
|
|
184
|
+
buildOutputMessages,
|
|
185
|
+
}
|
|
@@ -23,6 +23,7 @@ module.exports = {
|
|
|
23
23
|
fsOperationStart: dc.channel('apm:fs:operation:start'),
|
|
24
24
|
graphqlMiddlewareChannel: dc.tracingChannel('datadog:apollo:middleware'),
|
|
25
25
|
httpClientRequestStart: dc.channel('apm:http:client:request:start'),
|
|
26
|
+
httpClientResponseStart: dc.channel('apm:http:client:response:start'),
|
|
26
27
|
httpClientResponseFinish: dc.channel('apm:http:client:response:finish'),
|
|
27
28
|
incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
|
|
28
29
|
incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
|