dd-trace 5.99.1 → 5.101.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/LICENSE-3rdparty.csv +0 -1
- package/index.d.ts +14 -0
- package/package.json +8 -8
- package/packages/datadog-instrumentations/src/cucumber.js +69 -5
- package/packages/datadog-instrumentations/src/cypress.js +5 -3
- package/packages/datadog-instrumentations/src/express.js +3 -2
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/hono.js +15 -4
- package/packages/datadog-instrumentations/src/http/client.js +20 -3
- package/packages/datadog-instrumentations/src/jest.js +146 -90
- package/packages/datadog-instrumentations/src/mocha/common.js +4 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +43 -26
- package/packages/datadog-instrumentations/src/mocha/utils.js +114 -96
- package/packages/datadog-instrumentations/src/mocha/worker.js +7 -4
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +11 -6
- package/packages/datadog-instrumentations/src/path-to-regexp.js +44 -0
- package/packages/datadog-instrumentations/src/playwright.js +108 -18
- package/packages/datadog-instrumentations/src/router.js +53 -33
- package/packages/datadog-instrumentations/src/vitest.js +76 -30
- package/packages/datadog-plugin-aws-sdk/src/base.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/dynamodb.js +1 -1
- package/packages/datadog-plugin-bullmq/src/consumer.js +5 -4
- package/packages/datadog-plugin-bullmq/src/producer.js +37 -29
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +49 -9
- package/packages/datadog-plugin-cypress/src/plugin.js +5 -14
- package/packages/datadog-plugin-cypress/src/support.js +22 -21
- package/packages/datadog-plugin-grpc/src/client.js +1 -1
- package/packages/datadog-plugin-grpc/src/server.js +1 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -9
- package/packages/datadog-plugin-kafkajs/src/producer.js +2 -8
- package/packages/datadog-plugin-mongodb-core/src/index.js +2 -3
- package/packages/datadog-plugin-playwright/src/index.js +6 -0
- package/packages/datadog-plugin-router/src/index.js +13 -0
- package/packages/dd-trace/index.js +4 -3
- package/packages/dd-trace/src/aiguard/sdk.js +2 -2
- package/packages/dd-trace/src/appsec/reporter.js +4 -1
- package/packages/dd-trace/src/baggage.js +10 -0
- package/packages/dd-trace/src/ci-visibility/lage.js +2 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +11 -33
- package/packages/dd-trace/src/config/config-types.d.ts +0 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +17 -41
- package/packages/dd-trace/src/config/index.js +7 -60
- package/packages/dd-trace/src/config/normalize-service.js +31 -0
- package/packages/dd-trace/src/config/supported-configurations.json +15 -32
- package/packages/dd-trace/src/datastreams/checkpointer.js +4 -10
- package/packages/dd-trace/src/datastreams/encoding.js +39 -28
- package/packages/dd-trace/src/datastreams/pathway.js +29 -26
- package/packages/dd-trace/src/datastreams/processor.js +17 -15
- package/packages/dd-trace/src/datastreams/size.js +6 -2
- package/packages/dd-trace/src/debugger/config.js +6 -3
- package/packages/dd-trace/src/debugger/devtools_client/index.js +2 -5
- package/packages/dd-trace/src/debugger/devtools_client/send.js +2 -1
- package/packages/dd-trace/src/dogstatsd.js +10 -7
- package/packages/dd-trace/src/encode/0.4.js +3 -3
- package/packages/dd-trace/src/encode/0.5.js +2 -2
- package/packages/dd-trace/src/encode/agentless-json.js +2 -2
- package/packages/dd-trace/src/encode/tags-processors.js +2 -27
- package/packages/dd-trace/src/exporters/common/request.js +22 -11
- package/packages/dd-trace/src/exporters/common/retry.js +104 -0
- package/packages/dd-trace/src/git_metadata.js +66 -0
- package/packages/dd-trace/src/git_metadata_tagger.js +13 -5
- package/packages/dd-trace/src/heap_snapshots.js +4 -4
- package/packages/dd-trace/src/id.js +15 -26
- package/packages/dd-trace/src/llmobs/constants/tags.js +2 -0
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +27 -16
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +3 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +30 -13
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +20 -50
- package/packages/dd-trace/src/llmobs/sdk.js +5 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +28 -2
- package/packages/dd-trace/src/llmobs/tagger.js +42 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +29 -0
- package/packages/dd-trace/src/llmobs/util.js +80 -5
- package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +2 -2
- package/packages/dd-trace/src/opentelemetry/active-span-proxy.js +42 -0
- package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +106 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +22 -10
- package/packages/dd-trace/src/opentelemetry/span-helpers.js +308 -0
- package/packages/dd-trace/src/opentelemetry/span.js +42 -108
- package/packages/dd-trace/src/opentelemetry/tracer.js +11 -36
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +95 -36
- package/packages/dd-trace/src/opentracing/propagation/tracestate.js +98 -32
- package/packages/dd-trace/src/opentracing/span.js +58 -49
- package/packages/dd-trace/src/opentracing/span_context.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +119 -32
- package/packages/dd-trace/src/plugins/util/test.js +293 -27
- package/packages/dd-trace/src/priority_sampler.js +6 -4
- package/packages/dd-trace/src/profiling/config.js +5 -4
- package/packages/dd-trace/src/profiling/ssi-heuristics.js +2 -2
- package/packages/dd-trace/src/propagation-hash/index.js +1 -1
- package/packages/dd-trace/src/proxy.js +3 -3
- package/packages/dd-trace/src/remote_config/index.js +5 -3
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/span_format.js +52 -5
- package/packages/dd-trace/src/span_processor.js +1 -5
- package/packages/dd-trace/src/spanleak.js +0 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +7 -5
- package/packages/dd-trace/src/tracer_metadata.js +1 -1
- package/packages/dd-trace/src/util.js +17 -0
- package/vendor/dist/path-to-regexp/LICENSE +0 -21
- package/vendor/dist/path-to-regexp/index.js +0 -1
|
@@ -8,11 +8,11 @@ const { threadId } = require('worker_threads')
|
|
|
8
8
|
const log = require('./log')
|
|
9
9
|
|
|
10
10
|
async function scheduleSnapshot (config, total) {
|
|
11
|
-
if (total > config.
|
|
11
|
+
if (total > config.DD_HEAP_SNAPSHOT_COUNT) return
|
|
12
12
|
|
|
13
|
-
await setTimeout(config.
|
|
13
|
+
await setTimeout(config.DD_HEAP_SNAPSHOT_INTERVAL * 1000, null, { ref: false })
|
|
14
14
|
await clearMemory()
|
|
15
|
-
writeHeapSnapshot(getName(config.
|
|
15
|
+
writeHeapSnapshot(getName(config.DD_HEAP_SNAPSHOT_DESTINATION))
|
|
16
16
|
await scheduleSnapshot(config, total + 1)
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -49,7 +49,7 @@ module.exports = {
|
|
|
49
49
|
* @param {import('./config/config-base')} config - Tracer configuration
|
|
50
50
|
*/
|
|
51
51
|
async start (config) {
|
|
52
|
-
const destination = config.
|
|
52
|
+
const destination = config.DD_HEAP_SNAPSHOT_DESTINATION
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
55
|
await scheduleSnapshot(config, 1)
|
|
@@ -7,19 +7,19 @@ const UINT_MAX = 4_294_967_296
|
|
|
7
7
|
const data = new Uint8Array(8 * 8192)
|
|
8
8
|
const zeroId = new Uint8Array(8)
|
|
9
9
|
|
|
10
|
-
const map = Array.prototype.map
|
|
11
|
-
const pad = byte => `${byte < 16 ? '0' : ''}${byte.toString(16)}`
|
|
12
|
-
|
|
13
10
|
let batch = 0
|
|
14
11
|
|
|
15
12
|
// Internal representation of a trace or span ID.
|
|
16
13
|
class Identifier {
|
|
14
|
+
/** @type {number[] | Uint8Array} */
|
|
15
|
+
#buffer
|
|
16
|
+
|
|
17
17
|
/**
|
|
18
18
|
* @param {string} value
|
|
19
19
|
* @param {number} [radix]
|
|
20
20
|
*/
|
|
21
21
|
constructor (value, radix = 16) {
|
|
22
|
-
this
|
|
22
|
+
this.#buffer = radix === 16
|
|
23
23
|
? createBuffer(value)
|
|
24
24
|
: fromString(value, radix)
|
|
25
25
|
}
|
|
@@ -30,32 +30,32 @@ class Identifier {
|
|
|
30
30
|
*/
|
|
31
31
|
toString (radix = 16) {
|
|
32
32
|
return radix === 16
|
|
33
|
-
?
|
|
34
|
-
: toNumberString(this
|
|
33
|
+
? Buffer.from(this.#buffer).toString('hex')
|
|
34
|
+
: toNumberString(this.#buffer, radix)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* @returns {bigint}
|
|
39
39
|
*/
|
|
40
40
|
toBigInt () {
|
|
41
|
-
return Buffer.from(this
|
|
41
|
+
return Buffer.from(this.#buffer).readBigUInt64BE(0)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @returns {number[] | Uint8Array}
|
|
46
46
|
*/
|
|
47
47
|
toBuffer () {
|
|
48
|
-
return this
|
|
48
|
+
return this.#buffer
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* @returns {number[] | Uint8Array}
|
|
53
53
|
*/
|
|
54
54
|
toArray () {
|
|
55
|
-
if (this.
|
|
56
|
-
return this
|
|
55
|
+
if (this.#buffer.length === 8) {
|
|
56
|
+
return this.#buffer
|
|
57
57
|
}
|
|
58
|
-
return this.
|
|
58
|
+
return this.#buffer.slice(-8)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/**
|
|
@@ -70,12 +70,10 @@ class Identifier {
|
|
|
70
70
|
* @returns {boolean}
|
|
71
71
|
*/
|
|
72
72
|
equals (other) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
for (let i = length, j = otherLength; i >= 0 && j >= 0; i--, j--) {
|
|
78
|
-
if (this._buffer[i] !== other._buffer[j]) return false
|
|
73
|
+
// Big-endian suffix compare: when buffers differ in length, only the
|
|
74
|
+
// rightmost `min(this.length, other.length)` bytes are checked.
|
|
75
|
+
for (let i = this.#buffer.length - 1, j = other.#buffer.length - 1; i >= 0 && j >= 0; i--, j--) {
|
|
76
|
+
if (this.#buffer[i] !== other.#buffer[j]) return false
|
|
79
77
|
}
|
|
80
78
|
|
|
81
79
|
return true
|
|
@@ -174,15 +172,6 @@ function toNumberString (buffer, radix) {
|
|
|
174
172
|
return str
|
|
175
173
|
}
|
|
176
174
|
|
|
177
|
-
// Convert a buffer to a hexadecimal string.
|
|
178
|
-
/**
|
|
179
|
-
* @param {number[] | Uint8Array} buffer
|
|
180
|
-
* @returns {string}
|
|
181
|
-
*/
|
|
182
|
-
function toHexString (buffer) {
|
|
183
|
-
return map.call(buffer, pad).join('')
|
|
184
|
-
}
|
|
185
|
-
|
|
186
175
|
// Simple pseudo-random 64-bit ID generator.
|
|
187
176
|
/**
|
|
188
177
|
* @returns {number[] | Uint8Array}
|
|
@@ -7,7 +7,9 @@ module.exports = {
|
|
|
7
7
|
DECORATOR: '_ml_obs.decorator',
|
|
8
8
|
INTEGRATION: '_ml_obs.integration',
|
|
9
9
|
METADATA: '_ml_obs.meta.metadata',
|
|
10
|
+
COST_TAGS: '_ml_obs.meta.metadata._dd.cost_tags',
|
|
10
11
|
METRICS: '_ml_obs.metrics',
|
|
12
|
+
TOOL_DEFINITIONS: '_ml_obs.meta.tool_definitions',
|
|
11
13
|
ML_APP: '_ml_obs.meta.ml_app',
|
|
12
14
|
PROPAGATED_PARENT_ID_KEY: '_dd.p.llmobs_parent_id',
|
|
13
15
|
PROPAGATED_ML_APP_KEY: '_dd.p.llmobs_ml_app',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { UNKNOWN_MODEL_PROVIDER } = require('../../constants/tags')
|
|
4
|
+
const { safeJsonParse } = require('../../util')
|
|
4
5
|
const LLMObsPlugin = require('../base')
|
|
5
6
|
const { appendMessage } = require('./util')
|
|
6
7
|
|
|
@@ -47,6 +48,8 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
47
48
|
const { type } = contentBlock
|
|
48
49
|
if (type === 'text') {
|
|
49
50
|
response.content.push({ type, text: contentBlock.text })
|
|
51
|
+
} else if (type === 'thinking') {
|
|
52
|
+
response.content.push({ type, thinking: contentBlock.thinking ?? '' })
|
|
50
53
|
} else if (type === 'tool_use') {
|
|
51
54
|
response.content.push({ type, name: contentBlock.name, input: '', id: contentBlock.id })
|
|
52
55
|
}
|
|
@@ -56,20 +59,29 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
56
59
|
const { delta } = chunk
|
|
57
60
|
if (!delta) continue
|
|
58
61
|
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
const lastBlock = response.content[response.content.length - 1]
|
|
63
|
+
if (!lastBlock) continue
|
|
64
|
+
|
|
65
|
+
if (delta.type === 'thinking_delta') {
|
|
66
|
+
const { thinking } = delta
|
|
67
|
+
if (thinking) lastBlock.thinking += thinking
|
|
68
|
+
} else if (delta.type === 'signature_delta') {
|
|
69
|
+
// Signature is for internal verification only; skip it.
|
|
70
|
+
} else if (delta.type === 'input_json_delta') {
|
|
71
|
+
const partialJson = delta.partial_json
|
|
72
|
+
if (partialJson) lastBlock.input += partialJson
|
|
73
|
+
} else {
|
|
74
|
+
const { text } = delta
|
|
75
|
+
if (text) lastBlock.text += text
|
|
65
76
|
}
|
|
66
77
|
break
|
|
67
78
|
}
|
|
68
79
|
case 'content_block_stop': {
|
|
69
|
-
const
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
const lastBlock = response.content[response.content.length - 1]
|
|
81
|
+
if (!lastBlock) break
|
|
82
|
+
if (lastBlock.type === 'tool_use') {
|
|
83
|
+
const input = lastBlock.input ?? '{}'
|
|
84
|
+
lastBlock.input = safeJsonParse(input, {})
|
|
73
85
|
}
|
|
74
86
|
break
|
|
75
87
|
}
|
|
@@ -167,18 +179,17 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
167
179
|
|
|
168
180
|
const outputMessages = []
|
|
169
181
|
for (const block of content) {
|
|
182
|
+
if (block.type === 'thinking') {
|
|
183
|
+
outputMessages.push({ content: block.thinking ?? '', role: 'reasoning' })
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
170
186
|
const { text } = block
|
|
171
187
|
if (typeof text === 'string') {
|
|
172
188
|
outputMessages.push({ content: text, role })
|
|
173
189
|
} else if (block.type === 'tool_use') {
|
|
174
|
-
let input = block.input
|
|
175
|
-
if (typeof input === 'string') {
|
|
176
|
-
input = JSON.parse(input)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
190
|
const toolCall = {
|
|
180
191
|
name: block.name,
|
|
181
|
-
arguments: input,
|
|
192
|
+
arguments: safeJsonParse(block.input, {}),
|
|
182
193
|
toolId: block.id,
|
|
183
194
|
type: block.type,
|
|
184
195
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {{type: 'text', text: string}} TextBlock
|
|
5
5
|
* @typedef {{type: 'image'}} ImageBlock
|
|
6
|
+
* @typedef {{type: 'thinking', thinking: string, signature?: string}} ThinkingBlock
|
|
6
7
|
* @typedef {{
|
|
7
8
|
* type: 'tool_use', text: string, name: string, id: string, input: string | Record<string, unknown>
|
|
8
9
|
* }} ToolUseBlock
|
|
@@ -70,6 +71,8 @@ function appendMessage (messages, { role, content }) {
|
|
|
70
71
|
messages.push({ content: block.text, role })
|
|
71
72
|
} else if (block.type === 'image') {
|
|
72
73
|
messages.push({ content: '([IMAGE DETECTED])', role })
|
|
74
|
+
} else if (block.type === 'thinking') {
|
|
75
|
+
messages.push({ content: block.thinking ?? '', role: 'reasoning' })
|
|
73
76
|
} else if (block.type === 'tool_use') {
|
|
74
77
|
const { text, name, id, type } = block
|
|
75
78
|
let input = block.input
|
|
@@ -237,15 +237,23 @@ function formatContentObject (content) {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
// Two filter passes over `parts` collapse to a single walk. Most parts are
|
|
241
|
+
// text-only so neither bucket is allocated unless a matching part appears.
|
|
242
|
+
let functionCalls
|
|
243
|
+
let functionResponses
|
|
244
|
+
for (const part of parts) {
|
|
245
|
+
if (part.functionCall) {
|
|
246
|
+
(functionCalls ??= []).push(part)
|
|
247
|
+
} else if (part.functionResponse) {
|
|
248
|
+
(functionResponses ??= []).push(part)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (functionCalls) {
|
|
243
253
|
return formatFunctionCallMessage(parts, functionCalls, role)
|
|
244
254
|
}
|
|
245
255
|
|
|
246
|
-
|
|
247
|
-
const functionResponses = parts.filter(part => part.functionResponse)
|
|
248
|
-
if (functionResponses.length > 0) {
|
|
256
|
+
if (functionResponses) {
|
|
249
257
|
return formatFunctionResponseMessage(functionResponses, role)
|
|
250
258
|
}
|
|
251
259
|
|
|
@@ -326,15 +334,26 @@ function formatNonStreamingCandidate (candidate) {
|
|
|
326
334
|
|
|
327
335
|
const { parts } = content
|
|
328
336
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
337
|
+
// One walk replaces three (`filter` + two `find`); priority order is
|
|
338
|
+
// functionCall > executableCode > codeExecutionResult, same as before.
|
|
339
|
+
let functionCalls
|
|
340
|
+
let executableCode
|
|
341
|
+
let codeExecutionResult
|
|
342
|
+
for (const part of parts) {
|
|
343
|
+
if (part.functionCall) {
|
|
344
|
+
(functionCalls ??= []).push(part)
|
|
345
|
+
} else if (!executableCode && part.executableCode) {
|
|
346
|
+
executableCode = part
|
|
347
|
+
} else if (!codeExecutionResult && part.codeExecutionResult) {
|
|
348
|
+
codeExecutionResult = part
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (functionCalls) {
|
|
332
353
|
messages.push(formatFunctionCallMessage(parts, functionCalls, ROLES.ASSISTANT))
|
|
333
354
|
return messages
|
|
334
355
|
}
|
|
335
356
|
|
|
336
|
-
// Check for executable code
|
|
337
|
-
const executableCode = parts.find(part => part.executableCode)
|
|
338
357
|
if (executableCode) {
|
|
339
358
|
messages.push({
|
|
340
359
|
role: ROLES.ASSISTANT,
|
|
@@ -346,8 +365,6 @@ function formatNonStreamingCandidate (candidate) {
|
|
|
346
365
|
return messages
|
|
347
366
|
}
|
|
348
367
|
|
|
349
|
-
// Check for code execution result
|
|
350
|
-
const codeExecutionResult = parts.find(part => part.codeExecutionResult)
|
|
351
368
|
if (codeExecutionResult) {
|
|
352
369
|
messages.push({
|
|
353
370
|
role: ROLES.ASSISTANT,
|
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
INSTRUMENTATION_METHOD_AUTO,
|
|
8
8
|
UNKNOWN_MODEL_PROVIDER,
|
|
9
9
|
} = require('../../constants/tags')
|
|
10
|
+
const { safeJsonParse } = require('../../util')
|
|
10
11
|
const {
|
|
11
12
|
extractChatTemplateFromInstructions,
|
|
12
13
|
normalizePromptVariables,
|
|
@@ -183,13 +184,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
183
184
|
_tagChatCompletion (span, inputs, response, error) {
|
|
184
185
|
const { messages, model, ...parameters } = inputs
|
|
185
186
|
|
|
186
|
-
const metadata =
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
const metadata = {}
|
|
188
|
+
for (const key of Object.keys(parameters)) {
|
|
189
|
+
if (key !== 'tools' && key !== 'functions') {
|
|
190
|
+
metadata[key] = parameters[key]
|
|
189
191
|
}
|
|
190
|
-
|
|
191
|
-
return obj
|
|
192
|
-
}, {})
|
|
192
|
+
}
|
|
193
193
|
|
|
194
194
|
this._tagger.tagMetadata(span, metadata)
|
|
195
195
|
|
|
@@ -213,14 +213,14 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
213
213
|
if (message.function_call) {
|
|
214
214
|
const functionCallInfo = {
|
|
215
215
|
name: message.function_call.name,
|
|
216
|
-
arguments:
|
|
216
|
+
arguments: safeJsonParse(message.function_call.arguments),
|
|
217
217
|
}
|
|
218
218
|
outputMessages.push({ content, role, toolCalls: [functionCallInfo] })
|
|
219
219
|
} else if (message.tool_calls) {
|
|
220
220
|
const toolCallsInfo = []
|
|
221
221
|
for (const toolCall of message.tool_calls) {
|
|
222
222
|
const toolCallInfo = {
|
|
223
|
-
arguments:
|
|
223
|
+
arguments: safeJsonParse(toolCall.function.arguments),
|
|
224
224
|
name: toolCall.function.name,
|
|
225
225
|
toolId: toolCall.id,
|
|
226
226
|
type: toolCall.type,
|
|
@@ -277,22 +277,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
277
277
|
inputMessages.push({ role, content })
|
|
278
278
|
}
|
|
279
279
|
} else if (item.type === 'function_call') {
|
|
280
|
-
// Function call: convert to message with tool_calls
|
|
281
|
-
// Parse arguments if it's a JSON string
|
|
282
|
-
let parsedArgs = item.arguments
|
|
283
|
-
if (typeof parsedArgs === 'string') {
|
|
284
|
-
try {
|
|
285
|
-
parsedArgs = JSON.parse(parsedArgs)
|
|
286
|
-
} catch {
|
|
287
|
-
parsedArgs = {}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
280
|
inputMessages.push({
|
|
291
281
|
role: 'assistant',
|
|
292
282
|
toolCalls: [{
|
|
293
283
|
toolId: item.call_id,
|
|
294
284
|
name: item.name,
|
|
295
|
-
arguments:
|
|
285
|
+
arguments: safeJsonParse(item.arguments, {}),
|
|
296
286
|
type: item.type,
|
|
297
287
|
}],
|
|
298
288
|
})
|
|
@@ -317,12 +307,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
317
307
|
inputMessages.push({ role: 'user', content: input })
|
|
318
308
|
}
|
|
319
309
|
|
|
320
|
-
const inputMetadata =
|
|
310
|
+
const inputMetadata = {}
|
|
311
|
+
for (const key of Object.keys(parameters)) {
|
|
321
312
|
if (allowedParamKeys.has(key)) {
|
|
322
|
-
|
|
313
|
+
inputMetadata[key] = parameters[key]
|
|
323
314
|
}
|
|
324
|
-
|
|
325
|
-
}, {})
|
|
315
|
+
}
|
|
326
316
|
|
|
327
317
|
this._tagger.tagMetadata(span, inputMetadata)
|
|
328
318
|
|
|
@@ -354,21 +344,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
354
344
|
})
|
|
355
345
|
} else if (item.type === 'function_call') {
|
|
356
346
|
// Handle function_call type (responses API tool calls)
|
|
357
|
-
let args = item.arguments
|
|
358
|
-
// Parse arguments if it's a JSON string
|
|
359
|
-
if (typeof args === 'string') {
|
|
360
|
-
try {
|
|
361
|
-
args = JSON.parse(args)
|
|
362
|
-
} catch {
|
|
363
|
-
args = {}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
347
|
outputMessages.push({
|
|
367
348
|
role: 'assistant',
|
|
368
349
|
toolCalls: [{
|
|
369
350
|
toolId: item.call_id,
|
|
370
351
|
name: item.name,
|
|
371
|
-
arguments:
|
|
352
|
+
arguments: safeJsonParse(item.arguments, {}),
|
|
372
353
|
type: item.type,
|
|
373
354
|
}],
|
|
374
355
|
})
|
|
@@ -390,23 +371,12 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
390
371
|
|
|
391
372
|
// Extract tool calls if present in message.tool_calls
|
|
392
373
|
if (Array.isArray(item.tool_calls)) {
|
|
393
|
-
outputMsg.toolCalls = item.tool_calls.map(tc => {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
} catch {
|
|
400
|
-
args = {}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
return {
|
|
404
|
-
toolId: tc.id,
|
|
405
|
-
name: tc.function?.name || tc.name,
|
|
406
|
-
arguments: args,
|
|
407
|
-
type: tc.type || 'function_call',
|
|
408
|
-
}
|
|
409
|
-
})
|
|
374
|
+
outputMsg.toolCalls = item.tool_calls.map(tc => ({
|
|
375
|
+
toolId: tc.id,
|
|
376
|
+
name: tc.function?.name || tc.name,
|
|
377
|
+
arguments: safeJsonParse(tc.function?.arguments || tc.arguments, {}),
|
|
378
|
+
type: tc.type || 'function_call',
|
|
379
|
+
}))
|
|
410
380
|
}
|
|
411
381
|
|
|
412
382
|
outputMessages.push(outputMsg)
|
|
@@ -251,7 +251,7 @@ class LLMObs extends NoopLLMObs {
|
|
|
251
251
|
throw new Error('LLMObs span must have a span kind specified')
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
const { inputData, outputData, metadata, metrics, tags, prompt } = options
|
|
254
|
+
const { inputData, outputData, metadata, metrics, tags, prompt, costTags } = options
|
|
255
255
|
|
|
256
256
|
if (inputData || outputData) {
|
|
257
257
|
if (spanKind === 'llm') {
|
|
@@ -271,9 +271,13 @@ class LLMObs extends NoopLLMObs {
|
|
|
271
271
|
if (metrics) {
|
|
272
272
|
this._tagger.tagMetrics(span, metrics)
|
|
273
273
|
}
|
|
274
|
+
// Apply tags before costTags so costTags can reference tags from the same annotation.
|
|
274
275
|
if (tags) {
|
|
275
276
|
this._tagger.tagSpanTags(span, tags)
|
|
276
277
|
}
|
|
278
|
+
if (costTags != null) {
|
|
279
|
+
this._tagger.tagCostTags(span, costTags, 'annotate')
|
|
280
|
+
}
|
|
277
281
|
if (prompt) {
|
|
278
282
|
this._tagger.tagPrompt(span, prompt)
|
|
279
283
|
}
|
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
MODEL_NAME,
|
|
15
15
|
MODEL_PROVIDER,
|
|
16
16
|
METADATA,
|
|
17
|
+
COST_TAGS,
|
|
18
|
+
TOOL_DEFINITIONS,
|
|
17
19
|
INPUT_MESSAGES,
|
|
18
20
|
INPUT_VALUE,
|
|
19
21
|
INTEGRATION,
|
|
@@ -130,8 +132,20 @@ class LLMObsSpanProcessor {
|
|
|
130
132
|
meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
if (mlObsTags[METADATA]) {
|
|
134
|
-
|
|
135
|
+
if (mlObsTags[METADATA] || mlObsTags[COST_TAGS]) {
|
|
136
|
+
const metadata = {}
|
|
137
|
+
if (mlObsTags[METADATA]) this.#addObject(mlObsTags[METADATA], metadata)
|
|
138
|
+
// Only seed `metadata._dd` when there's something to put in it (currently cost_tags). Mirrors
|
|
139
|
+
// dd-trace-py and the cross-language wire format enforced by system-tests — metadata-only
|
|
140
|
+
// spans must not carry an empty `_dd: {}` block.
|
|
141
|
+
if (mlObsTags[COST_TAGS]) {
|
|
142
|
+
this.#getDdMetadata(metadata).cost_tags = mlObsTags[COST_TAGS]
|
|
143
|
+
}
|
|
144
|
+
meta.metadata = metadata
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (mlObsTags[TOOL_DEFINITIONS]) {
|
|
148
|
+
this.#addObject(mlObsTags[TOOL_DEFINITIONS], meta.tool_definitions = [])
|
|
135
149
|
}
|
|
136
150
|
|
|
137
151
|
if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
@@ -259,6 +273,18 @@ class LLMObsSpanProcessor {
|
|
|
259
273
|
add(obj, carrier)
|
|
260
274
|
}
|
|
261
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Returns `metadata._dd`, normalizing it to a fresh object if missing or invalid.
|
|
278
|
+
* @param {Record<string, unknown>} metadata
|
|
279
|
+
* @returns {Record<string, unknown>}
|
|
280
|
+
*/
|
|
281
|
+
#getDdMetadata (metadata) {
|
|
282
|
+
if (!metadata._dd || typeof metadata._dd !== 'object' || Array.isArray(metadata._dd)) {
|
|
283
|
+
metadata._dd = {}
|
|
284
|
+
}
|
|
285
|
+
return metadata._dd
|
|
286
|
+
}
|
|
287
|
+
|
|
262
288
|
#getTags (span, mlApp, sessionId, error) {
|
|
263
289
|
let tags = {
|
|
264
290
|
...this.#config.parsedDdTags,
|
|
@@ -12,7 +12,9 @@ const {
|
|
|
12
12
|
INPUT_DOCUMENTS,
|
|
13
13
|
OUTPUT_VALUE,
|
|
14
14
|
METADATA,
|
|
15
|
+
COST_TAGS,
|
|
15
16
|
METRICS,
|
|
17
|
+
TOOL_DEFINITIONS,
|
|
16
18
|
PARENT_ID_KEY,
|
|
17
19
|
INPUT_MESSAGES,
|
|
18
20
|
OUTPUT_MESSAGES,
|
|
@@ -41,6 +43,7 @@ const {
|
|
|
41
43
|
INSTRUMENTATION_METHOD_ANNOTATED,
|
|
42
44
|
} = require('./constants/tags')
|
|
43
45
|
const { storage } = require('./storage')
|
|
46
|
+
const { validateCostTags } = require('./util')
|
|
44
47
|
|
|
45
48
|
// global registry of LLMObs spans
|
|
46
49
|
// maps LLMObs spans to their annotations
|
|
@@ -120,6 +123,11 @@ class LLMObsTagger {
|
|
|
120
123
|
const tags = annotationContext?.tags
|
|
121
124
|
if (tags) this.tagSpanTags(span, tags)
|
|
122
125
|
|
|
126
|
+
// apply after tags so only keys present at span start are accepted.
|
|
127
|
+
if (annotationContext?.costTags != null) {
|
|
128
|
+
this.tagCostTags(span, annotationContext.costTags, 'annotation_context')
|
|
129
|
+
}
|
|
130
|
+
|
|
123
131
|
// apply annotation context name
|
|
124
132
|
const annotationContextName = annotationContext?.name
|
|
125
133
|
if (annotationContextName) this._setTag(span, NAME, annotationContextName)
|
|
@@ -159,6 +167,14 @@ class LLMObsTagger {
|
|
|
159
167
|
this.#tagText(span, outputData, OUTPUT_VALUE)
|
|
160
168
|
}
|
|
161
169
|
|
|
170
|
+
tagToolDefinitions (span, toolDefinitions) {
|
|
171
|
+
if (Array.isArray(toolDefinitions) && toolDefinitions.length > 0) {
|
|
172
|
+
this._setTag(span, TOOL_DEFINITIONS, toolDefinitions)
|
|
173
|
+
} else {
|
|
174
|
+
this.#handleFailure('Tool definitions must be a non-empty array.', 'invalid_tool_definitions')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
162
178
|
tagMetadata (span, metadata) {
|
|
163
179
|
const existingMetadata = registry.get(span)?.[METADATA]
|
|
164
180
|
if (existingMetadata) {
|
|
@@ -225,6 +241,32 @@ class LLMObsTagger {
|
|
|
225
241
|
}
|
|
226
242
|
}
|
|
227
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Validates and tags cost tag keys on an LLMObs span. Cost tag references are validated against
|
|
246
|
+
* the span's already-applied tags, which are read from the registry.
|
|
247
|
+
* @param {import('../opentracing/span')} span
|
|
248
|
+
* @param {unknown} costTags Raw user-provided cost tags; validated here.
|
|
249
|
+
* @param {'annotate' | 'annotation_context'} source
|
|
250
|
+
*/
|
|
251
|
+
tagCostTags (span, costTags, source) {
|
|
252
|
+
const spanTags = registry.get(span)?.[TAGS] || {}
|
|
253
|
+
const validatedCostTags = validateCostTags(span, costTags, source, spanTags)
|
|
254
|
+
if (!validatedCostTags.length) return
|
|
255
|
+
|
|
256
|
+
// Might consider switching to a `Set` if per-span cost tag cardinality grows large enough that
|
|
257
|
+
// this `.includes`/`.push` merge becomes a hot spot
|
|
258
|
+
const currentCostTags = registry.get(span)?.[COST_TAGS]
|
|
259
|
+
if (currentCostTags) {
|
|
260
|
+
for (const costTag of validatedCostTags) {
|
|
261
|
+
if (!currentCostTags.includes(costTag)) {
|
|
262
|
+
currentCostTags.push(costTag)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
this._setTag(span, COST_TAGS, validatedCostTags)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
228
270
|
/**
|
|
229
271
|
* Tags a prompt on an LLMObs span.
|
|
230
272
|
* @param {import('../opentracing/span')} span
|
|
@@ -6,6 +6,7 @@ const telemetryMetrics = require('../telemetry/metrics')
|
|
|
6
6
|
const {
|
|
7
7
|
SPAN_KIND,
|
|
8
8
|
MODEL_PROVIDER,
|
|
9
|
+
ML_APP,
|
|
9
10
|
PARENT_ID_KEY,
|
|
10
11
|
SESSION_ID,
|
|
11
12
|
ROOT_PARENT_ID,
|
|
@@ -123,6 +124,32 @@ function recordLLMObsAnnotate (span, err, value = 1) {
|
|
|
123
124
|
llmobsMetrics.count('annotations', tags).inc(value)
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
function recordCostTagsAnnotated (span, source, value = 1) {
|
|
128
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
|
|
129
|
+
const tags = {
|
|
130
|
+
span_kind: mlObsTags[SPAN_KIND] || 'N/A',
|
|
131
|
+
source,
|
|
132
|
+
ml_app: mlObsTags[ML_APP] || 'N/A',
|
|
133
|
+
model_provider: mlObsTags[MODEL_PROVIDER] || 'N/A',
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
llmobsMetrics.count('cost_tags.annotated', tags).inc(value)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function recordCostTagsSubmitted (span, count, source, state, reason = 'none') {
|
|
140
|
+
const mlObsTags = LLMObsTagger.tagMap.get(span) || {}
|
|
141
|
+
const tags = {
|
|
142
|
+
span_kind: mlObsTags[SPAN_KIND] || 'N/A',
|
|
143
|
+
source,
|
|
144
|
+
ml_app: mlObsTags[ML_APP] || 'N/A',
|
|
145
|
+
model_provider: mlObsTags[MODEL_PROVIDER] || 'N/A',
|
|
146
|
+
state,
|
|
147
|
+
reason,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
llmobsMetrics.count('cost_tags.submitted', tags).inc(count)
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
function recordUserFlush (err, value = 1) {
|
|
127
154
|
const tags = { error: Number(!!err) }
|
|
128
155
|
if (err) tags.error_type = err
|
|
@@ -167,6 +194,8 @@ module.exports = {
|
|
|
167
194
|
recordLLMObsSpanSize,
|
|
168
195
|
recordDroppedPayload,
|
|
169
196
|
recordLLMObsAnnotate,
|
|
197
|
+
recordCostTagsAnnotated,
|
|
198
|
+
recordCostTagsSubmitted,
|
|
170
199
|
recordUserFlush,
|
|
171
200
|
recordExportSpan,
|
|
172
201
|
recordSubmitEvaluation,
|