dd-trace 6.18.0 → 6.20.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 +166 -24
- package/package.json +7 -7
- package/packages/datadog-esbuild/index.js +9 -6
- package/packages/datadog-instrumentations/src/ai.js +26 -0
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/aws-sdk.js +2 -2
- 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/hooks.js +0 -7
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +2 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +25 -0
- 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-openai/src/stream-helpers.js +50 -1
- 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 +8 -4
- package/packages/dd-trace/src/config/remote_config.js +12 -0
- package/packages/dd-trace/src/config/supported-configurations.json +26 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/debugger/constants.js +13 -4
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +109 -6
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +65 -6
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +22 -10
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +12 -8
- package/packages/dd-trace/src/debugger/devtools_client/send.js +14 -0
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +43 -5
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +3 -8
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +2 -122
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +0 -2
- package/packages/dd-trace/src/debugger/index.js +60 -28
- package/packages/dd-trace/src/debugger/inspect-segment.js +92 -23
- package/packages/dd-trace/src/debugger/pause-duration-histogram.js +74 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +192 -52
- package/packages/dd-trace/src/debugger/redaction.js +139 -0
- package/packages/dd-trace/src/knuth-hash.js +17 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +33 -8
- package/packages/dd-trace/src/llmobs/experiments/client.js +66 -2
- package/packages/dd-trace/src/llmobs/experiments/evaluator.js +138 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +213 -90
- package/packages/dd-trace/src/llmobs/experiments/index.js +40 -1
- package/packages/dd-trace/src/llmobs/experiments/noop.js +53 -2
- package/packages/dd-trace/src/llmobs/experiments/remote-evaluator.js +110 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +34 -6
- 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 +15 -2
- 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/sdk.js +19 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +52 -91
- package/packages/dd-trace/src/llmobs/tagger.js +37 -36
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -6
- package/packages/dd-trace/src/openfeature/constants/constants.js +10 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -1
- package/packages/dd-trace/src/openfeature/writers/base.js +39 -18
- package/packages/dd-trace/src/openfeature/writers/flag-eval-evp-hook.js +105 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-aggregation.js +190 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-consumer.js +233 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-context.js +262 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-payload.js +181 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-pii.js +108 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-telemetry.js +154 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluation-worker.js +77 -0
- package/packages/dd-trace/src/openfeature/writers/flag-evaluations.js +398 -0
- package/packages/dd-trace/src/openfeature/writers/util.js +5 -4
- package/packages/dd-trace/src/opentelemetry/context_manager.js +3 -0
- package/packages/dd-trace/src/opentelemetry/span_context.js +6 -2
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +6 -0
- package/packages/dd-trace/src/opentelemetry/tracer.js +16 -18
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +116 -25
- package/packages/dd-trace/src/opentracing/propagation/tracestate.js +80 -16
- package/packages/dd-trace/src/opentracing/span.js +14 -9
- package/packages/dd-trace/src/opentracing/tracer.js +10 -3
- package/packages/dd-trace/src/otel-sampling.js +171 -0
- package/packages/dd-trace/src/plugins/index.js +0 -1
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/priority_sampler.js +155 -33
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/remote_config/capabilities.js +9 -0
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/sampler.js +2 -5
- package/packages/dd-trace/src/sampling_rule.js +4 -8
- package/packages/dd-trace/src/span_format.js +6 -0
- package/packages/dd-trace/src/span_processor.js +32 -3
- package/packages/dd-trace/src/standalone/index.js +5 -4
- package/packages/dd-trace/src/standalone/tracesource_priority_sampler.js +25 -5
- package/packages/dd-trace/src/telemetry/metrics.js +4 -3
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
- package/packages/datadog-instrumentations/src/postgres.js +0 -7
- package/packages/datadog-instrumentations/src/supabase.js +0 -15
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { storage } = require('../../../../datadog-core')
|
|
4
|
+
const log = require('../../log')
|
|
4
5
|
const telemetry = require('../telemetry')
|
|
6
|
+
const { safeJsonParse } = require('../util')
|
|
5
7
|
const {
|
|
8
|
+
buildUsage,
|
|
6
9
|
extractRequestParams,
|
|
7
10
|
extractTextAndResponseReason,
|
|
11
|
+
mergeStreamedUsage,
|
|
8
12
|
parseModelId,
|
|
9
13
|
extractTextAndResponseReasonFromStream,
|
|
10
14
|
extractConverseToolDefinitions,
|
|
@@ -52,12 +56,45 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
52
56
|
// avoids instrumenting other non supported runtime operations
|
|
53
57
|
if (!ENABLED_OPERATIONS.has(operation)) return
|
|
54
58
|
|
|
55
|
-
|
|
59
|
+
// the SDK rejects a request with no model id, and the parser assumes a string
|
|
60
|
+
const modelId = request.params?.modelId
|
|
61
|
+
if (typeof modelId !== 'string') return
|
|
62
|
+
|
|
63
|
+
const { modelProvider, modelName } = parseModelId(modelId)
|
|
56
64
|
|
|
57
65
|
// avoids instrumenting non llm type
|
|
58
66
|
if (modelName.includes('embed')) return
|
|
59
67
|
|
|
60
68
|
const span = ctx.currentStore?.span
|
|
69
|
+
if (!span) return
|
|
70
|
+
|
|
71
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
72
|
+
// no LLMObs payload to build, so the usage comes from the response headers and, where
|
|
73
|
+
// those are absent, from whatever reported it
|
|
74
|
+
let usage
|
|
75
|
+
if (CONVERSE_OPERATIONS.has(operation)) {
|
|
76
|
+
// a non-streamed Converse puts it on the response, a streamed one on a metadata event
|
|
77
|
+
usage = buildUsage(response.usage) ?? ctx.streamedUsage
|
|
78
|
+
} else if (operation.toLowerCase().includes('stream')) {
|
|
79
|
+
// every streamed frame was folded into the running totals as it arrived
|
|
80
|
+
usage = ctx.streamedUsage
|
|
81
|
+
} else {
|
|
82
|
+
// headers can report some counts and the body others, so both are read and
|
|
83
|
+
// `extractTokens` merges them field by field
|
|
84
|
+
usage = responseBodyUsage(response, modelProvider, modelName)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this._setGenAiApmTags(span, {
|
|
88
|
+
spanKind: 'llm',
|
|
89
|
+
modelName: modelId.toLowerCase(),
|
|
90
|
+
modelProvider: 'amazon_bedrock',
|
|
91
|
+
// a count no source reported comes back undefined and is left off the span: reporting
|
|
92
|
+
// zeros for every metric would be worse than reporting none
|
|
93
|
+
metrics: extractTokens({ tokensFromHeaders, usage: usage ?? {} }),
|
|
94
|
+
})
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
61
98
|
this.setLLMObsTags({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders })
|
|
62
99
|
})
|
|
63
100
|
|
|
@@ -71,6 +108,10 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
71
108
|
const cacheReadTokenCount = headers['x-amzn-bedrock-cache-read-input-token-count']
|
|
72
109
|
const cacheWriteTokenCount = headers['x-amzn-bedrock-cache-write-input-token-count']
|
|
73
110
|
|
|
111
|
+
// Responses that report no counts at all, error responses included, would otherwise cache a
|
|
112
|
+
// record of undefined fields that reads as a measurement of zero.
|
|
113
|
+
if (!inputTokenCount && !outputTokenCount && !cacheReadTokenCount && !cacheWriteTokenCount) return
|
|
114
|
+
|
|
74
115
|
pendingTokenHeaders.set(requestId, {
|
|
75
116
|
inputTokensFromHeaders: inputTokenCount && Number.parseInt(inputTokenCount, 10),
|
|
76
117
|
outputTokensFromHeaders: outputTokenCount && Number.parseInt(outputTokenCount, 10),
|
|
@@ -80,6 +121,13 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
80
121
|
})
|
|
81
122
|
|
|
82
123
|
this.addSub('apm:aws:response:streamed-chunk:bedrockruntime', ({ ctx, chunk }) => {
|
|
124
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
125
|
+
// only the token counts are needed, for the `gen_ai.usage.*` metrics; the generated
|
|
126
|
+
// content is left to the LLMObs path, so nothing is retained past the running totals
|
|
127
|
+
ctx.streamedUsage = mergeChunkUsage(ctx, chunk)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
83
131
|
if (!ctx.chunks) ctx.chunks = []
|
|
84
132
|
|
|
85
133
|
if (chunk) ctx.chunks.push(chunk)
|
|
@@ -141,10 +189,10 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
141
189
|
max_tokens: Number.parseInt(requestParams.maxTokens, 10) || 0,
|
|
142
190
|
})
|
|
143
191
|
this._tagger.tagLLMIO(span, requestParams.prompt, textAndResponseReason.messages)
|
|
144
|
-
this._tagger.tagMetrics(span, extractTokens({
|
|
192
|
+
this._tagger.tagMetrics(span, zeroFilled(extractTokens({
|
|
145
193
|
tokensFromHeaders,
|
|
146
194
|
usage: textAndResponseReason.usage,
|
|
147
|
-
}))
|
|
195
|
+
})))
|
|
148
196
|
}
|
|
149
197
|
}
|
|
150
198
|
|
|
@@ -159,9 +207,74 @@ function consumeTokenHeaders (requestId) {
|
|
|
159
207
|
}
|
|
160
208
|
|
|
161
209
|
/**
|
|
162
|
-
*
|
|
210
|
+
* Fold one streamed frame's token counts into the totals on `ctx`. Converse reports them on a
|
|
211
|
+
* metadata event; `invokeModel` reports them in the frame body, in a shape that varies by
|
|
212
|
+
* provider, so the body is read through the same table the LLMObs path uses.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} ctx
|
|
215
|
+
* @param {object} [chunk]
|
|
216
|
+
* @returns {import('../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
|
|
217
|
+
* .StreamedUsage | undefined}
|
|
218
|
+
*/
|
|
219
|
+
function mergeChunkUsage (ctx, chunk) {
|
|
220
|
+
const metadataUsage = chunk?.metadata?.usage
|
|
221
|
+
if (metadataUsage) return buildUsage(metadataUsage) ?? ctx.streamedUsage
|
|
222
|
+
|
|
223
|
+
const bytes = chunk?.chunk?.bytes
|
|
224
|
+
if (!ArrayBuffer.isView(bytes)) return ctx.streamedUsage
|
|
225
|
+
|
|
226
|
+
// a view, not a copy: this runs on every frame of every streamed response
|
|
227
|
+
const text = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('utf8')
|
|
228
|
+
const body = safeJsonParse(text, null)
|
|
229
|
+
// a frame the model filled with generated text rather than JSON must not reach the application
|
|
230
|
+
if (typeof body !== 'object' || body === null) return ctx.streamedUsage
|
|
231
|
+
|
|
232
|
+
return mergeStreamedUsage(ctx.streamedUsage, body, streamModelProvider(ctx))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Token usage a non-streamed `invokeModel` reports in its own response body, which several
|
|
237
|
+
* providers carry and the headers do not always correlate. Read through the same extractor the
|
|
238
|
+
* LLMObs path uses, which parses this body on every request anyway.
|
|
239
|
+
*
|
|
240
|
+
* @param {{ body?: Uint8Array }} response
|
|
241
|
+
* @param {string} modelProvider
|
|
242
|
+
* @param {string} modelName
|
|
243
|
+
* @returns {Record<string, number | undefined> | undefined}
|
|
244
|
+
*/
|
|
245
|
+
function responseBodyUsage (response, modelProvider, modelName) {
|
|
246
|
+
if (!response?.body) return
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
return extractTextAndResponseReason(response, modelProvider, modelName).usage
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// the extractor parses the body itself; a malformed one must not disable the plugin
|
|
252
|
+
log.debug('Failed to read Bedrock response usage: %s', e.message)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The provider is fixed for the life of the stream, so it is parsed off the request once.
|
|
258
|
+
*
|
|
259
|
+
* @param {object} ctx
|
|
260
|
+
*/
|
|
261
|
+
function streamModelProvider (ctx) {
|
|
262
|
+
if (ctx.streamModelProvider === undefined) {
|
|
263
|
+
const modelId = (ctx.request ?? ctx.response?.request)?.params?.modelId
|
|
264
|
+
ctx.streamModelProvider = typeof modelId === 'string'
|
|
265
|
+
? parseModelId(modelId).modelProvider.toUpperCase()
|
|
266
|
+
: ''
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return ctx.streamModelProvider
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Combine response-body usage with header-derived counts, preferring the body. A count no source
|
|
274
|
+
* reported stays undefined rather than becoming a zero that reads as a measurement.
|
|
163
275
|
*
|
|
164
276
|
* @param {{ tokensFromHeaders: HeaderTokens | undefined, usage: Record<string, number | undefined> }} options
|
|
277
|
+
* @returns {Record<string, number | undefined>}
|
|
165
278
|
*/
|
|
166
279
|
function extractTokens ({ tokensFromHeaders, usage }) {
|
|
167
280
|
const {
|
|
@@ -171,21 +284,60 @@ function extractTokens ({ tokensFromHeaders, usage }) {
|
|
|
171
284
|
cacheWriteTokensFromHeaders,
|
|
172
285
|
} = tokensFromHeaders ?? {}
|
|
173
286
|
|
|
174
|
-
const inputTokens = usage.inputTokens
|
|
175
|
-
const outputTokens = usage.outputTokens
|
|
176
|
-
const cacheReadTokens = usage.cacheReadTokens
|
|
177
|
-
const cacheWriteTokens = usage.cacheWriteTokens
|
|
287
|
+
const inputTokens = resolveCount(usage.inputTokens, inputTokensFromHeaders)
|
|
288
|
+
const outputTokens = resolveCount(usage.outputTokens, outputTokensFromHeaders)
|
|
289
|
+
const cacheReadTokens = resolveCount(usage.cacheReadTokens, cacheReadTokensFromHeaders)
|
|
290
|
+
const cacheWriteTokens = resolveCount(usage.cacheWriteTokens, cacheWriteTokensFromHeaders)
|
|
178
291
|
|
|
179
292
|
// adjust for the fact that bedrock input tokens only count non-cached tokens
|
|
180
|
-
const normalizedInputTokens = inputTokens
|
|
293
|
+
const normalizedInputTokens = inputTokens === undefined &&
|
|
294
|
+
cacheReadTokens === undefined &&
|
|
295
|
+
cacheWriteTokens === undefined
|
|
296
|
+
? undefined
|
|
297
|
+
: (inputTokens ?? 0) + (cacheReadTokens ?? 0) + (cacheWriteTokens ?? 0)
|
|
298
|
+
|
|
299
|
+
const totalTokens = normalizedInputTokens === undefined && outputTokens === undefined
|
|
300
|
+
? undefined
|
|
301
|
+
: (normalizedInputTokens ?? 0) + (outputTokens ?? 0)
|
|
181
302
|
|
|
182
303
|
return {
|
|
183
304
|
inputTokens: normalizedInputTokens,
|
|
184
305
|
outputTokens,
|
|
185
|
-
totalTokens
|
|
306
|
+
totalTokens,
|
|
186
307
|
cacheReadTokens,
|
|
187
308
|
cacheWriteTokens,
|
|
188
309
|
}
|
|
189
310
|
}
|
|
190
311
|
|
|
312
|
+
/**
|
|
313
|
+
* The body wins over the headers whenever it reported a count, a measured zero included, and a
|
|
314
|
+
* value neither reported as a number is left undefined: header counts are parsed from strings
|
|
315
|
+
* and can arrive empty.
|
|
316
|
+
*
|
|
317
|
+
* @param {unknown} fromBody
|
|
318
|
+
* @param {unknown} fromHeaders
|
|
319
|
+
* @returns {number | undefined}
|
|
320
|
+
*/
|
|
321
|
+
function resolveCount (fromBody, fromHeaders) {
|
|
322
|
+
const value = typeof fromBody === 'number' ? fromBody : fromHeaders
|
|
323
|
+
return typeof value === 'number' && !Number.isNaN(value) ? value : undefined
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The LLMObs metrics contract reports an unmeasured count as zero, where the `gen_ai.*` APM
|
|
328
|
+
* attributes leave it off the span entirely.
|
|
329
|
+
*
|
|
330
|
+
* @param {Record<string, number | undefined>} tokens
|
|
331
|
+
* @returns {Record<string, number>}
|
|
332
|
+
*/
|
|
333
|
+
function zeroFilled (tokens) {
|
|
334
|
+
return {
|
|
335
|
+
inputTokens: tokens.inputTokens ?? 0,
|
|
336
|
+
outputTokens: tokens.outputTokens ?? 0,
|
|
337
|
+
totalTokens: tokens.totalTokens ?? 0,
|
|
338
|
+
cacheReadTokens: tokens.cacheReadTokens ?? 0,
|
|
339
|
+
cacheWriteTokens: tokens.cacheWriteTokens ?? 0,
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
191
343
|
module.exports = BedrockRuntimeLLMObsPlugin
|
|
@@ -5,6 +5,7 @@ const { storage: llmobsStorage } = require('../../storage')
|
|
|
5
5
|
const { NAME, SESSION_ID } = require('../../constants/tags')
|
|
6
6
|
const { splitModel } = require('../../../../../datadog-plugin-claude-agent-sdk/src/util')
|
|
7
7
|
|
|
8
|
+
const SYSTEM_PROMPT_DYNAMIC_BOUNDARY = '__SYSTEM_PROMPT_DYNAMIC_BOUNDARY__'
|
|
8
9
|
const subagentToolIds = new Set()
|
|
9
10
|
|
|
10
11
|
function normalizeToolOutputString (raw) {
|
|
@@ -39,6 +40,27 @@ function getToolOutputText (raw) {
|
|
|
39
40
|
return JSON.stringify(raw)
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} [usage]
|
|
45
|
+
* @returns {Record<string, number> | undefined}
|
|
46
|
+
*/
|
|
47
|
+
function extractUsageMetrics (usage) {
|
|
48
|
+
if (!usage) return
|
|
49
|
+
|
|
50
|
+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0
|
|
51
|
+
const cacheReadTokens = usage.cache_read_input_tokens ?? 0
|
|
52
|
+
const inputTokens = (usage.input_tokens ?? 0) + cacheWriteTokens + cacheReadTokens
|
|
53
|
+
const outputTokens = usage.output_tokens ?? 0
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
input_tokens: inputTokens,
|
|
57
|
+
output_tokens: outputTokens,
|
|
58
|
+
cache_read_input_tokens: cacheReadTokens,
|
|
59
|
+
cache_write_input_tokens: cacheWriteTokens,
|
|
60
|
+
total_tokens: inputTokens + outputTokens,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
42
64
|
function buildOutputMessages (chunks, llmStartIdx, llmEndIdx) {
|
|
43
65
|
let thinking = ''
|
|
44
66
|
let text = ''
|
|
@@ -85,6 +107,13 @@ class QueryLLMObsPlugin extends LLMObsPlugin {
|
|
|
85
107
|
super.asyncEnd(ctx)
|
|
86
108
|
}
|
|
87
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @override
|
|
112
|
+
*/
|
|
113
|
+
getGenAiApmEndTags (ctx) {
|
|
114
|
+
return { sessionId: ctx.session_id }
|
|
115
|
+
}
|
|
116
|
+
|
|
88
117
|
setLLMObsTags (ctx) {
|
|
89
118
|
const span = ctx.currentStore?.span
|
|
90
119
|
if (!span) return
|
|
@@ -99,6 +128,11 @@ class QueryLLMObsPlugin extends LLMObsPlugin {
|
|
|
99
128
|
|
|
100
129
|
if (cwd) metadata.cwd = cwd
|
|
101
130
|
if (permissionMode) metadata.permissionMode = permissionMode
|
|
131
|
+
const systemPrompt = ctx.arguments?.[0]?.options?.systemPrompt
|
|
132
|
+
if (systemPrompt?.type === 'preset') {
|
|
133
|
+
if (typeof systemPrompt.preset === 'string') metadata.systemPromptPreset = systemPrompt.preset
|
|
134
|
+
if (typeof systemPrompt.append === 'string') metadata.systemPromptAppend = systemPrompt.append
|
|
135
|
+
}
|
|
102
136
|
|
|
103
137
|
this._tagger.tagMetadata(span, metadata)
|
|
104
138
|
}
|
|
@@ -149,6 +183,13 @@ class LlmLlmObsPlugin extends LLMObsPlugin {
|
|
|
149
183
|
return { kind: 'llm', name: ctx.model, modelName, modelProvider, sessionId: ctx.sessionId }
|
|
150
184
|
}
|
|
151
185
|
|
|
186
|
+
/**
|
|
187
|
+
* @override
|
|
188
|
+
*/
|
|
189
|
+
getGenAiApmEndTags (ctx) {
|
|
190
|
+
return { metrics: extractUsageMetrics(ctx.usage) }
|
|
191
|
+
}
|
|
192
|
+
|
|
152
193
|
end (ctx) {
|
|
153
194
|
super.end(ctx)
|
|
154
195
|
super.asyncEnd(ctx)
|
|
@@ -158,31 +199,32 @@ class LlmLlmObsPlugin extends LLMObsPlugin {
|
|
|
158
199
|
const span = ctx.currentStore?.span
|
|
159
200
|
if (!span) return
|
|
160
201
|
|
|
161
|
-
const { chunks, llmStartIdx, llmEndIdx, parentToolUseId, initialPrompt, usage } = ctx
|
|
202
|
+
const { chunks, llmStartIdx, llmEndIdx, parentToolUseId, initialPrompt, systemPrompt, usage } = ctx
|
|
162
203
|
|
|
163
204
|
if (chunks) {
|
|
164
|
-
const inputMessages = this.#buildInputMessages(chunks, llmStartIdx, parentToolUseId, initialPrompt)
|
|
205
|
+
const inputMessages = this.#buildInputMessages(chunks, llmStartIdx, parentToolUseId, initialPrompt, systemPrompt)
|
|
165
206
|
const outputMessages = buildOutputMessages(chunks, llmStartIdx, llmEndIdx)
|
|
166
207
|
this._tagger.tagLLMIO(span, inputMessages, outputMessages)
|
|
167
208
|
}
|
|
168
209
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const cacheReadTokens = usage.cache_read_input_tokens ?? 0
|
|
172
|
-
const inputTokens = (usage.input_tokens ?? 0) + cacheWriteTokens + cacheReadTokens
|
|
173
|
-
const outputTokens = usage.output_tokens ?? 0
|
|
174
|
-
this._tagger.tagMetrics(span, {
|
|
175
|
-
input_tokens: inputTokens,
|
|
176
|
-
output_tokens: outputTokens,
|
|
177
|
-
cache_read_input_tokens: cacheReadTokens,
|
|
178
|
-
cache_write_input_tokens: cacheWriteTokens,
|
|
179
|
-
total_tokens: inputTokens + outputTokens,
|
|
180
|
-
})
|
|
181
|
-
}
|
|
210
|
+
const metrics = extractUsageMetrics(usage)
|
|
211
|
+
if (metrics) this._tagger.tagMetrics(span, metrics)
|
|
182
212
|
}
|
|
183
213
|
|
|
184
|
-
#buildInputMessages (chunks, llmStartIdx, parentToolUseId, initialPrompt) {
|
|
214
|
+
#buildInputMessages (chunks, llmStartIdx, parentToolUseId, initialPrompt, systemPrompt) {
|
|
185
215
|
const messages = []
|
|
216
|
+
let configuredPrompt = systemPrompt
|
|
217
|
+
if (systemPrompt?.type === 'custom') configuredPrompt = systemPrompt.prompt
|
|
218
|
+
else if (systemPrompt?.type === 'preset') configuredPrompt = systemPrompt.append
|
|
219
|
+
if (typeof configuredPrompt === 'string') {
|
|
220
|
+
if (configuredPrompt) messages.push({ role: 'system', content: configuredPrompt })
|
|
221
|
+
} else if (Array.isArray(configuredPrompt)) {
|
|
222
|
+
for (const part of configuredPrompt) {
|
|
223
|
+
if (typeof part === 'string' && part && part !== SYSTEM_PROMPT_DYNAMIC_BOUNDARY) {
|
|
224
|
+
messages.push({ role: 'system', content: part })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
186
228
|
if (initialPrompt) messages.push({ role: 'user', content: initialPrompt })
|
|
187
229
|
const seenIds = new Set()
|
|
188
230
|
|
|
@@ -245,6 +287,13 @@ class ToolLlmObsPlugin extends LLMObsPlugin {
|
|
|
245
287
|
super.asyncEnd(ctx)
|
|
246
288
|
}
|
|
247
289
|
|
|
290
|
+
/**
|
|
291
|
+
* @override
|
|
292
|
+
*/
|
|
293
|
+
getGenAiApmEndTags (ctx) {
|
|
294
|
+
return subagentToolIds.delete(ctx.id) ? { spanKind: 'agent' } : {}
|
|
295
|
+
}
|
|
296
|
+
|
|
248
297
|
setLLMObsTags (ctx) {
|
|
249
298
|
const span = ctx.currentStore?.span
|
|
250
299
|
if (!span) return
|
|
@@ -23,6 +23,13 @@ class GenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
23
23
|
|
|
24
24
|
// Subscribe to streaming chunk events
|
|
25
25
|
this.addSub('apm:google:genai:request:chunk', ({ ctx, chunk, done }) => {
|
|
26
|
+
if (!this._llmobsEnabledFor(ctx)) {
|
|
27
|
+
// only the token usage is needed, for the `gen_ai.usage.*` metrics. The aggregated
|
|
28
|
+
// response is left alone: it feeds the LLMObs payload and `google_genai.response.model`.
|
|
29
|
+
if (chunk?.usageMetadata) ctx.streamedUsageMetadata = chunk.usageMetadata
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
ctx.isStreaming = true
|
|
27
34
|
ctx.chunks ||= []
|
|
28
35
|
|
|
@@ -49,6 +56,13 @@ class GenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
/**
|
|
60
|
+
* @override
|
|
61
|
+
*/
|
|
62
|
+
getGenAiApmEndTags (ctx) {
|
|
63
|
+
return { metrics: extractMetrics(ctx.result ?? { usageMetadata: ctx.streamedUsageMetadata }) }
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
setLLMObsTags (ctx) {
|
|
53
67
|
const { args, methodName } = ctx
|
|
54
68
|
const span = ctx.currentStore?.span
|
|
@@ -38,19 +38,6 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
const outputMessages = []
|
|
41
|
-
let inputTokens = 0
|
|
42
|
-
let outputTokens = 0
|
|
43
|
-
let totalTokens = 0
|
|
44
|
-
let tokensSetTopLevel = false
|
|
45
|
-
const tokensPerRunId = {}
|
|
46
|
-
|
|
47
|
-
if (!isWorkflow) {
|
|
48
|
-
const tokens = this.checkTokenUsageChatOrLLMResult(results)
|
|
49
|
-
inputTokens = tokens.inputTokens
|
|
50
|
-
outputTokens = tokens.outputTokens
|
|
51
|
-
totalTokens = tokens.totalTokens
|
|
52
|
-
tokensSetTopLevel = totalTokens > 0
|
|
53
|
-
}
|
|
54
41
|
|
|
55
42
|
for (const messageSet of results.generations) {
|
|
56
43
|
for (const chatCompletion of messageSet) {
|
|
@@ -59,35 +46,47 @@ class LangChainLLMObsChatModelHandler extends LangChainLLMObsHandler {
|
|
|
59
46
|
const content = chatCompletionMessage.text || ''
|
|
60
47
|
const toolCalls = this.extractToolCalls(chatCompletionMessage)
|
|
61
48
|
outputMessages.push({ content, role, toolCalls })
|
|
62
|
-
|
|
63
|
-
if (!isWorkflow && !tokensSetTopLevel) {
|
|
64
|
-
const { tokens, runId } = this.checkTokenUsageFromAIMessage(chatCompletionMessage)
|
|
65
|
-
if (tokensPerRunId[runId]) {
|
|
66
|
-
tokensPerRunId[runId].inputTokens += tokens.inputTokens
|
|
67
|
-
tokensPerRunId[runId].outputTokens += tokens.outputTokens
|
|
68
|
-
tokensPerRunId[runId].totalTokens += tokens.totalTokens
|
|
69
|
-
} else {
|
|
70
|
-
tokensPerRunId[runId] = tokens
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
49
|
}
|
|
74
50
|
}
|
|
75
51
|
|
|
76
|
-
if (!isWorkflow && !tokensSetTopLevel) {
|
|
77
|
-
inputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.inputTokens, 0)
|
|
78
|
-
outputTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.outputTokens, 0)
|
|
79
|
-
totalTokens = Object.values(tokensPerRunId).reduce((acc, val) => acc + val.totalTokens, 0)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
52
|
if (isWorkflow) {
|
|
83
53
|
this._tagger.tagTextIO(span, inputMessages, outputMessages)
|
|
84
54
|
} else {
|
|
85
55
|
this._tagger.tagLLMIO(span, inputMessages, outputMessages)
|
|
86
|
-
this._tagger.tagMetrics(span,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
56
|
+
this._tagger.tagMetrics(span, this.getTokenUsage(results))
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @override
|
|
62
|
+
*/
|
|
63
|
+
getTokenUsage (results) {
|
|
64
|
+
const tokens = this.checkTokenUsageChatOrLLMResult(results)
|
|
65
|
+
if (tokens.totalTokens > 0) return tokens
|
|
66
|
+
|
|
67
|
+
// providers that report usage on each generated message instead of on `llmOutput`; counts are
|
|
68
|
+
// summed per run so a run split across generations is totalled once
|
|
69
|
+
if (!results.generations) return tokens
|
|
70
|
+
|
|
71
|
+
const tokensPerRunId = {}
|
|
72
|
+
for (const messageSet of results.generations) {
|
|
73
|
+
for (const chatCompletion of messageSet) {
|
|
74
|
+
const { tokens: messageTokens, runId } = this.checkTokenUsageFromAIMessage(chatCompletion.message)
|
|
75
|
+
if (tokensPerRunId[runId]) {
|
|
76
|
+
tokensPerRunId[runId].inputTokens += messageTokens.inputTokens
|
|
77
|
+
tokensPerRunId[runId].outputTokens += messageTokens.outputTokens
|
|
78
|
+
tokensPerRunId[runId].totalTokens += messageTokens.totalTokens
|
|
79
|
+
} else {
|
|
80
|
+
tokensPerRunId[runId] = messageTokens
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const perRun = Object.values(tokensPerRunId)
|
|
86
|
+
return {
|
|
87
|
+
inputTokens: perRun.reduce((acc, val) => acc + val.inputTokens, 0),
|
|
88
|
+
outputTokens: perRun.reduce((acc, val) => acc + val.outputTokens, 0),
|
|
89
|
+
totalTokens: perRun.reduce((acc, val) => acc + val.totalTokens, 0),
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
|
|
@@ -12,6 +12,16 @@ class LangChainLLMObsHandler {
|
|
|
12
12
|
|
|
13
13
|
setMetaTags () {}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Token usage for a chat or LLM result. Subclasses widen this where the provider reports usage
|
|
17
|
+
* somewhere other than `llmOutput`.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} results
|
|
20
|
+
*/
|
|
21
|
+
getTokenUsage (results) {
|
|
22
|
+
return this.checkTokenUsageChatOrLLMResult(results)
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
checkTokenUsageChatOrLLMResult (results) {
|
|
16
26
|
const llmOutput = results.llmOutput
|
|
17
27
|
const tokens = {
|
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
const log = require('../../../log')
|
|
4
4
|
const LLMObsPlugin = require('../base')
|
|
5
5
|
|
|
6
|
-
const pluginManager = require('../../../../../..')._pluginManager
|
|
7
|
-
|
|
8
6
|
const ANTHROPIC_PROVIDER_NAME = 'anthropic'
|
|
9
7
|
const BEDROCK_PROVIDER_NAME = 'amazon_bedrock'
|
|
10
8
|
const OPENAI_PROVIDER_NAME = 'openai'
|
|
@@ -76,6 +74,28 @@ class BaseLangChainLLMObsPlugin extends LLMObsPlugin {
|
|
|
76
74
|
}
|
|
77
75
|
}
|
|
78
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @override
|
|
79
|
+
*/
|
|
80
|
+
getGenAiApmEndTags (ctx, spanKind) {
|
|
81
|
+
// the helper reports zeros when the result carries no usage, and zeroed token metrics would
|
|
82
|
+
// read as a real measurement
|
|
83
|
+
const tokens = this._handlers[ctx.type]?.getTokenUsage(ctx.result ?? {})
|
|
84
|
+
const metrics = tokens?.totalTokens ? tokens : undefined
|
|
85
|
+
|
|
86
|
+
// langchain-openai calls an untraced beta client when `response_format` is set, so this span is
|
|
87
|
+
// the only model span for the call. `handlers/chat_model.js` applies the same correction through
|
|
88
|
+
// `changeKind` while building the LLMObs payload.
|
|
89
|
+
if (spanKind !== WORKFLOW || ctx.type !== 'chat_model' || !ctx.arguments?.[1]?.response_format) {
|
|
90
|
+
return { metrics }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const provider = ctx.currentStore?.span?.context().getTags()['langchain.request.provider']
|
|
94
|
+
const isOpenAI = this.getIntegrationName(ctx.type, provider) === OPENAI_PROVIDER_NAME
|
|
95
|
+
|
|
96
|
+
return isOpenAI ? { spanKind: LLM, metrics } : { metrics }
|
|
97
|
+
}
|
|
98
|
+
|
|
79
99
|
setLLMObsTags (ctx) {
|
|
80
100
|
ctx.args = ctx.arguments
|
|
81
101
|
ctx.instance = ctx.self
|
|
@@ -158,6 +178,9 @@ class BaseLangChainLLMObsPlugin extends LLMObsPlugin {
|
|
|
158
178
|
}
|
|
159
179
|
|
|
160
180
|
isLLMIntegrationEnabled (integration) {
|
|
181
|
+
// read off the owning manager rather than a module-scope capture: the tracer, and with it the
|
|
182
|
+
// plugin manager, can be rebuilt after this module is first loaded
|
|
183
|
+
const pluginManager = this._tracer?._pluginManager
|
|
161
184
|
return SUPPORTED_INTEGRATIONS.has(integration) && pluginManager?._pluginsByName[integration]?.llmobs?._enabled
|
|
162
185
|
}
|
|
163
186
|
}
|
|
@@ -14,15 +14,16 @@ class PregelStreamLLMObsPlugin extends LLMObsPlugin {
|
|
|
14
14
|
getLLMObsSpanRegisterOptions (ctx) {
|
|
15
15
|
const name = ctx.self.name || 'LangGraph'
|
|
16
16
|
|
|
17
|
-
const enabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
|
|
18
|
-
if (!enabled) return
|
|
19
|
-
|
|
20
17
|
const span = ctx.currentStore?.span
|
|
21
18
|
if (!span) return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
|
|
20
|
+
// only the LLMObs payload consumes the accumulated stream
|
|
21
|
+
if (this._llmobsEnabledFor(ctx)) {
|
|
22
|
+
streamDataMap.set(span, {
|
|
23
|
+
streamInputs: ctx.arguments?.[0],
|
|
24
|
+
chunks: [],
|
|
25
|
+
})
|
|
26
|
+
}
|
|
26
27
|
|
|
27
28
|
return {
|
|
28
29
|
kind: 'workflow',
|
|
@@ -74,6 +74,19 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* @override
|
|
79
|
+
*/
|
|
80
|
+
getGenAiApmEndTags (ctx) {
|
|
81
|
+
const response = ctx.result?.data
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
// the response model is the resolved one, e.g. the dated version behind an alias
|
|
85
|
+
modelName: response?.model,
|
|
86
|
+
metrics: response && this._extractMetrics(response),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
setLLMObsTags (ctx) {
|
|
78
91
|
const span = ctx.currentStore?.span
|
|
79
92
|
const resource = ctx.methodName
|
|
@@ -81,8 +94,8 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
|
|
|
81
94
|
if (!methodName) return // we will not trace all openai methods for llmobs
|
|
82
95
|
|
|
83
96
|
const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument
|
|
84
|
-
const response = ctx.result?.data // no result if error
|
|
85
|
-
const error = !!span.context().getTag('error')
|
|
97
|
+
const response = ctx.result?.data // no result if error, or if a stream ended before any response arrived
|
|
98
|
+
const error = !!span.context().getTag('error') || response == null
|
|
86
99
|
|
|
87
100
|
const operation = getOperation(methodName)
|
|
88
101
|
|
|
@@ -155,6 +155,11 @@ class RealtimeLLMObsPlugin extends LLMObsPlugin {
|
|
|
155
155
|
static integration = 'openai'
|
|
156
156
|
static system = 'openai'
|
|
157
157
|
|
|
158
|
+
// The instrumentation retains a turn's audio only while these plugins are subscribed, and only
|
|
159
|
+
// the LLM Observability payload reads those bytes. Staying subscribed for the `gen_ai.*` tags
|
|
160
|
+
// alone would buffer megabytes per turn for no consumer, so realtime opts out.
|
|
161
|
+
static emitsGenAiApmTags = false
|
|
162
|
+
|
|
158
163
|
/**
|
|
159
164
|
* The instrumentation replays the turn with `traceSync`, which never publishes `asyncEnd`, so tag
|
|
160
165
|
* on `end` instead — before the sibling tracing plugin's `end` finishes the span, since the LLM
|
|
@@ -23,6 +23,13 @@ class VertexAILLMObsPlugin extends LLMObsPlugin {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* @override
|
|
28
|
+
*/
|
|
29
|
+
getGenAiApmEndTags (ctx) {
|
|
30
|
+
return { metrics: extractMetrics(ctx.result) }
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
setLLMObsTags (ctx) {
|
|
27
34
|
const span = ctx.currentStore?.span
|
|
28
35
|
if (!span) return
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('../../../../../index').llmobs.FormattedPromptMessage} FormattedPromptMessage */
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// Match double braces first; preserve surrounding braces such as the closing object in {"age": {age}}.
|
|
6
|
+
const VARIABLE_PATTERN = /\{\{\s*(\w+)\s*\}\}|\{\s*(\w+)\s*\}/g
|
|
6
7
|
|
|
7
8
|
function isMessage (value) {
|
|
8
9
|
return typeof value?.role === 'string' &&
|