dd-trace 5.107.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.
Files changed (66) hide show
  1. package/index.d.ts +22 -1
  2. package/package.json +6 -5
  3. package/packages/datadog-instrumentations/src/ai.js +43 -48
  4. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  5. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  7. package/packages/datadog-instrumentations/src/electron.js +1 -1
  8. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  11. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  12. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  13. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  14. package/packages/datadog-instrumentations/src/next.js +34 -0
  15. package/packages/datadog-instrumentations/src/openai.js +77 -18
  16. package/packages/datadog-instrumentations/src/redis.js +0 -1
  17. package/packages/datadog-instrumentations/src/vitest.js +60 -1
  18. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  19. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  20. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  21. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  22. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  23. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  24. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  25. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -7
  26. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  27. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +44 -27
  28. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  29. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  30. package/packages/datadog-plugin-fs/src/index.js +1 -0
  31. package/packages/datadog-plugin-redis/src/index.js +1 -2
  32. package/packages/datadog-plugin-vitest/src/index.js +4 -1
  33. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  34. package/packages/dd-trace/src/aiguard/index.js +11 -49
  35. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  36. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  37. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  38. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  39. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  40. package/packages/dd-trace/src/appsec/channels.js +1 -0
  41. package/packages/dd-trace/src/appsec/downstream_requests.js +114 -60
  42. package/packages/dd-trace/src/appsec/iast/index.js +3 -2
  43. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  44. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  45. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  46. package/packages/dd-trace/src/appsec/rasp/ssrf.js +21 -12
  47. package/packages/dd-trace/src/appsec/reporter.js +1 -1
  48. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  49. package/packages/dd-trace/src/config/supported-configurations.json +31 -2
  50. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -0
  51. package/packages/dd-trace/src/dogstatsd.js +15 -8
  52. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  53. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  54. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  55. package/packages/dd-trace/src/openfeature/flagging_provider.js +8 -1
  56. package/packages/dd-trace/src/plugins/ci_plugin.js +27 -2
  57. package/packages/dd-trace/src/plugins/index.js +3 -0
  58. package/packages/dd-trace/src/profiling/config.js +2 -0
  59. package/packages/dd-trace/src/profiling/profilers/events.js +26 -4
  60. package/packages/dd-trace/src/profiling/profilers/space.js +3 -1
  61. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  62. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  63. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  64. package/vendor/dist/protobufjs/index.js +1 -1
  65. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  66. 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 stringified value, falling back to an empty string for absent values.
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 convertFilePartToImageUrl (part) {
58
- const { data, mediaType } = part
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
- * Converts a LanguageModelV2Prompt to the AI guard style message format.
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 {Array<{role: string, content: string|Array<{type: string}>}>} prompt
85
- * @returns {Array<{role: string, content?: string|Array<{type: string}>, tool_calls?: Array, tool_call_id?: string}>}
44
+ * @param {unknown} value
45
+ * @returns {string}
86
46
  */
87
- function convertVercelPromptToMessages (prompt) {
88
- if (!Array.isArray(prompt)) return []
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
- * Converts LLM output tool calls to AI guard style message format.
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 {Array<object>} inputMessages - The input messages already in AI guard style format
246
- * @param {string} text - The assistant's text response
247
- * @returns {Array<object>}
102
+ * @param {object} callArgs - First argument passed to the wrapped method
103
+ * @returns {Array<object>|undefined}
248
104
  */
249
- function buildTextOutputMessages (inputMessages, text) {
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
- * Parses a Vercel AI content array and dispatches to the appropriate output message builder.
110
+ * Extracts OpenAI output messages from a `chat.completions.create` parsed body.
258
111
  *
259
- * @param {Array<object>} inputMessages - The input messages already in AI guard style format
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 buildOutputMessages (inputMessages, content) {
264
- const toolCalls = content.filter(c => c.type === 'tool-call')
265
- const text = content.filter(c => c.type === 'text').map(c => c.text).join('\n')
266
- if (toolCalls.length) return buildToolCallOutputMessages(inputMessages, toolCalls)
267
- if (text) return buildTextOutputMessages(inputMessages, text)
268
- return inputMessages
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
- * Converts one OpenAI reusable prompt variable value to message content.
174
+ * Extracts OpenAI input messages from a `responses.create` call.
319
175
  *
320
- * Routes every variable through `openAIResponseContentToMessageContent` so the
321
- * result follows the same string-when-text-only / array-when-multimodal shape
322
- * convention used elsewhere in this file. Media variables that produce no
323
- * usable content (e.g. an `input_image` with no URL or `file_id`) fall back to
324
- * a stable text marker so AI Guard still observes that a media variable was
325
- * attached.
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
- buildToolCallOutputMessages,
485
- buildTextOutputMessages,
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'),