dd-trace 5.62.0 → 5.63.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 (33) hide show
  1. package/README.md +0 -5
  2. package/package.json +2 -2
  3. package/packages/datadog-instrumentations/src/ai.js +140 -0
  4. package/packages/datadog-instrumentations/src/couchbase.js +102 -65
  5. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  6. package/packages/datadog-instrumentations/src/helpers/register.js +2 -22
  7. package/packages/datadog-instrumentations/src/hono.js +11 -8
  8. package/packages/datadog-instrumentations/src/knex.js +15 -17
  9. package/packages/datadog-instrumentations/src/mongodb-core.js +4 -6
  10. package/packages/datadog-instrumentations/src/next.js +4 -8
  11. package/packages/datadog-instrumentations/src/pg.js +38 -48
  12. package/packages/datadog-plugin-aerospike/src/index.js +6 -2
  13. package/packages/datadog-plugin-ai/src/index.js +17 -0
  14. package/packages/datadog-plugin-ai/src/tracing.js +33 -0
  15. package/packages/datadog-plugin-ai/src/utils.js +28 -0
  16. package/packages/datadog-plugin-couchbase/src/index.js +37 -17
  17. package/packages/datadog-plugin-pg/src/index.js +5 -2
  18. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +14 -7
  19. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  20. package/packages/dd-trace/src/appsec/recommended.json +271 -2
  21. package/packages/dd-trace/src/guardrails/telemetry.js +18 -2
  22. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +351 -0
  23. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +179 -0
  24. package/packages/dd-trace/src/llmobs/writers/base.js +3 -2
  25. package/packages/dd-trace/src/opentracing/span_context.js +4 -0
  26. package/packages/dd-trace/src/plugin_manager.js +8 -4
  27. package/packages/dd-trace/src/plugins/index.js +1 -0
  28. package/packages/dd-trace/src/plugins/util/ip_extractor.js +44 -3
  29. package/packages/dd-trace/src/profiling/profilers/event_plugins/event.js +24 -23
  30. package/packages/dd-trace/src/profiling/profilers/events.js +3 -2
  31. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -2
  32. package/packages/dd-trace/src/supported-configurations.json +2 -0
  33. package/packages/dd-trace/src/tracer_metadata.js +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "2.2",
3
3
  "metadata": {
4
- "rules_version": "1.15.0"
4
+ "rules_version": "1.15.1"
5
5
  },
6
6
  "rules": [
7
7
  {
@@ -5539,6 +5539,7 @@
5539
5539
  "confidence": "0",
5540
5540
  "module": "waf"
5541
5541
  },
5542
+ "max_version": "1.24.9",
5542
5543
  "conditions": [
5543
5544
  {
5544
5545
  "parameters": {
@@ -6671,7 +6672,10 @@
6671
6672
  {
6672
6673
  "address": "graphql.server.resolver"
6673
6674
  }
6674
- ]
6675
+ ],
6676
+ "options": {
6677
+ "path-inspection": true
6678
+ }
6675
6679
  },
6676
6680
  "operator": "ssrf_detector"
6677
6681
  }
@@ -8916,6 +8920,271 @@
8916
8920
  "transformers": []
8917
8921
  }
8918
8922
  ],
8923
+ "rules_compat": [
8924
+ {
8925
+ "id": "api-001-100",
8926
+ "name": "JWT: No expiry is present",
8927
+ "tags": {
8928
+ "type": "jwt",
8929
+ "category": "api_security",
8930
+ "confidence": "0",
8931
+ "module": "business-logic"
8932
+ },
8933
+ "min_version": "1.25.0",
8934
+ "conditions": [
8935
+ {
8936
+ "parameters": {
8937
+ "inputs": [
8938
+ {
8939
+ "address": "server.request.jwt",
8940
+ "key_path": [
8941
+ "payload",
8942
+ "exp"
8943
+ ]
8944
+ }
8945
+ ]
8946
+ },
8947
+ "operator": "!exists"
8948
+ }
8949
+ ],
8950
+ "transformers": [],
8951
+ "output": {
8952
+ "event": false,
8953
+ "keep": false,
8954
+ "attributes": {
8955
+ "_dd.appsec.api.jwt.no_expiry": {
8956
+ "value": 1
8957
+ }
8958
+ }
8959
+ }
8960
+ },
8961
+ {
8962
+ "id": "api-001-110",
8963
+ "name": "JWT: Collect algorithm used",
8964
+ "tags": {
8965
+ "type": "jwt",
8966
+ "category": "api_security",
8967
+ "confidence": "0",
8968
+ "module": "business-logic"
8969
+ },
8970
+ "min_version": "1.25.0",
8971
+ "conditions": [
8972
+ {
8973
+ "parameters": {
8974
+ "inputs": [
8975
+ {
8976
+ "address": "server.request.jwt",
8977
+ "key_path": [
8978
+ "header",
8979
+ "alg"
8980
+ ]
8981
+ }
8982
+ ]
8983
+ },
8984
+ "operator": "exists"
8985
+ }
8986
+ ],
8987
+ "transformers": [],
8988
+ "output": {
8989
+ "event": false,
8990
+ "keep": false,
8991
+ "attributes": {
8992
+ "_dd.appsec.api.jwt_alg": {
8993
+ "address": "server.request.jwt",
8994
+ "key_path": [
8995
+ "header",
8996
+ "alg"
8997
+ ]
8998
+ }
8999
+ }
9000
+ }
9001
+ },
9002
+ {
9003
+ "id": "api-001-120",
9004
+ "name": "JWT: No audience is specified",
9005
+ "tags": {
9006
+ "type": "jwt",
9007
+ "category": "api_security",
9008
+ "confidence": "0",
9009
+ "module": "business-logic"
9010
+ },
9011
+ "min_version": "1.25.0",
9012
+ "conditions": [
9013
+ {
9014
+ "parameters": {
9015
+ "inputs": [
9016
+ {
9017
+ "address": "server.request.jwt",
9018
+ "key_path": [
9019
+ "payload",
9020
+ "aud"
9021
+ ]
9022
+ }
9023
+ ]
9024
+ },
9025
+ "operator": "!exists"
9026
+ }
9027
+ ],
9028
+ "transformers": [],
9029
+ "output": {
9030
+ "event": false,
9031
+ "keep": false,
9032
+ "attributes": {
9033
+ "_dd.appsec.api.jwt.no_audience": {
9034
+ "value": 1
9035
+ }
9036
+ }
9037
+ }
9038
+ },
9039
+ {
9040
+ "id": "api-001-130",
9041
+ "name": "JWT: None algorithm used",
9042
+ "tags": {
9043
+ "type": "jwt",
9044
+ "category": "api_security",
9045
+ "confidence": "0",
9046
+ "module": "business-logic"
9047
+ },
9048
+ "min_version": "1.25.0",
9049
+ "conditions": [
9050
+ {
9051
+ "parameters": {
9052
+ "inputs": [
9053
+ {
9054
+ "address": "server.request.jwt",
9055
+ "key_path": [
9056
+ "header",
9057
+ "alg"
9058
+ ]
9059
+ }
9060
+ ],
9061
+ "list": [
9062
+ "none",
9063
+ "nonE",
9064
+ "noNe",
9065
+ "noNE",
9066
+ "nOne",
9067
+ "nOnE",
9068
+ "nONe",
9069
+ "nONE",
9070
+ "None",
9071
+ "NonE",
9072
+ "NoNe",
9073
+ "NoNE",
9074
+ "NOne",
9075
+ "NOnE",
9076
+ "NONe",
9077
+ "NONE"
9078
+ ]
9079
+ },
9080
+ "operator": "exact_match"
9081
+ }
9082
+ ],
9083
+ "transformers": [],
9084
+ "output": {
9085
+ "event": false,
9086
+ "keep": true,
9087
+ "attributes": {
9088
+ "_dd.appsec.api.jwt.none_alg": {
9089
+ "value": 1
9090
+ }
9091
+ }
9092
+ }
9093
+ },
9094
+ {
9095
+ "id": "ua0-600-551",
9096
+ "name": "Datadog test scanner - scalar trace-tagging version: user-agent",
9097
+ "tags": {
9098
+ "type": "security_scanner",
9099
+ "category": "attack_attempt",
9100
+ "cwe": "200",
9101
+ "capec": "1000/118/169",
9102
+ "tool_name": "Datadog Canary Test",
9103
+ "confidence": "1",
9104
+ "module": "waf"
9105
+ },
9106
+ "min_version": "1.25.0",
9107
+ "conditions": [
9108
+ {
9109
+ "parameters": {
9110
+ "inputs": [
9111
+ {
9112
+ "address": "server.request.headers.no_cookies",
9113
+ "key_path": [
9114
+ "user-agent"
9115
+ ]
9116
+ },
9117
+ {
9118
+ "address": "grpc.server.request.metadata",
9119
+ "key_path": [
9120
+ "dd-canary"
9121
+ ]
9122
+ }
9123
+ ],
9124
+ "regex": "^dd-test-scanner-tag-scalar(?:$|/|\\s)"
9125
+ },
9126
+ "operator": "match_regex"
9127
+ }
9128
+ ],
9129
+ "transformers": [],
9130
+ "output": {
9131
+ "event": false,
9132
+ "attributes": {
9133
+ "_dd.appsec.test.scanner.scalar": {
9134
+ "value": 1
9135
+ }
9136
+ }
9137
+ }
9138
+ },
9139
+ {
9140
+ "id": "ua0-600-552",
9141
+ "name": "Datadog test scanner - reference trace-tagging version: user-agent",
9142
+ "tags": {
9143
+ "type": "security_scanner",
9144
+ "category": "attack_attempt",
9145
+ "cwe": "200",
9146
+ "capec": "1000/118/169",
9147
+ "tool_name": "Datadog Canary Test",
9148
+ "confidence": "1",
9149
+ "module": "waf"
9150
+ },
9151
+ "min_version": "1.25.0",
9152
+ "conditions": [
9153
+ {
9154
+ "parameters": {
9155
+ "inputs": [
9156
+ {
9157
+ "address": "server.request.headers.no_cookies",
9158
+ "key_path": [
9159
+ "user-agent"
9160
+ ]
9161
+ },
9162
+ {
9163
+ "address": "grpc.server.request.metadata",
9164
+ "key_path": [
9165
+ "dd-canary"
9166
+ ]
9167
+ }
9168
+ ],
9169
+ "regex": "^dd-test-scanner-tag-ref(?:$|/|\\s)"
9170
+ },
9171
+ "operator": "match_regex"
9172
+ }
9173
+ ],
9174
+ "transformers": [],
9175
+ "output": {
9176
+ "event": false,
9177
+ "attributes": {
9178
+ "_dd.appsec.test.scanner.reference": {
9179
+ "address": "server.request.headers.no_cookies",
9180
+ "key_path": [
9181
+ "user-agent"
9182
+ ]
9183
+ }
9184
+ }
9185
+ }
9186
+ }
9187
+ ],
8919
9188
  "processors": [
8920
9189
  {
8921
9190
  "id": "http-endpoint-fingerprint",
@@ -22,7 +22,10 @@ var metadata = {
22
22
  runtime_name: 'nodejs',
23
23
  runtime_version: process.versions.node,
24
24
  tracer_version: tracerVersion,
25
- pid: process.pid
25
+ pid: process.pid,
26
+ result: 'unknown',
27
+ result_reason: 'unknown',
28
+ result_class: 'unknown'
26
29
  }
27
30
 
28
31
  var seen = {}
@@ -64,14 +67,27 @@ function sendTelemetry (name, tags) {
64
67
  })
65
68
  proc.on('error', function () {
66
69
  log.error('Failed to spawn telemetry forwarder')
70
+ metadata.result = 'error'
71
+ metadata.result_class = 'internal_error'
72
+ metadata.result_reason = 'Failed to spawn telemetry forwarder'
67
73
  })
68
74
  proc.on('exit', function (code) {
69
- if (code !== 0) {
75
+ if (code === 0) {
76
+ metadata.result = 'success'
77
+ metadata.result_class = 'success'
78
+ metadata.result_reason = 'Successfully configured ddtrace package'
79
+ } else {
70
80
  log.error('Telemetry forwarder exited with code', code)
81
+ metadata.result = 'error'
82
+ metadata.result_class = 'internal_error'
83
+ metadata.result_reason = 'Telemetry forwarder exited with code ' + code
71
84
  }
72
85
  })
73
86
  proc.stdin.on('error', function () {
74
87
  log.error('Failed to write telemetry data to telemetry forwarder')
88
+ metadata.result = 'error'
89
+ metadata.result_class = 'internal_error'
90
+ metadata.result_reason = 'Failed to write telemetry data to telemetry forwarder'
75
91
  })
76
92
  proc.stdin.end(JSON.stringify({ metadata: metadata, points: points }))
77
93
  }
@@ -0,0 +1,351 @@
1
+ 'use strict'
2
+
3
+ const BaseLLMObsPlugin = require('../base')
4
+ const { getModelProvider } = require('../../../../../datadog-plugin-ai/src/utils')
5
+
6
+ const { channel } = require('dc-polyfill')
7
+
8
+ const toolCreationCh = channel('dd-trace:vercel-ai:tool')
9
+ const setAttributesCh = channel('dd-trace:vercel-ai:span:setAttributes')
10
+
11
+ const { MODEL_NAME, MODEL_PROVIDER, NAME } = require('../../constants/tags')
12
+ const {
13
+ getSpanTags,
14
+ getOperation,
15
+ getUsage,
16
+ getJsonStringValue,
17
+ getModelMetadata,
18
+ getGenerationMetadata,
19
+ getToolNameFromTags,
20
+ getToolCallResultContent
21
+ } = require('./util')
22
+
23
+ const SPAN_NAME_TO_KIND_MAPPING = {
24
+ // embeddings
25
+ embed: 'workflow',
26
+ embedMany: 'workflow',
27
+ doEmbed: 'embedding',
28
+ // object generation
29
+ generateObject: 'workflow',
30
+ streamObject: 'workflow',
31
+ // text generation
32
+ generateText: 'workflow',
33
+ streamText: 'workflow',
34
+ // llm operations
35
+ doGenerate: 'llm',
36
+ doStream: 'llm',
37
+ // tools
38
+ toolCall: 'tool'
39
+ }
40
+
41
+ class VercelAILLMObsPlugin extends BaseLLMObsPlugin {
42
+ static id = 'ai'
43
+ static integration = 'ai'
44
+ static prefix = 'tracing:dd-trace:vercel-ai'
45
+
46
+ /**
47
+ * The available tools within the runtime scope of this integration.
48
+ * This essentially acts as a global registry for all tools made through the Vercel AI SDK.
49
+ * @type {Set<Record<string, any>>}
50
+ */
51
+ #availableTools
52
+
53
+ /**
54
+ * A mapping of tool call IDs to tool names.
55
+ * This is used to map the tool call ID to the tool name for the output message.
56
+ * @type {Record<string, string>}
57
+ */
58
+ #toolCallIdsToName
59
+
60
+ constructor (...args) {
61
+ super(...args)
62
+
63
+ this.#toolCallIdsToName = {}
64
+ this.#availableTools = new Set()
65
+ toolCreationCh.subscribe(toolArgs => {
66
+ this.#availableTools.add(toolArgs)
67
+ })
68
+
69
+ setAttributesCh.subscribe(({ ctx, attributes }) => {
70
+ Object.assign(ctx.attributes, attributes)
71
+ })
72
+ }
73
+
74
+ /**
75
+ * Does a best-effort attempt to find the right tool name for the given tool description.
76
+ * This is because the Vercel AI SDK does not tag tools by name properly, but
77
+ * rather by the index they were passed in. Tool names appear nowhere in the span tags.
78
+ *
79
+ * We use the tool description as the next best identifier for a tool.
80
+ *
81
+ * @param {string} toolDescription
82
+ * @returns {string}
83
+ */
84
+ findToolName (toolDescription) {
85
+ for (const availableTool of this.#availableTools) {
86
+ const description = availableTool.description
87
+ if (description === toolDescription) {
88
+ return availableTool.id
89
+ }
90
+ }
91
+ }
92
+
93
+ getLLMObsSpanRegisterOptions (ctx) {
94
+ const span = ctx.currentStore?.span
95
+ const operation = getOperation(span)
96
+ const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
97
+ if (!kind) return
98
+
99
+ return { kind, name: operation }
100
+ }
101
+
102
+ setLLMObsTags (ctx) {
103
+ const span = ctx.currentStore?.span
104
+ if (!span) return
105
+
106
+ const operation = getOperation(span)
107
+ const kind = SPAN_NAME_TO_KIND_MAPPING[operation]
108
+ if (!kind) return
109
+
110
+ const tags = getSpanTags(ctx)
111
+
112
+ if (['embedding', 'llm'].includes(kind)) {
113
+ this._tagger._setTag(span, MODEL_NAME, tags['ai.model.id'])
114
+ this._tagger._setTag(span, MODEL_PROVIDER, getModelProvider(tags))
115
+ }
116
+
117
+ switch (operation) {
118
+ case 'embed':
119
+ case 'embedMany':
120
+ this.setEmbeddingWorkflowTags(span, tags)
121
+ break
122
+ case 'doEmbed':
123
+ this.setEmbeddingTags(span, tags)
124
+ break
125
+ case 'generateObject':
126
+ case 'streamObject':
127
+ this.setObjectGenerationTags(span, tags)
128
+ break
129
+ case 'generateText':
130
+ case 'streamText':
131
+ this.setTextGenerationTags(span, tags)
132
+ break
133
+ case 'doGenerate':
134
+ case 'doStream':
135
+ this.setLLMOperationTags(span, tags)
136
+ break
137
+ case 'toolCall':
138
+ this.setToolTags(span, tags)
139
+ break
140
+ default:
141
+ break
142
+ }
143
+ }
144
+
145
+ setEmbeddingWorkflowTags (span, tags) {
146
+ const inputs = tags['ai.value'] ?? tags['ai.values']
147
+ const parsedInputs = Array.isArray(inputs)
148
+ ? inputs.map(input => getJsonStringValue(input, ''))
149
+ : getJsonStringValue(inputs, '')
150
+
151
+ const embeddingsOutput = tags['ai.embedding'] ?? tags['ai.embeddings']
152
+ const isSingleEmbedding = !Array.isArray(embeddingsOutput)
153
+ const numberOfEmbeddings = isSingleEmbedding ? 1 : embeddingsOutput.length
154
+ const embeddingsLength = getJsonStringValue(isSingleEmbedding ? embeddingsOutput : embeddingsOutput?.[0], []).length
155
+ const output = `[${numberOfEmbeddings} embedding(s) returned with size ${embeddingsLength}]`
156
+
157
+ this._tagger.tagTextIO(span, parsedInputs, output)
158
+
159
+ const metadata = getGenerationMetadata(tags)
160
+ this._tagger.tagMetadata(span, metadata)
161
+ }
162
+
163
+ setEmbeddingTags (span, tags) {
164
+ const inputs = tags['ai.values']
165
+ if (!Array.isArray(inputs)) return
166
+
167
+ const parsedInputs = inputs.map(input => getJsonStringValue(input, ''))
168
+
169
+ const embeddingsOutput = tags['ai.embeddings']
170
+ const numberOfEmbeddings = embeddingsOutput?.length
171
+ const embeddingsLength = getJsonStringValue(embeddingsOutput?.[0], []).length
172
+ const output = `[${numberOfEmbeddings} embedding(s) returned with size ${embeddingsLength}]`
173
+
174
+ this._tagger.tagEmbeddingIO(span, parsedInputs, output)
175
+
176
+ const usage = tags['ai.usage.tokens']
177
+ this._tagger.tagMetrics(span, {
178
+ inputTokens: usage,
179
+ totalTokens: usage
180
+ })
181
+ }
182
+
183
+ setObjectGenerationTags (span, tags) {
184
+ const promptInfo = getJsonStringValue(tags['ai.prompt'], {})
185
+ const lastUserPrompt =
186
+ promptInfo.prompt ??
187
+ promptInfo.messages.reverse().find(message => message.role === 'user')?.content
188
+ const prompt = Array.isArray(lastUserPrompt) ? lastUserPrompt.map(part => part.text ?? '').join('') : lastUserPrompt
189
+
190
+ const output = tags['ai.response.object']
191
+
192
+ this._tagger.tagTextIO(span, prompt, output)
193
+
194
+ const metadata = getGenerationMetadata(tags) ?? {}
195
+ metadata.schema = getJsonStringValue(tags['ai.schema'], {})
196
+ this._tagger.tagMetadata(span, metadata)
197
+ }
198
+
199
+ setTextGenerationTags (span, tags) {
200
+ const promptInfo = getJsonStringValue(tags['ai.prompt'], {})
201
+ const lastUserPrompt =
202
+ promptInfo.prompt ??
203
+ promptInfo.messages.reverse().find(message => message.role === 'user')?.content
204
+ const prompt = Array.isArray(lastUserPrompt) ? lastUserPrompt.map(part => part.text ?? '').join('') : lastUserPrompt
205
+
206
+ const output = tags['ai.response.text']
207
+
208
+ this._tagger.tagTextIO(span, prompt, output)
209
+
210
+ const metadata = getGenerationMetadata(tags)
211
+ this._tagger.tagMetadata(span, metadata)
212
+ }
213
+
214
+ setLLMOperationTags (span, tags) {
215
+ const toolsForModel = tags['ai.prompt.tools']?.map(getJsonStringValue)
216
+
217
+ const inputMessages = getJsonStringValue(tags['ai.prompt.messages'], [])
218
+ const parsedInputMessages = []
219
+ for (const message of inputMessages) {
220
+ const formattedMessages = this.formatMessage(message, toolsForModel)
221
+ parsedInputMessages.push(...formattedMessages)
222
+ }
223
+
224
+ const outputMessage = this.formatOutputMessage(tags, toolsForModel)
225
+
226
+ this._tagger.tagLLMIO(span, parsedInputMessages, outputMessage)
227
+
228
+ const metadata = getModelMetadata(tags)
229
+ this._tagger.tagMetadata(span, metadata)
230
+
231
+ const usage = getUsage(tags)
232
+ this._tagger.tagMetrics(span, usage)
233
+ }
234
+
235
+ setToolTags (span, tags) {
236
+ const toolCallId = tags['ai.toolCall.id']
237
+ const name = getToolNameFromTags(tags) ?? this.#toolCallIdsToName[toolCallId]
238
+ if (name) this._tagger._setTag(span, NAME, name)
239
+
240
+ const input = tags['ai.toolCall.args']
241
+ const output = tags['ai.toolCall.result']
242
+
243
+ this._tagger.tagTextIO(span, input, output)
244
+ }
245
+
246
+ formatOutputMessage (tags, toolsForModel) {
247
+ const outputMessageText = tags['ai.response.text'] ?? tags['ai.response.object']
248
+ const outputMessageToolCalls = getJsonStringValue(tags['ai.response.toolCalls'], [])
249
+
250
+ const formattedToolCalls = []
251
+ for (const toolCall of outputMessageToolCalls) {
252
+ const toolCallArgs = getJsonStringValue(toolCall.args, {})
253
+ const toolDescription = toolsForModel?.find(tool => toolCall.toolName === tool.name)?.description
254
+ const name = this.findToolName(toolDescription)
255
+ this.#toolCallIdsToName[toolCall.toolCallId] = name
256
+
257
+ formattedToolCalls.push({
258
+ arguments: toolCallArgs,
259
+ name,
260
+ toolId: toolCall.toolCallId,
261
+ type: 'function'
262
+ })
263
+ }
264
+
265
+ return {
266
+ role: 'assistant',
267
+ content: outputMessageText,
268
+ toolCalls: formattedToolCalls
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Returns a list of formatted messages from a message object.
274
+ * Most of these will just be one entry, but in the case of a "tool" role,
275
+ * it is possible to have multiple tool call results in a single message that we
276
+ * need to split into multiple messages.
277
+ *
278
+ * @param {*} message
279
+ * @param {*} toolsForModel
280
+ * @returns {Array<{role: string, content: string, toolId?: string,
281
+ * toolCalls?: Array<{arguments: string, name: string, toolId: string, type: string}>}>}
282
+ */
283
+ formatMessage (message, toolsForModel) {
284
+ const { role, content } = message
285
+
286
+ if (role === 'system') {
287
+ return [{ role, content }]
288
+ } else if (role === 'user') {
289
+ let finalContent = ''
290
+ for (const part of content) {
291
+ const { type } = part
292
+ if (type === 'text') {
293
+ finalContent += part.text
294
+ }
295
+ }
296
+
297
+ return [{ role, content: finalContent }]
298
+ } else if (role === 'assistant') {
299
+ const toolCalls = []
300
+ let finalContent = ''
301
+
302
+ for (const part of content) {
303
+ const { type } = part
304
+ // TODO(sabrenner): do we want to include reasoning?
305
+ if (['text', 'reasoning', 'redacted-reasoning'].includes(type)) {
306
+ finalContent += part.text ?? part.data
307
+ } else if (type === 'tool-call') {
308
+ const toolDescription = toolsForModel?.find(tool => part.toolName === tool.name)?.description
309
+ const name = this.findToolName(toolDescription)
310
+
311
+ toolCalls.push({
312
+ arguments: part.args,
313
+ name,
314
+ toolId: part.toolCallId,
315
+ type: 'function'
316
+ })
317
+ }
318
+ }
319
+
320
+ const finalMessage = {
321
+ role,
322
+ content: finalContent
323
+ }
324
+
325
+ if (toolCalls.length) {
326
+ finalMessage.toolCalls = toolCalls.length ? toolCalls : undefined
327
+ }
328
+
329
+ return [finalMessage]
330
+ } else if (role === 'tool') {
331
+ const finalMessages = []
332
+ for (const part of content) {
333
+ if (part.type === 'tool-result') {
334
+ const safeResult = getToolCallResultContent(part)
335
+
336
+ finalMessages.push({
337
+ role,
338
+ content: safeResult,
339
+ toolId: part.toolCallId
340
+ })
341
+ }
342
+ }
343
+
344
+ return finalMessages
345
+ }
346
+
347
+ return []
348
+ }
349
+ }
350
+
351
+ module.exports = VercelAILLMObsPlugin