dd-trace 5.105.0 → 5.106.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/package.json +1 -1
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -2
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +19 -0
- package/packages/datadog-instrumentations/src/cucumber.js +312 -152
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +218 -4
- package/packages/dd-trace/src/id.js +15 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +91 -5
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +43 -21
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +22 -3
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -10
- package/packages/dd-trace/src/opentracing/span_context.js +1 -3
- package/packages/dd-trace/src/profiling/config.js +10 -23
- package/packages/dd-trace/src/profiling/exporters/agent.js +11 -10
- package/packages/dd-trace/src/profiling/profiler.js +19 -9
- package/packages/dd-trace/src/profiling/profilers/wall.js +2 -3
|
@@ -131,6 +131,7 @@ class Generation {
|
|
|
131
131
|
outputTokens,
|
|
132
132
|
cacheReadTokens,
|
|
133
133
|
cacheWriteTokens,
|
|
134
|
+
messages,
|
|
134
135
|
} = {}) {
|
|
135
136
|
// stringify message as it could be a single generated message as well as a list of embeddings
|
|
136
137
|
this.message = typeof message === 'string' ? message : JSON.stringify(message) || ''
|
|
@@ -143,6 +144,7 @@ class Generation {
|
|
|
143
144
|
cacheReadTokens,
|
|
144
145
|
cacheWriteTokens,
|
|
145
146
|
}
|
|
147
|
+
this.messages = messages ?? [{ content: this.message, role: this.role }]
|
|
146
148
|
}
|
|
147
149
|
}
|
|
148
150
|
|
|
@@ -401,10 +403,7 @@ function extractTextAndResponseReason (response, provider, modelName) {
|
|
|
401
403
|
message: output.message?.content[0]?.text ?? 'Unsupported content type',
|
|
402
404
|
finishReason: body.stopReason,
|
|
403
405
|
role: output.message?.role,
|
|
404
|
-
|
|
405
|
-
outputTokens: body.usage?.outputTokens,
|
|
406
|
-
cacheReadInputTokenCount: body.usage?.cacheReadInputTokenCount,
|
|
407
|
-
cacheWriteInputTokenCount: body.usage?.cacheWriteInputTokenCount,
|
|
406
|
+
...buildUsage(body.usage),
|
|
408
407
|
})
|
|
409
408
|
}
|
|
410
409
|
break
|
|
@@ -476,6 +475,216 @@ function extractTextAndResponseReason (response, provider, modelName) {
|
|
|
476
475
|
return new Generation()
|
|
477
476
|
}
|
|
478
477
|
|
|
478
|
+
/**
|
|
479
|
+
* Convert a Converse content-block array to an LLMObs message array.
|
|
480
|
+
*
|
|
481
|
+
* @param {string} role
|
|
482
|
+
* @param {Array<object>} contentBlocks
|
|
483
|
+
* @returns {{ content?: string, role: string, toolCalls?: Array, toolResults?: Array } | undefined}
|
|
484
|
+
*/
|
|
485
|
+
function extractMessagesFromConverseContent (role, contentBlocks) {
|
|
486
|
+
let content = ''
|
|
487
|
+
const toolCalls = []
|
|
488
|
+
const toolResults = []
|
|
489
|
+
|
|
490
|
+
for (const block of contentBlocks || []) {
|
|
491
|
+
if (block == null || typeof block !== 'object') continue
|
|
492
|
+
if (typeof block.text === 'string') {
|
|
493
|
+
content += block.text
|
|
494
|
+
} else if (block.toolUse) {
|
|
495
|
+
toolCalls.push(buildToolCall(block.toolUse))
|
|
496
|
+
} else if (block.toolResult) {
|
|
497
|
+
toolResults.push(buildToolResult(block.toolResult))
|
|
498
|
+
} else {
|
|
499
|
+
content += `[Unsupported content type: ${getContentBlockType(block)}]`
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (!content && toolCalls.length === 0 && toolResults.length === 0) return
|
|
504
|
+
|
|
505
|
+
const message = { role }
|
|
506
|
+
if (content) message.content = content
|
|
507
|
+
if (toolCalls.length > 0) message.toolCalls = toolCalls
|
|
508
|
+
if (toolResults.length > 0) message.toolResults = toolResults
|
|
509
|
+
return message
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Resolve a Converse `ContentBlock`'s member type. The block is a key-presence
|
|
514
|
+
* tagged union (no `type` discriminator), so the active member is its sole own
|
|
515
|
+
* key. For forward-compat `$unknown` members the real type is the first element
|
|
516
|
+
* of the `[name, value]` tuple.
|
|
517
|
+
*
|
|
518
|
+
* @param {object} block
|
|
519
|
+
* @returns {string}
|
|
520
|
+
*/
|
|
521
|
+
function getContentBlockType (block) {
|
|
522
|
+
const key = Object.keys(block)[0]
|
|
523
|
+
if (key === '$unknown') return block.$unknown?.[0] ?? 'unknown'
|
|
524
|
+
return key ?? 'unknown'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Always emit at least one output message so downstream tagging has a role to attach to.
|
|
528
|
+
function toOutputMessages (role, contentBlocks) {
|
|
529
|
+
const message = extractMessagesFromConverseContent(role, contentBlocks)
|
|
530
|
+
return message ? [message] : [{ role, content: '' }]
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function buildToolCall ({ name, input, toolUseId }) {
|
|
534
|
+
return { name: name ?? '', arguments: input ?? {}, toolId: toolUseId ?? '', type: 'toolUse' }
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function parseToolInput (inputStr) {
|
|
538
|
+
try {
|
|
539
|
+
return JSON.parse(inputStr)
|
|
540
|
+
} catch {
|
|
541
|
+
log.warn('Failed to parse Converse stream toolUse.input JSON; emitting empty arguments')
|
|
542
|
+
return {}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildToolResult ({ toolUseId, content }) {
|
|
547
|
+
const result = (content || []).map(resolveToolResultItem).join('')
|
|
548
|
+
return { name: '', result, toolId: toolUseId ?? '', type: 'tool_result' }
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function resolveToolResultItem (item) {
|
|
552
|
+
if (typeof item.text === 'string') return item.text
|
|
553
|
+
if (item.json != null) return JSON.stringify(item.json)
|
|
554
|
+
return `[Unsupported content type(s): ${getContentBlockType(item)}]`
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function buildUsage (usage = {}) {
|
|
558
|
+
return {
|
|
559
|
+
inputTokens: usage.inputTokens,
|
|
560
|
+
outputTokens: usage.outputTokens,
|
|
561
|
+
cacheReadTokens: usage.cacheReadInputTokens ?? usage.cacheReadInputTokenCount,
|
|
562
|
+
cacheWriteTokens: usage.cacheWriteInputTokens ?? usage.cacheWriteInputTokenCount,
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Extract tool definitions from a Converse request's `toolConfig.tools`,
|
|
568
|
+
* mapping Bedrock's `toolSpec` shape to LLMObs `ToolDefinition` shape.
|
|
569
|
+
*
|
|
570
|
+
* @param {object} params - Converse request params with optional `toolConfig.tools[].toolSpec`.
|
|
571
|
+
* @returns {Array<{ name: string, description: string, schema: object }>}
|
|
572
|
+
*/
|
|
573
|
+
function extractConverseToolDefinitions (params) {
|
|
574
|
+
const toolDefinitions = []
|
|
575
|
+
for (const tool of params.toolConfig?.tools || []) {
|
|
576
|
+
const toolSpec = tool?.toolSpec
|
|
577
|
+
if (!toolSpec?.name) continue
|
|
578
|
+
toolDefinitions.push({
|
|
579
|
+
name: toolSpec.name,
|
|
580
|
+
description: toolSpec.description ?? '',
|
|
581
|
+
schema: toolSpec.inputSchema ?? {},
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
return toolDefinitions
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Extract request metadata + rendered input messages from a Converse /
|
|
589
|
+
* ConverseStream request.
|
|
590
|
+
*
|
|
591
|
+
* @param {{ modelId?: string, messages?: Array, system?: Array, inferenceConfig?: object, toolConfig?: object }} params
|
|
592
|
+
* @returns {RequestParams}
|
|
593
|
+
*/
|
|
594
|
+
function extractRequestParamsConverse (params) {
|
|
595
|
+
const prompt = []
|
|
596
|
+
for (const block of params.system || []) {
|
|
597
|
+
if (typeof block?.text === 'string') prompt.push({ content: block.text, role: 'system' })
|
|
598
|
+
}
|
|
599
|
+
for (const msg of params.messages || []) {
|
|
600
|
+
if (msg == null || typeof msg !== 'object') continue
|
|
601
|
+
const message = extractMessagesFromConverseContent(msg.role || 'user', msg.content)
|
|
602
|
+
if (message) prompt.push(message)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const { temperature, topP, maxTokens, stopSequences } = params.inferenceConfig || {}
|
|
606
|
+
return new RequestParams({ prompt, temperature, topP, maxTokens, stopSequences })
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Extract output messages + usage from a non-stream Converse response.
|
|
611
|
+
*
|
|
612
|
+
* @param {{ output?: { message?: { role?: string, content?: Array } }, stopReason?: string, usage?: object }} response
|
|
613
|
+
* @returns {Generation}
|
|
614
|
+
*/
|
|
615
|
+
function extractTextAndResponseReasonConverse (response) {
|
|
616
|
+
const outputMessage = response?.output?.message
|
|
617
|
+
const role = outputMessage?.role || 'assistant'
|
|
618
|
+
|
|
619
|
+
return new Generation({
|
|
620
|
+
role,
|
|
621
|
+
finishReason: response?.stopReason || '',
|
|
622
|
+
...buildUsage(response?.usage),
|
|
623
|
+
messages: toOutputMessages(role, outputMessage?.content),
|
|
624
|
+
})
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Aggregate Converse stream events into a single output message + usage.
|
|
629
|
+
* One messageStart / messageStop pair per response, so one message out.
|
|
630
|
+
*
|
|
631
|
+
* Stream events describe the same content-block structure as the non-stream
|
|
632
|
+
* response, spread across start/delta chunks. We reassemble those chunks
|
|
633
|
+
* into a normalized content-block array and reuse the non-stream extractor.
|
|
634
|
+
*
|
|
635
|
+
* @param {Array<object>} chunks - Ordered ConverseStreamOutput events.
|
|
636
|
+
* @returns {Generation}
|
|
637
|
+
*/
|
|
638
|
+
function extractTextAndResponseReasonConverseFromStream (chunks) {
|
|
639
|
+
let role = 'assistant'
|
|
640
|
+
let stopReason = ''
|
|
641
|
+
let usage = {}
|
|
642
|
+
const blocksByIdx = new Map()
|
|
643
|
+
|
|
644
|
+
for (const chunk of chunks || []) {
|
|
645
|
+
if (chunk.messageStart?.role) {
|
|
646
|
+
role = chunk.messageStart.role
|
|
647
|
+
} else if (chunk.messageStop?.stopReason) {
|
|
648
|
+
stopReason = chunk.messageStop.stopReason
|
|
649
|
+
} else if (chunk.metadata?.usage) {
|
|
650
|
+
usage = chunk.metadata.usage
|
|
651
|
+
} else if (chunk.contentBlockStart?.start?.toolUse) {
|
|
652
|
+
const { contentBlockIndex, start: { toolUse } } = chunk.contentBlockStart
|
|
653
|
+
blocksByIdx.set(contentBlockIndex, {
|
|
654
|
+
toolUse: { toolUseId: toolUse.toolUseId, name: toolUse.name, inputStr: '' },
|
|
655
|
+
})
|
|
656
|
+
} else if (chunk.contentBlockDelta) {
|
|
657
|
+
const { contentBlockIndex, delta } = chunk.contentBlockDelta
|
|
658
|
+
if (typeof delta?.text === 'string') {
|
|
659
|
+
const block = blocksByIdx.get(contentBlockIndex) ?? {}
|
|
660
|
+
block.text = (block.text ?? '') + delta.text
|
|
661
|
+
blocksByIdx.set(contentBlockIndex, block)
|
|
662
|
+
} else if (typeof delta?.toolUse?.input === 'string') {
|
|
663
|
+
const block = blocksByIdx.get(contentBlockIndex) ?? { toolUse: { inputStr: '' } }
|
|
664
|
+
block.toolUse ??= { inputStr: '' }
|
|
665
|
+
block.toolUse.inputStr += delta.toolUse.input
|
|
666
|
+
blocksByIdx.set(contentBlockIndex, block)
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const contentBlocks = [...blocksByIdx.keys()].sort((a, b) => a - b).map(i => {
|
|
672
|
+
const block = blocksByIdx.get(i)
|
|
673
|
+
if (block.toolUse) {
|
|
674
|
+
const { toolUseId, name, inputStr } = block.toolUse
|
|
675
|
+
block.toolUse = { toolUseId, name, input: parseToolInput(inputStr) }
|
|
676
|
+
}
|
|
677
|
+
return block
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
return new Generation({
|
|
681
|
+
role,
|
|
682
|
+
finishReason: stopReason,
|
|
683
|
+
...buildUsage(usage),
|
|
684
|
+
messages: toOutputMessages(role, contentBlocks),
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
479
688
|
module.exports = {
|
|
480
689
|
Generation,
|
|
481
690
|
RequestParams,
|
|
@@ -483,5 +692,10 @@ module.exports = {
|
|
|
483
692
|
parseModelId,
|
|
484
693
|
extractRequestParams,
|
|
485
694
|
extractTextAndResponseReason,
|
|
695
|
+
extractMessagesFromConverseContent,
|
|
696
|
+
extractConverseToolDefinitions,
|
|
697
|
+
extractRequestParamsConverse,
|
|
698
|
+
extractTextAndResponseReasonConverse,
|
|
699
|
+
extractTextAndResponseReasonConverseFromStream,
|
|
486
700
|
PROVIDER,
|
|
487
701
|
}
|
|
@@ -78,6 +78,21 @@ class Identifier {
|
|
|
78
78
|
return this.toString()
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Returns the full hex trace ID. When this is a 64-bit identifier and `traceIdHigh`
|
|
83
|
+
* is provided, prepends it to form the 128-bit trace ID. Otherwise returns
|
|
84
|
+
* only this identifier's hex representation.
|
|
85
|
+
*
|
|
86
|
+
* @param {string | undefined} traceIdHigh - 16-char hex of the upper 64 bits, or undefined
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
toTraceIdHex (traceIdHigh) {
|
|
90
|
+
if (traceIdHigh && this.#buffer.length <= 8) {
|
|
91
|
+
return traceIdHigh + this.toString(16)
|
|
92
|
+
}
|
|
93
|
+
return this.toString(16)
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
/**
|
|
82
97
|
* @param {Identifier} other
|
|
83
98
|
* @returns {boolean}
|
|
@@ -66,11 +66,22 @@ function getOperation (span) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Get the LLM token usage from the span tags
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
69
|
+
* Get the LLM token usage from the span tags.
|
|
70
|
+
*
|
|
71
|
+
* Supports both AI SDK v4 (promptTokens/completionTokens) and v5+
|
|
72
|
+
* (inputTokens/outputTokens), and surfaces prompt-cache metrics for providers
|
|
73
|
+
* that report them. The AI SDK convention is that `inputTokens` already
|
|
74
|
+
* includes cached tokens, so cache reads are reported as a subset of input
|
|
75
|
+
* tokens rather than added on top.
|
|
76
|
+
*
|
|
77
|
+
* @param {SpanTags} tags
|
|
78
|
+
* @returns {{
|
|
79
|
+
* inputTokens?: number,
|
|
80
|
+
* outputTokens?: number,
|
|
81
|
+
* totalTokens?: number,
|
|
82
|
+
* cacheReadTokens?: number,
|
|
83
|
+
* cacheWriteTokens?: number
|
|
84
|
+
* }}
|
|
74
85
|
*/
|
|
75
86
|
function getUsage (tags) {
|
|
76
87
|
const usage = {}
|
|
@@ -87,9 +98,84 @@ function getUsage (tags) {
|
|
|
87
98
|
const totalTokens = tags['ai.usage.totalTokens'] ?? (inputTokens + outputTokens)
|
|
88
99
|
if (!Number.isNaN(totalTokens)) usage.totalTokens = totalTokens
|
|
89
100
|
|
|
101
|
+
// Prompt-cache metrics. AI SDK v6 standardizes cache READ tokens via
|
|
102
|
+
// `ai.usage.cachedInputTokens`; cache WRITE tokens (and earlier AI SDK
|
|
103
|
+
// versions / providers that don't fill `cachedInputTokens`) are only
|
|
104
|
+
// available through provider-specific `ai.response.providerMetadata`.
|
|
105
|
+
// Skip zero values: the AI SDK sets `cachedInputTokens=0` on every span
|
|
106
|
+
// regardless of provider, so emitting it would add noise to spans that
|
|
107
|
+
// don't actually use prompt caching (e.g. OpenAI).
|
|
108
|
+
const providerCache = getProviderCacheTokens(tags['ai.response.providerMetadata'])
|
|
109
|
+
|
|
110
|
+
const cacheReadTokens = tags['ai.usage.cachedInputTokens'] ?? providerCache.cacheReadTokens
|
|
111
|
+
if (cacheReadTokens) usage.cacheReadTokens = cacheReadTokens
|
|
112
|
+
|
|
113
|
+
if (providerCache.cacheWriteTokens) usage.cacheWriteTokens = providerCache.cacheWriteTokens
|
|
114
|
+
|
|
115
|
+
// Normalize `inputTokens` to the sum convention used by `bedrockruntime.js`.
|
|
116
|
+
// Some SDK combinations (e.g. `ai@5` + `@ai-sdk/amazon-bedrock@3`) pass the
|
|
117
|
+
// raw fresh count through, which makes `nonCached = input - cacheRead -
|
|
118
|
+
// cacheWrite` go negative downstream.
|
|
119
|
+
//
|
|
120
|
+
// Detection: if `inputTokens < cacheSum`, the value cannot already be a sum
|
|
121
|
+
// that includes them (non-negative arithmetic). This is provider/version
|
|
122
|
+
// agnostic and won't double-count on stacks where the SDK already
|
|
123
|
+
// normalized (`ai@6` + `bedrock@4` / `anthropic@3`, OpenAI, Google).
|
|
124
|
+
if (usage.inputTokens != null) {
|
|
125
|
+
const cacheSum = (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0)
|
|
126
|
+
if (usage.inputTokens < cacheSum) {
|
|
127
|
+
usage.inputTokens += cacheSum
|
|
128
|
+
if (usage.totalTokens != null) {
|
|
129
|
+
usage.totalTokens = usage.inputTokens + (usage.outputTokens || 0)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
90
134
|
return usage
|
|
91
135
|
}
|
|
92
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Extract prompt-cache token counts from the stringified
|
|
139
|
+
* `ai.response.providerMetadata` attribute.
|
|
140
|
+
*
|
|
141
|
+
* The AI SDK does not standardize cache WRITE tokens on the usage object, and
|
|
142
|
+
* earlier versions / providers may also omit `ai.usage.cachedInputTokens`, so
|
|
143
|
+
* we read the provider-specific shape directly. Only Bedrock and Anthropic
|
|
144
|
+
* are handled here as they are the providers that report cache writes today.
|
|
145
|
+
*
|
|
146
|
+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points
|
|
147
|
+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
|
|
148
|
+
*
|
|
149
|
+
* @param {string | undefined} providerMetadataJson
|
|
150
|
+
* @returns {{ cacheReadTokens?: number, cacheWriteTokens?: number }}
|
|
151
|
+
*/
|
|
152
|
+
function getProviderCacheTokens (providerMetadataJson) {
|
|
153
|
+
if (!providerMetadataJson) return {}
|
|
154
|
+
|
|
155
|
+
const metadata = getJsonStringValue(providerMetadataJson, null)
|
|
156
|
+
if (!metadata || typeof metadata !== 'object') return {}
|
|
157
|
+
|
|
158
|
+
const result = {}
|
|
159
|
+
|
|
160
|
+
const bedrockUsage = metadata.bedrock?.usage
|
|
161
|
+
if (bedrockUsage) {
|
|
162
|
+
if (bedrockUsage.cacheReadInputTokens != null) result.cacheReadTokens = bedrockUsage.cacheReadInputTokens
|
|
163
|
+
if (bedrockUsage.cacheWriteInputTokens != null) result.cacheWriteTokens = bedrockUsage.cacheWriteInputTokens
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const anthropic = metadata.anthropic
|
|
167
|
+
if (anthropic) {
|
|
168
|
+
if (result.cacheReadTokens == null && anthropic.cacheReadInputTokens != null) {
|
|
169
|
+
result.cacheReadTokens = anthropic.cacheReadInputTokens
|
|
170
|
+
}
|
|
171
|
+
if (result.cacheWriteTokens == null && anthropic.cacheCreationInputTokens != null) {
|
|
172
|
+
result.cacheWriteTokens = anthropic.cacheCreationInputTokens
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
93
179
|
/**
|
|
94
180
|
* Safely JSON parses a string value with a default fallback
|
|
95
181
|
* @template T typeof defaultValue
|
|
@@ -7,12 +7,22 @@ const {
|
|
|
7
7
|
extractTextAndResponseReason,
|
|
8
8
|
parseModelId,
|
|
9
9
|
extractTextAndResponseReasonFromStream,
|
|
10
|
+
extractConverseToolDefinitions,
|
|
11
|
+
extractRequestParamsConverse,
|
|
12
|
+
extractTextAndResponseReasonConverse,
|
|
13
|
+
extractTextAndResponseReasonConverseFromStream,
|
|
10
14
|
} = require('../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
|
|
11
15
|
const BaseLLMObsPlugin = require('./base')
|
|
12
16
|
|
|
13
17
|
const llmobsStore = storage('llmobs')
|
|
14
18
|
|
|
15
|
-
const ENABLED_OPERATIONS = new Set([
|
|
19
|
+
const ENABLED_OPERATIONS = new Set([
|
|
20
|
+
'invokeModel',
|
|
21
|
+
'invokeModelWithResponseStream',
|
|
22
|
+
'converse',
|
|
23
|
+
'converseStream',
|
|
24
|
+
])
|
|
25
|
+
const CONVERSE_OPERATIONS = new Set(['converse', 'converseStream'])
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
28
|
* @typedef {{
|
|
@@ -79,10 +89,18 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
79
89
|
setLLMObsTags ({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders }) {
|
|
80
90
|
const isStream = request?.operation?.toLowerCase().includes('stream')
|
|
81
91
|
telemetry.incrementLLMObsSpanStartCount({ autoinstrumented: true, integration: 'bedrock' })
|
|
92
|
+
this.#registerSpan(span, request)
|
|
82
93
|
|
|
94
|
+
if (CONVERSE_OPERATIONS.has(request?.operation)) {
|
|
95
|
+
this.#tagConverseSpan({ ctx, request, span, response, tokensFromHeaders, isStream })
|
|
96
|
+
} else {
|
|
97
|
+
this.#tagInvokeModelSpan({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders, isStream })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#registerSpan (span, request) {
|
|
83
102
|
const parent = llmobsStore.getStore()?.span
|
|
84
103
|
// Use full modelId and unified provider for LLMObs (required for backend cost estimation).
|
|
85
|
-
// Split modelProvider/modelName from parseModelId() are still used below for response parsing.
|
|
86
104
|
this._tagger.registerLLMObsSpan(span, {
|
|
87
105
|
parent,
|
|
88
106
|
modelName: request.params.modelId.toLowerCase(),
|
|
@@ -91,38 +109,42 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
91
109
|
name: 'bedrock-runtime.command',
|
|
92
110
|
integration: 'bedrock',
|
|
93
111
|
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#tagConverseSpan ({ ctx, request, span, response, tokensFromHeaders, isStream }) {
|
|
115
|
+
const requestParams = extractRequestParamsConverse(request.params)
|
|
116
|
+
const textAndResponseReason = isStream
|
|
117
|
+
? extractTextAndResponseReasonConverseFromStream(ctx.chunks)
|
|
118
|
+
: extractTextAndResponseReasonConverse(response)
|
|
119
|
+
|
|
120
|
+
const toolDefinitions = extractConverseToolDefinitions(request.params)
|
|
121
|
+
if (toolDefinitions.length > 0) this._tagger.tagToolDefinitions(span, toolDefinitions)
|
|
122
|
+
if (textAndResponseReason.finishReason) {
|
|
123
|
+
this._tagger.tagMetadata(span, { stop_reason: textAndResponseReason.finishReason })
|
|
124
|
+
}
|
|
125
|
+
this.#tagCommon({ span, requestParams, textAndResponseReason, tokensFromHeaders })
|
|
126
|
+
}
|
|
94
127
|
|
|
128
|
+
#tagInvokeModelSpan ({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders, isStream }) {
|
|
95
129
|
const requestParams = extractRequestParams(request.params, modelProvider)
|
|
96
130
|
// for streamed responses, we'll use the coerced response object we formed in the stream handler
|
|
97
131
|
const textAndResponseReason = isStream
|
|
98
132
|
? extractTextAndResponseReasonFromStream(ctx.chunks, modelProvider, modelName)
|
|
99
133
|
: extractTextAndResponseReason(response, modelProvider, modelName)
|
|
100
134
|
|
|
101
|
-
|
|
135
|
+
this.#tagCommon({ span, requestParams, textAndResponseReason, tokensFromHeaders })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#tagCommon ({ span, requestParams, textAndResponseReason, tokensFromHeaders }) {
|
|
102
139
|
this._tagger.tagMetadata(span, {
|
|
103
140
|
temperature: Number.parseFloat(requestParams.temperature) || 0,
|
|
104
141
|
max_tokens: Number.parseInt(requestParams.maxTokens) || 0,
|
|
105
142
|
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this._tagger.tagLLMIO(
|
|
109
|
-
span,
|
|
110
|
-
requestParams.prompt,
|
|
111
|
-
[{ content: textAndResponseReason.message, role: textAndResponseReason.role }]
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
// add token metrics
|
|
115
|
-
const { inputTokens, outputTokens, totalTokens, cacheReadTokens, cacheWriteTokens } = extractTokens({
|
|
143
|
+
this._tagger.tagLLMIO(span, requestParams.prompt, textAndResponseReason.messages)
|
|
144
|
+
this._tagger.tagMetrics(span, extractTokens({
|
|
116
145
|
tokensFromHeaders,
|
|
117
146
|
usage: textAndResponseReason.usage,
|
|
118
|
-
})
|
|
119
|
-
this._tagger.tagMetrics(span, {
|
|
120
|
-
inputTokens,
|
|
121
|
-
outputTokens,
|
|
122
|
-
totalTokens,
|
|
123
|
-
cacheReadTokens,
|
|
124
|
-
cacheWriteTokens,
|
|
125
|
-
})
|
|
147
|
+
}))
|
|
126
148
|
}
|
|
127
149
|
}
|
|
128
150
|
|
|
@@ -16,6 +16,11 @@ const SPAN_KIND_CONSUMER = protoSpanKind.values.SPAN_KIND_CONSUMER
|
|
|
16
16
|
// Cached zero Identifier used to detect zero IDs without re-allocating per span.
|
|
17
17
|
const ZERO_ID = id('0')
|
|
18
18
|
|
|
19
|
+
// DD propagation tag carrying the upper 64 bits of a 128-bit trace ID as 16 hex chars.
|
|
20
|
+
// span_format.js#extractChunkTags only copies this onto the first-in-chunk span, so the
|
|
21
|
+
// transformer scans the batch to find it and applies it to every span's traceId.
|
|
22
|
+
const TRACE_ID_128 = '_dd.p.tid'
|
|
23
|
+
|
|
19
24
|
/**
|
|
20
25
|
* @typedef {import('../../id').Identifier} Identifier
|
|
21
26
|
*
|
|
@@ -65,6 +70,7 @@ const STATUS_CODE_ERROR = 2
|
|
|
65
70
|
const EXCLUDED_META_KEYS = new Set([
|
|
66
71
|
'_dd.span_links',
|
|
67
72
|
'span.kind',
|
|
73
|
+
TRACE_ID_128,
|
|
68
74
|
])
|
|
69
75
|
|
|
70
76
|
/**
|
|
@@ -113,6 +119,18 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
113
119
|
* @returns {object[]} Array of scope span objects
|
|
114
120
|
*/
|
|
115
121
|
#transformScopeSpans (spans) {
|
|
122
|
+
let traceKey
|
|
123
|
+
let traceIdHigh
|
|
124
|
+
const otlpSpans = spans.map((span) => {
|
|
125
|
+
// `_dd.p.tid` lives only on the first-in-chunk span of each trace.
|
|
126
|
+
// Reset at each trace boundary for batching of multiple traces.
|
|
127
|
+
const key = span.trace_id.toString(16)
|
|
128
|
+
if (key !== traceKey) {
|
|
129
|
+
traceKey = key
|
|
130
|
+
traceIdHigh = span.meta?.[TRACE_ID_128]?.toLowerCase()
|
|
131
|
+
}
|
|
132
|
+
return this.#transformSpan(span, traceIdHigh)
|
|
133
|
+
})
|
|
116
134
|
return [{
|
|
117
135
|
scope: {
|
|
118
136
|
name: 'dd-trace-js',
|
|
@@ -121,7 +139,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
121
139
|
droppedAttributesCount: 0,
|
|
122
140
|
},
|
|
123
141
|
schemaUrl: '',
|
|
124
|
-
spans:
|
|
142
|
+
spans: otlpSpans,
|
|
125
143
|
}]
|
|
126
144
|
}
|
|
127
145
|
|
|
@@ -129,14 +147,15 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
|
|
|
129
147
|
* Transforms a single DD-formatted span to an OTLP Span object.
|
|
130
148
|
*
|
|
131
149
|
* @param {DDFormattedSpan} span - DD-formatted span to transform
|
|
150
|
+
* @param {string | undefined} traceIdHigh - 16-char hex of the upper 64 bits of the trace ID
|
|
132
151
|
* @returns {object} OTLP Span object
|
|
133
152
|
*/
|
|
134
|
-
#transformSpan (span) {
|
|
153
|
+
#transformSpan (span, traceIdHigh) {
|
|
135
154
|
const parentId = span.parent_id
|
|
136
155
|
const links = this.#extractLinks(span.meta?.['_dd.span_links'])
|
|
137
156
|
|
|
138
157
|
return {
|
|
139
|
-
traceId:
|
|
158
|
+
traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
|
|
140
159
|
spanId: this.#idToBytes(span.span_id, 8),
|
|
141
160
|
parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
|
|
142
161
|
name: span.resource,
|
|
@@ -284,7 +284,7 @@ class TextMapPropagator {
|
|
|
284
284
|
(DD_MAJOR < 6 && this._hasPropagationStyle('inject', 'b3'))
|
|
285
285
|
if (!hasB3multi) return
|
|
286
286
|
|
|
287
|
-
carrier[b3TraceKey] =
|
|
287
|
+
carrier[b3TraceKey] = spanContext._traceId.toTraceIdHex(spanContext._trace.tags['_dd.p.tid'])
|
|
288
288
|
carrier[b3SpanKey] = spanContext._spanId.toString(16)
|
|
289
289
|
carrier[b3SampledKey] = spanContext._sampling.priority >= AUTO_KEEP ? '1' : '0'
|
|
290
290
|
|
|
@@ -303,7 +303,7 @@ class TextMapPropagator {
|
|
|
303
303
|
(DD_MAJOR >= 6 && this._hasPropagationStyle('inject', 'b3'))
|
|
304
304
|
if (!hasB3SingleHeader) return
|
|
305
305
|
|
|
306
|
-
const traceId =
|
|
306
|
+
const traceId = spanContext._traceId.toTraceIdHex(spanContext._trace.tags['_dd.p.tid'])
|
|
307
307
|
const spanId = spanContext._spanId.toString(16)
|
|
308
308
|
const sampled = spanContext._sampling.priority >= AUTO_KEEP ? '1' : '0'
|
|
309
309
|
|
|
@@ -859,14 +859,6 @@ class TextMapPropagator {
|
|
|
859
859
|
}
|
|
860
860
|
}
|
|
861
861
|
|
|
862
|
-
_getB3TraceId (spanContext) {
|
|
863
|
-
if (spanContext._traceId.toBuffer().length <= 8 && spanContext._trace.tags['_dd.p.tid']) {
|
|
864
|
-
return spanContext._trace.tags['_dd.p.tid'] + spanContext._traceId.toString(16)
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return spanContext._traceId.toString(16)
|
|
868
|
-
}
|
|
869
|
-
|
|
870
862
|
/**
|
|
871
863
|
* @param {number} traceparentSampled
|
|
872
864
|
* @param {number|undefined} tracestateSamplingPriority
|
|
@@ -46,9 +46,7 @@ class DatadogSpanContext {
|
|
|
46
46
|
|
|
47
47
|
toTraceId (get128bitId = false) {
|
|
48
48
|
if (get128bitId) {
|
|
49
|
-
return this._traceId.
|
|
50
|
-
? this._trace.tags[TRACE_ID_128] + this._traceId.toString(16).padStart(16, '0')
|
|
51
|
-
: this._traceId.toString(16).padStart(32, '0')
|
|
49
|
+
return this._traceId.toTraceIdHex(this._trace.tags[TRACE_ID_128]).padStart(32, '0')
|
|
52
50
|
}
|
|
53
51
|
return this._traceId.toString(10)
|
|
54
52
|
}
|