dd-trace 6.18.0 → 6.19.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 +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +3 -2
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +40 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/playwright.js +26 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +19 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -4
- package/packages/dd-trace/src/config/supported-configurations.json +5 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
- package/packages/dd-trace/src/llmobs/tagger.js +2 -36
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/span_processor.js +6 -1
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../log')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits an SDK stream, consumes one branch, and returns the other after inspection.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} stream
|
|
9
|
+
* @param {(chunks: Array<object>) => void|Promise<void>} inspect
|
|
10
|
+
* @returns {object|Promise<object>}
|
|
11
|
+
*/
|
|
12
|
+
function interceptStream (stream, inspect) {
|
|
13
|
+
if (typeof stream?.tee !== 'function') return stream
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const [inspectionStream, resultStream] = stream.tee()
|
|
17
|
+
return drainStream(inspectionStream).then(chunks => {
|
|
18
|
+
return Promise.resolve(inspect(chunks)).then(() => resultStream)
|
|
19
|
+
})
|
|
20
|
+
} catch {
|
|
21
|
+
return stream
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Buffers the whole stream. A stream that ends early still yields what it delivered: truncated
|
|
27
|
+
* output can carry the very violation the evaluation is looking for, so it is judged rather than
|
|
28
|
+
* skipped, and the read failure is logged because nothing downstream reports it.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} stream
|
|
31
|
+
* @returns {Promise<Array<object>>}
|
|
32
|
+
*/
|
|
33
|
+
function drainStream (stream) {
|
|
34
|
+
const chunks = []
|
|
35
|
+
const iterator = stream[Symbol.asyncIterator]()
|
|
36
|
+
|
|
37
|
+
function onReadError (error) {
|
|
38
|
+
log.error('AIGuard: the streamed response ended after %s chunks: %s', chunks.length, error)
|
|
39
|
+
return chunks
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readAll () {
|
|
43
|
+
let next
|
|
44
|
+
try {
|
|
45
|
+
next = iterator.next()
|
|
46
|
+
} catch (error) {
|
|
47
|
+
return Promise.resolve(onReadError(error))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return next.then(({ done, value }) => {
|
|
51
|
+
if (done) return chunks
|
|
52
|
+
chunks.push(value)
|
|
53
|
+
return readAll()
|
|
54
|
+
}, onReadError)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return readAll()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { interceptStream }
|
|
@@ -431,10 +431,74 @@ function getMessagesOutputMessages (body) {
|
|
|
431
431
|
return convertAnthropicMessage({ role, content: body.content })
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
/**
|
|
435
|
+
* Combines Anthropic message stream events into regular output messages.
|
|
436
|
+
*
|
|
437
|
+
* Accumulates exactly the way the SDK does: every `content_block_start` appends a block, and
|
|
438
|
+
* deltas address blocks by position. Keying blocks by `event.index` instead would let a repeated
|
|
439
|
+
* or out-of-range index hide output that the caller still receives.
|
|
440
|
+
*
|
|
441
|
+
* @param {Array<object>} events
|
|
442
|
+
* @returns {Array<object>}
|
|
443
|
+
*/
|
|
444
|
+
function getStreamedMessagesOutputMessages (events) {
|
|
445
|
+
let message
|
|
446
|
+
let contentBlocks
|
|
447
|
+
// Keyed by block, not by index: distinct indices can resolve to one block, and a second
|
|
448
|
+
// message must not inherit partial JSON accumulated for the first.
|
|
449
|
+
const inputJson = new Map()
|
|
450
|
+
|
|
451
|
+
for (const event of events) {
|
|
452
|
+
if (!event || typeof event !== 'object') continue
|
|
453
|
+
|
|
454
|
+
if (event.type === 'message_start' && event.message && typeof event.message === 'object') {
|
|
455
|
+
contentBlocks = Array.isArray(event.message.content)
|
|
456
|
+
? event.message.content.map(block => ({ ...block }))
|
|
457
|
+
: []
|
|
458
|
+
message = { role: event.message.role || 'assistant' }
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (event.type === 'content_block_start' && event.content_block && typeof event.content_block === 'object') {
|
|
463
|
+
message ??= { role: 'assistant' }
|
|
464
|
+
contentBlocks ??= []
|
|
465
|
+
contentBlocks.push({ ...event.content_block })
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (event.type !== 'content_block_delta' || !event.delta || typeof event.delta !== 'object') continue
|
|
470
|
+
|
|
471
|
+
const block = contentBlocks?.at(event.index ?? 0)
|
|
472
|
+
if (!block) continue
|
|
473
|
+
|
|
474
|
+
if (event.delta.type === 'text_delta' && block.type === 'text' && typeof event.delta.text === 'string') {
|
|
475
|
+
block.text = (block.text || '') + event.delta.text
|
|
476
|
+
} else if (event.delta.type === 'input_json_delta' && typeof event.delta.partial_json === 'string') {
|
|
477
|
+
inputJson.set(block, (inputJson.get(block) || '') + event.delta.partial_json)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!message) return []
|
|
482
|
+
|
|
483
|
+
for (const [block, json] of inputJson) {
|
|
484
|
+
// An empty buffer is what a no-argument tool call accumulates; the SDK keeps `{}` there.
|
|
485
|
+
if (!json) continue
|
|
486
|
+
try {
|
|
487
|
+
block.input = JSON.parse(json)
|
|
488
|
+
} catch {
|
|
489
|
+
block.input = json
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
message.content = contentBlocks
|
|
494
|
+
return getMessagesOutputMessages(message)
|
|
495
|
+
}
|
|
496
|
+
|
|
434
497
|
module.exports = {
|
|
435
498
|
convertAnthropicSystem,
|
|
436
499
|
convertAnthropicBlocksToContent,
|
|
437
500
|
convertAnthropicMessage,
|
|
438
501
|
getMessagesInputMessages,
|
|
439
502
|
getMessagesOutputMessages,
|
|
503
|
+
getStreamedMessagesOutputMessages,
|
|
440
504
|
}
|
|
@@ -574,8 +574,8 @@ export interface GeneratedConfig {
|
|
|
574
574
|
DD_CODE_COVERAGE_FLAGS: string | undefined;
|
|
575
575
|
DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: number | undefined;
|
|
576
576
|
DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
|
|
577
|
-
DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean
|
|
578
|
-
DD_TEST_FAILURE_VIDEOS_ENABLED: boolean
|
|
577
|
+
DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean;
|
|
578
|
+
DD_TEST_FAILURE_VIDEOS_ENABLED: boolean;
|
|
579
579
|
DD_TEST_FLEET_CONFIG_PATH: string | undefined;
|
|
580
580
|
DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
|
|
581
581
|
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
|
|
@@ -835,8 +835,8 @@ export interface GeneratedEnvVarConfig {
|
|
|
835
835
|
DD_TELEMETRY_METRICS_ENABLED: boolean;
|
|
836
836
|
DD_TEST_EARLY_FLAKE_DETECTION_RETRY_COUNT: number | undefined;
|
|
837
837
|
DD_TEST_FAILED_TEST_REPLAY_ENABLED: boolean;
|
|
838
|
-
DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean
|
|
839
|
-
DD_TEST_FAILURE_VIDEOS_ENABLED: boolean
|
|
838
|
+
DD_TEST_FAILURE_SCREENSHOTS_ENABLED: boolean;
|
|
839
|
+
DD_TEST_FAILURE_VIDEOS_ENABLED: boolean;
|
|
840
840
|
DD_TEST_FLEET_CONFIG_PATH: string | undefined;
|
|
841
841
|
DD_TEST_LOCAL_CONFIG_PATH: string | undefined;
|
|
842
842
|
DD_TEST_MANAGEMENT_ATTEMPT_TO_FIX_RETRIES: number;
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"type": "boolean",
|
|
60
60
|
"namespace": "aiguard",
|
|
61
61
|
"default": "false",
|
|
62
|
-
"description": "Analyze streamed OpenAI and Vercel AI responses with AI Guard After Model evaluation."
|
|
62
|
+
"description": "Analyze streamed Anthropic, OpenAI, and Vercel AI responses with AI Guard After Model evaluation."
|
|
63
63
|
}
|
|
64
64
|
],
|
|
65
65
|
"DD_AI_GUARD_BLOCK": [
|
|
@@ -2055,17 +2055,17 @@
|
|
|
2055
2055
|
],
|
|
2056
2056
|
"DD_TEST_FAILURE_SCREENSHOTS_ENABLED": [
|
|
2057
2057
|
{
|
|
2058
|
-
"implementation": "
|
|
2058
|
+
"implementation": "B",
|
|
2059
2059
|
"type": "boolean",
|
|
2060
|
-
"default":
|
|
2060
|
+
"default": "true",
|
|
2061
2061
|
"namespace": "testOptimization"
|
|
2062
2062
|
}
|
|
2063
2063
|
],
|
|
2064
2064
|
"DD_TEST_FAILURE_VIDEOS_ENABLED": [
|
|
2065
2065
|
{
|
|
2066
|
-
"implementation": "
|
|
2066
|
+
"implementation": "B",
|
|
2067
2067
|
"type": "boolean",
|
|
2068
|
-
"default":
|
|
2068
|
+
"default": "true",
|
|
2069
2069
|
"namespace": "testOptimization"
|
|
2070
2070
|
}
|
|
2071
2071
|
],
|
|
@@ -22,6 +22,8 @@ module.exports = {
|
|
|
22
22
|
SPAN_SAMPLING_RULE_RATE: '_dd.span_sampling.rule_rate',
|
|
23
23
|
SPAN_SAMPLING_MAX_PER_SECOND: '_dd.span_sampling.max_per_second',
|
|
24
24
|
SVC_SRC_KEY: '_dd.svc_src',
|
|
25
|
+
SDK_OTLP_EXPORT_KEY: '_dd.sdk.otlp_export',
|
|
26
|
+
SDK_SEMANTICS_KEY: 'datadog.sdk.semantics',
|
|
25
27
|
DATADOG_LAMBDA_EXTENSION_PATH: '/opt/extensions/datadog-agent',
|
|
26
28
|
DATADOG_MINI_AGENT_PATH: '/tmp/datadog/mini_agent_ready',
|
|
27
29
|
DECISION_MAKER_KEY: '_dd.p.dm',
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const INPUT_TOKENS_METRIC_KEY = 'input_tokens'
|
|
4
|
+
const OUTPUT_TOKENS_METRIC_KEY = 'output_tokens'
|
|
5
|
+
const TOTAL_TOKENS_METRIC_KEY = 'total_tokens'
|
|
6
|
+
const CACHE_READ_INPUT_TOKENS_METRIC_KEY = 'cache_read_input_tokens'
|
|
7
|
+
const CACHE_WRITE_INPUT_TOKENS_METRIC_KEY = 'cache_write_input_tokens'
|
|
8
|
+
const CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY = 'ephemeral_5m_input_tokens'
|
|
9
|
+
const CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY = 'ephemeral_1h_input_tokens'
|
|
10
|
+
const REASONING_OUTPUT_TOKENS_METRIC_KEY = 'reasoning_output_tokens'
|
|
11
|
+
|
|
3
12
|
module.exports = {
|
|
4
13
|
SPAN_KINDS: ['llm', 'agent', 'workflow', 'task', 'tool', 'embedding', 'retrieval', 'experiment'],
|
|
5
14
|
SPAN_KIND: '_ml_obs.meta.span.kind',
|
|
@@ -42,6 +51,7 @@ module.exports = {
|
|
|
42
51
|
MODEL_NAME: '_ml_obs.meta.model_name',
|
|
43
52
|
MODEL_PROVIDER: '_ml_obs.meta.model_provider',
|
|
44
53
|
UNKNOWN_MODEL_PROVIDER: 'unknown',
|
|
54
|
+
DEFAULT_MODEL: 'custom',
|
|
45
55
|
|
|
46
56
|
INPUT_DOCUMENTS: '_ml_obs.meta.input.documents',
|
|
47
57
|
INPUT_MESSAGES: '_ml_obs.meta.input.messages',
|
|
@@ -52,14 +62,27 @@ module.exports = {
|
|
|
52
62
|
OUTPUT_MESSAGES: '_ml_obs.meta.output.messages',
|
|
53
63
|
OUTPUT_VALUE: '_ml_obs.meta.output.value',
|
|
54
64
|
|
|
55
|
-
INPUT_TOKENS_METRIC_KEY
|
|
56
|
-
OUTPUT_TOKENS_METRIC_KEY
|
|
57
|
-
TOTAL_TOKENS_METRIC_KEY
|
|
58
|
-
CACHE_READ_INPUT_TOKENS_METRIC_KEY
|
|
59
|
-
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY
|
|
60
|
-
CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY
|
|
61
|
-
CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY
|
|
62
|
-
REASONING_OUTPUT_TOKENS_METRIC_KEY
|
|
65
|
+
INPUT_TOKENS_METRIC_KEY,
|
|
66
|
+
OUTPUT_TOKENS_METRIC_KEY,
|
|
67
|
+
TOTAL_TOKENS_METRIC_KEY,
|
|
68
|
+
CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
69
|
+
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
70
|
+
CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY,
|
|
71
|
+
CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY,
|
|
72
|
+
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
73
|
+
|
|
74
|
+
// integrations build metric objects with these camelCase spellings. Null prototype: a metric
|
|
75
|
+
// named after an `Object.prototype` member must not resolve to an inherited property.
|
|
76
|
+
METRIC_KEY_ALIASES: Object.assign(Object.create(null), {
|
|
77
|
+
inputTokens: INPUT_TOKENS_METRIC_KEY,
|
|
78
|
+
outputTokens: OUTPUT_TOKENS_METRIC_KEY,
|
|
79
|
+
totalTokens: TOTAL_TOKENS_METRIC_KEY,
|
|
80
|
+
cacheReadTokens: CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
81
|
+
cacheWriteTokens: CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
82
|
+
cacheWrite5mTokens: CACHE_WRITE_5M_INPUT_TOKENS_METRIC_KEY,
|
|
83
|
+
cacheWrite1hTokens: CACHE_WRITE_1H_INPUT_TOKENS_METRIC_KEY,
|
|
84
|
+
reasoningOutputTokens: REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
85
|
+
}),
|
|
63
86
|
|
|
64
87
|
DROPPED_IO_COLLECTION_ERROR: 'dropped_io',
|
|
65
88
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
ARTIFICIAL_GEN_AI_TAGS,
|
|
5
|
+
CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
6
|
+
CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
7
|
+
DEFAULT_MODEL,
|
|
8
|
+
GEN_AI_APPLICATION_NAME,
|
|
9
|
+
GEN_AI_CONVERSATION_ID,
|
|
10
|
+
GEN_AI_OPERATION_NAME,
|
|
11
|
+
GEN_AI_PROVIDER_NAME,
|
|
12
|
+
GEN_AI_REQUEST_MODEL,
|
|
13
|
+
GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
14
|
+
GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
15
|
+
GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
|
|
16
|
+
GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
|
|
17
|
+
GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
18
|
+
GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
|
|
19
|
+
INPUT_TOKENS_METRIC_KEY,
|
|
20
|
+
METRIC_KEY_ALIASES,
|
|
21
|
+
OUTPUT_TOKENS_METRIC_KEY,
|
|
22
|
+
REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
23
|
+
TOTAL_TOKENS_METRIC_KEY,
|
|
24
|
+
} = require('./constants/tags')
|
|
25
|
+
|
|
26
|
+
/** @type {Set<string | undefined>} */
|
|
27
|
+
const MODEL_BACKED_SPAN_KINDS = new Set(['llm', 'embedding'])
|
|
28
|
+
|
|
29
|
+
// null prototype: a metric named after an `Object.prototype` member must not resolve to an
|
|
30
|
+
// inherited property
|
|
31
|
+
const GEN_AI_USAGE_METRIC_KEYS = Object.assign(Object.create(null), {
|
|
32
|
+
[INPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_INPUT_TOKENS_METRIC_KEY,
|
|
33
|
+
[OUTPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_OUTPUT_TOKENS_METRIC_KEY,
|
|
34
|
+
[TOTAL_TOKENS_METRIC_KEY]: GEN_AI_USAGE_TOTAL_TOKENS_METRIC_KEY,
|
|
35
|
+
[CACHE_READ_INPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_CACHE_READ_INPUT_TOKENS_METRIC_KEY,
|
|
36
|
+
[CACHE_WRITE_INPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_CACHE_WRITE_INPUT_TOKENS_METRIC_KEY,
|
|
37
|
+
[REASONING_OUTPUT_TOKENS_METRIC_KEY]: GEN_AI_USAGE_REASONING_OUTPUT_TOKENS_METRIC_KEY,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {object} GenAiApmTags
|
|
42
|
+
* @property {string} [spanKind] LLMObs span kind
|
|
43
|
+
* @property {string} [modelName]
|
|
44
|
+
* @property {string} [modelProvider]
|
|
45
|
+
* @property {string} [mlApp]
|
|
46
|
+
* @property {string} [sessionId]
|
|
47
|
+
* @property {Record<string, unknown>} [metrics] LLMObs metrics, in either spelling
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Writes the scalar `gen_ai.*` attributes onto the APM span, so model, provider, application,
|
|
52
|
+
* conversation and token usage are searchable in APM. Message bodies stay off the APM span.
|
|
53
|
+
*
|
|
54
|
+
* @param {import('../opentracing/span')} span
|
|
55
|
+
* @param {GenAiApmTags} tags
|
|
56
|
+
*/
|
|
57
|
+
function setGenAiApmTags (span, tags) {
|
|
58
|
+
// mirrors the LLMObs span event: a model-backed span always reports a model and provider
|
|
59
|
+
updateGenAiApmTags(span, MODEL_BACKED_SPAN_KINDS.has(tags.spanKind)
|
|
60
|
+
? { ...tags, modelName: tags.modelName || DEFAULT_MODEL, modelProvider: tags.modelProvider || DEFAULT_MODEL }
|
|
61
|
+
: tags)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Writes only the `gen_ai.*` attributes present in `tags`, for values an integration resolves
|
|
66
|
+
* after the span started. Absent fields keep whatever the span already carries.
|
|
67
|
+
*
|
|
68
|
+
* @param {import('../opentracing/span')} span
|
|
69
|
+
* @param {GenAiApmTags} tags
|
|
70
|
+
*/
|
|
71
|
+
function updateGenAiApmTags (span, { spanKind, modelName, modelProvider, mlApp, sessionId, metrics }) {
|
|
72
|
+
const spanContext = span.context()
|
|
73
|
+
|
|
74
|
+
// written ahead of the attributes it covers: a throw partway through would otherwise leave
|
|
75
|
+
// `gen_ai.*` tags behind with nothing marking them as tracer-emitted
|
|
76
|
+
if (spanKind || modelName || modelProvider || mlApp || sessionId) {
|
|
77
|
+
markArtificialGenAiTags(spanContext)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (spanKind) spanContext.setTag(GEN_AI_OPERATION_NAME, spanKind)
|
|
81
|
+
if (modelName) spanContext.setTag(GEN_AI_REQUEST_MODEL, modelName)
|
|
82
|
+
if (modelProvider) spanContext.setTag(GEN_AI_PROVIDER_NAME, modelProvider.toLowerCase())
|
|
83
|
+
if (mlApp) spanContext.setTag(GEN_AI_APPLICATION_NAME, mlApp)
|
|
84
|
+
if (sessionId) spanContext.setTag(GEN_AI_CONVERSATION_ID, sessionId)
|
|
85
|
+
if (metrics) setGenAiApmUsageMetrics(span, spanKind, metrics)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {import('../opentracing/span_context')} spanContext
|
|
90
|
+
*/
|
|
91
|
+
function markArtificialGenAiTags (spanContext) {
|
|
92
|
+
spanContext.setTag(ARTIFICIAL_GEN_AI_TAGS, 'true')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Writes the `gen_ai.usage.*` metrics onto the APM span. Accepts both the LLMObs metric keys and
|
|
97
|
+
* the camelCase spellings integrations extract before the tagger normalizes them.
|
|
98
|
+
*
|
|
99
|
+
* @param {import('../opentracing/span')} span
|
|
100
|
+
* @param {string | undefined} spanKind LLMObs span kind
|
|
101
|
+
* @param {Record<string, unknown>} metrics
|
|
102
|
+
*/
|
|
103
|
+
function setGenAiApmUsageMetrics (span, spanKind, metrics) {
|
|
104
|
+
// Other kinds carry unrelated metrics that would be misleading under a `gen_ai.usage.*` key.
|
|
105
|
+
if (!MODEL_BACKED_SPAN_KINDS.has(spanKind)) return
|
|
106
|
+
|
|
107
|
+
const spanContext = span.context()
|
|
108
|
+
|
|
109
|
+
// ahead of the metrics, for the same reason `updateGenAiApmTags` marks before its scalars
|
|
110
|
+
markArtificialGenAiTags(spanContext)
|
|
111
|
+
|
|
112
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
113
|
+
if (typeof value !== 'number') continue
|
|
114
|
+
|
|
115
|
+
const genAiKey = GEN_AI_USAGE_METRIC_KEYS[METRIC_KEY_ALIASES[key] ?? key]
|
|
116
|
+
if (!genAiKey) continue
|
|
117
|
+
|
|
118
|
+
spanContext.setTag(genAiKey, value)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
MODEL_BACKED_SPAN_KINDS,
|
|
124
|
+
setGenAiApmTags,
|
|
125
|
+
setGenAiApmUsageMetrics,
|
|
126
|
+
updateGenAiApmTags,
|
|
127
|
+
}
|
|
@@ -89,6 +89,24 @@ class DdTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
89
89
|
return { kind, name: getLlmObsSpanName(operation, ctx.attributes['ai.telemetry.functionId']) }
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @override
|
|
94
|
+
*/
|
|
95
|
+
getGenAiApmEndTags (ctx, spanKind) {
|
|
96
|
+
if (spanKind !== 'llm' && spanKind !== 'embedding') return
|
|
97
|
+
|
|
98
|
+
const tags = /** @type {Record<string, string>} */ (getSpanTags(ctx))
|
|
99
|
+
const embeddingUsage = tags['ai.usage.tokens']
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
modelName: tags['ai.model.id'],
|
|
103
|
+
modelProvider: getModelProvider(tags),
|
|
104
|
+
metrics: spanKind === 'embedding'
|
|
105
|
+
? { inputTokens: embeddingUsage, totalTokens: embeddingUsage }
|
|
106
|
+
: getUsage(tags),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
/**
|
|
93
111
|
* @override
|
|
94
112
|
*/
|
|
@@ -140,6 +140,20 @@ function formatLanguageModelOutputMessages (content) {
|
|
|
140
140
|
return outputMessages
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* @param {object} [usage] AI SDK usage, from the result or the stream's `finish` chunk
|
|
145
|
+
* @returns {Record<string, number | undefined>}
|
|
146
|
+
*/
|
|
147
|
+
function extractUsageMetrics (usage) {
|
|
148
|
+
return {
|
|
149
|
+
inputTokens: usage?.inputTokens?.total,
|
|
150
|
+
cacheWriteTokens: usage?.inputTokens?.cacheWrite ?? 0,
|
|
151
|
+
cacheReadTokens: usage?.inputTokens?.cacheRead ?? 0,
|
|
152
|
+
outputTokens: usage?.outputTokens?.total,
|
|
153
|
+
reasoningOutputTokens: usage?.outputTokens?.reasoning ?? 0,
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
143
157
|
class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
144
158
|
static id = 'ai_llmobs_vercel_telemetry'
|
|
145
159
|
static integration = 'ai'
|
|
@@ -152,6 +166,13 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
152
166
|
super(...arguments)
|
|
153
167
|
|
|
154
168
|
this.addSub('dd-trace:vercel-ai:chunk', ({ ctx, chunk, done }) => {
|
|
169
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
170
|
+
// only the token usage is needed, for the `gen_ai.usage.*` metrics; the message bodies and
|
|
171
|
+
// `ctx.result` are left to the LLMObs path
|
|
172
|
+
if (chunk?.type === 'finish') ctx.streamedUsage = chunk.usage
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
ctx.chunks ??= []
|
|
156
177
|
const chunks = ctx.chunks
|
|
157
178
|
if (chunk) chunks.push(chunk)
|
|
@@ -202,6 +223,17 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
202
223
|
super.asyncEnd(ctx)
|
|
203
224
|
}
|
|
204
225
|
|
|
226
|
+
/**
|
|
227
|
+
* @override
|
|
228
|
+
*/
|
|
229
|
+
getGenAiApmEndTags (ctx, spanKind) {
|
|
230
|
+
const usage = ctx.result?.usage ?? ctx.streamedUsage
|
|
231
|
+
if (!usage) return {}
|
|
232
|
+
|
|
233
|
+
// `embed` reports a single token count, the generation operations a structured breakdown
|
|
234
|
+
return { metrics: spanKind === 'embedding' ? { inputTokens: usage.tokens } : extractUsageMetrics(usage) }
|
|
235
|
+
}
|
|
236
|
+
|
|
205
237
|
/**
|
|
206
238
|
* @override
|
|
207
239
|
*/
|
|
@@ -367,14 +399,7 @@ class VercelAiTelemetryPlugin extends BaseLLMObsPlugin {
|
|
|
367
399
|
if (!result) return
|
|
368
400
|
|
|
369
401
|
// metrics
|
|
370
|
-
|
|
371
|
-
this._tagger.tagMetrics(span, {
|
|
372
|
-
inputTokens: usage?.inputTokens?.total,
|
|
373
|
-
cacheWriteTokens: usage?.inputTokens?.cacheWrite ?? 0,
|
|
374
|
-
cacheReadTokens: usage?.inputTokens?.cacheRead ?? 0,
|
|
375
|
-
outputTokens: usage?.outputTokens?.total,
|
|
376
|
-
reasoningOutputTokens: usage?.outputTokens?.reasoning ?? 0,
|
|
377
|
-
})
|
|
402
|
+
this._tagger.tagMetrics(span, extractUsageMetrics(result.usage))
|
|
378
403
|
}
|
|
379
404
|
|
|
380
405
|
setToolTags (span, ctx) {
|
|
@@ -22,6 +22,13 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
22
22
|
super(...arguments)
|
|
23
23
|
|
|
24
24
|
this.addSub('apm:anthropic:request:chunk', ({ ctx, chunk, done }) => {
|
|
25
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
26
|
+
// only the token usage is needed, for the `gen_ai.usage.*` metrics; the message bodies
|
|
27
|
+
// and the aggregated response are left to the LLMObs path
|
|
28
|
+
if (chunk) ctx.streamedUsage = mergeChunkUsage(ctx.streamedUsage, chunk)
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
ctx.chunks ??= []
|
|
26
33
|
const chunks = ctx.chunks
|
|
27
34
|
if (chunk) chunks.push(chunk)
|
|
@@ -36,9 +43,8 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
36
43
|
const { message } = chunk
|
|
37
44
|
if (!message) continue
|
|
38
45
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (usage) response.usage = usage
|
|
46
|
+
if (message.role) response.role = message.role
|
|
47
|
+
response.usage = mergeChunkUsage(response.usage, chunk)
|
|
42
48
|
break
|
|
43
49
|
}
|
|
44
50
|
case 'content_block_start': {
|
|
@@ -86,22 +92,10 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
86
92
|
break
|
|
87
93
|
}
|
|
88
94
|
case 'message_delta': {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const finishReason = delta?.stop_reason
|
|
95
|
+
const finishReason = chunk.delta?.stop_reason
|
|
92
96
|
if (finishReason) response.finish_reason = finishReason
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
if (usage) {
|
|
96
|
-
const responseUsage = (response.usage ??= { input_tokens: 0, output_tokens: 0 })
|
|
97
|
-
responseUsage.output_tokens = usage.output_tokens
|
|
98
|
-
|
|
99
|
-
const cacheCreationTokens = usage.cache_creation_input_tokens
|
|
100
|
-
const cacheReadTokens = usage.cache_read_input_tokens
|
|
101
|
-
if (cacheCreationTokens) responseUsage.cache_creation_input_tokens = cacheCreationTokens
|
|
102
|
-
if (cacheReadTokens) responseUsage.cache_read_input_tokens = cacheReadTokens
|
|
103
|
-
}
|
|
104
|
-
|
|
98
|
+
response.usage = mergeChunkUsage(response.usage, chunk)
|
|
105
99
|
break
|
|
106
100
|
}
|
|
107
101
|
case 'error': {
|
|
@@ -140,6 +134,13 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
140
134
|
return UNKNOWN_MODEL_PROVIDER
|
|
141
135
|
}
|
|
142
136
|
|
|
137
|
+
/**
|
|
138
|
+
* @override
|
|
139
|
+
*/
|
|
140
|
+
getGenAiApmEndTags (ctx) {
|
|
141
|
+
return { metrics: extractUsage(ctx.result ?? { usage: ctx.streamedUsage }) }
|
|
142
|
+
}
|
|
143
|
+
|
|
143
144
|
setLLMObsTags (ctx) {
|
|
144
145
|
const span = ctx.currentStore?.span
|
|
145
146
|
if (!span) return
|
|
@@ -213,38 +214,73 @@ class AnthropicLLMObsPlugin extends LLMObsPlugin {
|
|
|
213
214
|
}
|
|
214
215
|
|
|
215
216
|
#tagAnthropicUsage (span, result) {
|
|
216
|
-
|
|
217
|
+
const metrics = extractUsage(result)
|
|
218
|
+
if (metrics) this._tagger.tagMetrics(span, metrics)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Merges the token usage a streamed chunk carries into the usage accumulated so far.
|
|
224
|
+
*
|
|
225
|
+
* @param {Record<string, number> | undefined} usage
|
|
226
|
+
* @param {object} chunk
|
|
227
|
+
* @returns {Record<string, number> | undefined}
|
|
228
|
+
*/
|
|
229
|
+
function mergeChunkUsage (usage, chunk) {
|
|
230
|
+
if (chunk.type === 'message_start') {
|
|
231
|
+
// copied: the `message_delta` counts are merged into this object, and the chunk is handed back
|
|
232
|
+
// to the application
|
|
233
|
+
const startUsage = chunk.message?.usage
|
|
234
|
+
return startUsage ? { ...startUsage } : usage
|
|
235
|
+
}
|
|
220
236
|
|
|
221
|
-
|
|
222
|
-
const outputTokens = usage.output_tokens
|
|
223
|
-
const cacheWriteTokens = usage.cache_creation_input_tokens
|
|
224
|
-
const cacheReadTokens = usage.cache_read_input_tokens
|
|
237
|
+
if (chunk.type !== 'message_delta' || !chunk.usage) return usage
|
|
225
238
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
239
|
+
const mergedUsage = usage ?? { input_tokens: 0, output_tokens: 0 }
|
|
240
|
+
mergedUsage.output_tokens = chunk.usage.output_tokens
|
|
241
|
+
|
|
242
|
+
const cacheCreationTokens = chunk.usage.cache_creation_input_tokens
|
|
243
|
+
const cacheReadTokens = chunk.usage.cache_read_input_tokens
|
|
244
|
+
if (cacheCreationTokens) mergedUsage.cache_creation_input_tokens = cacheCreationTokens
|
|
245
|
+
if (cacheReadTokens) mergedUsage.cache_read_input_tokens = cacheReadTokens
|
|
229
246
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (totalTokens) metrics.totalTokens = totalTokens
|
|
247
|
+
return mergedUsage
|
|
248
|
+
}
|
|
233
249
|
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
/**
|
|
251
|
+
* @param {object} [result]
|
|
252
|
+
* @returns {Record<string, number> | undefined}
|
|
253
|
+
*/
|
|
254
|
+
function extractUsage (result) {
|
|
255
|
+
const usage = result?.usage
|
|
256
|
+
if (!usage) return
|
|
257
|
+
|
|
258
|
+
const inputTokens = usage.input_tokens
|
|
259
|
+
const outputTokens = usage.output_tokens
|
|
260
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens
|
|
261
|
+
const cacheReadTokens = usage.cache_read_input_tokens
|
|
262
|
+
|
|
263
|
+
const metrics = {
|
|
264
|
+
inputTokens: (inputTokens ?? 0) + (cacheWriteTokens ?? 0) + (cacheReadTokens ?? 0),
|
|
265
|
+
}
|
|
236
266
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
metrics.cacheWrite1hTokens = cacheCreation.ephemeral_1h_input_tokens ?? 0
|
|
241
|
-
} else if (cacheWriteTokens != null) {
|
|
242
|
-
metrics.cacheWrite5mTokens = cacheWriteTokens
|
|
243
|
-
metrics.cacheWrite1hTokens = 0
|
|
244
|
-
}
|
|
267
|
+
if (outputTokens) metrics.outputTokens = outputTokens
|
|
268
|
+
const totalTokens = metrics.inputTokens + (outputTokens ?? 0)
|
|
269
|
+
if (totalTokens) metrics.totalTokens = totalTokens
|
|
245
270
|
|
|
246
|
-
|
|
271
|
+
if (cacheWriteTokens != null) metrics.cacheWriteTokens = cacheWriteTokens
|
|
272
|
+
if (cacheReadTokens != null) metrics.cacheReadTokens = cacheReadTokens
|
|
273
|
+
|
|
274
|
+
const cacheCreation = usage.cache_creation
|
|
275
|
+
if (cacheCreation) {
|
|
276
|
+
metrics.cacheWrite5mTokens = cacheCreation.ephemeral_5m_input_tokens ?? 0
|
|
277
|
+
metrics.cacheWrite1hTokens = cacheCreation.ephemeral_1h_input_tokens ?? 0
|
|
278
|
+
} else if (cacheWriteTokens != null) {
|
|
279
|
+
metrics.cacheWrite5mTokens = cacheWriteTokens
|
|
280
|
+
metrics.cacheWrite1hTokens = 0
|
|
247
281
|
}
|
|
282
|
+
|
|
283
|
+
return metrics
|
|
248
284
|
}
|
|
249
285
|
|
|
250
286
|
module.exports = AnthropicLLMObsPlugin
|