From ccaf2b2925e1caaffc671eb813ec17e1d644dc30 Mon Sep 17 00:00:00 2001 From: Crystal Magloire Date: Thu, 18 Sep 2025 09:05:54 -0400 Subject: [PATCH 01/16] instrumentation of genai --- .../datadog-instrumentations/src/genai.js | 203 ++++++++++++++++++ .../src/helpers/hooks.js | 1 + .../datadog-plugin-google-genai/src/index.js | 17 ++ .../src/tracing.js | 49 +++++ packages/dd-trace/src/llmobs/plugins/genai.js | 191 ++++++++++++++++ packages/dd-trace/src/plugins/index.js | 1 + .../src/service-naming/schemas/v0/web.js | 4 + .../src/service-naming/schemas/v1/web.js | 4 + .../src/supported-configurations.json | 1 + packages/dd-trace/test/plugins/externals.json | 6 + 10 files changed, 477 insertions(+) create mode 100644 packages/datadog-instrumentations/src/genai.js create mode 100644 packages/datadog-plugin-google-genai/src/index.js create mode 100644 packages/datadog-plugin-google-genai/src/tracing.js create mode 100644 packages/dd-trace/src/llmobs/plugins/genai.js diff --git a/packages/datadog-instrumentations/src/genai.js b/packages/datadog-instrumentations/src/genai.js new file mode 100644 index 00000000000..09f3ae34b5a --- /dev/null +++ b/packages/datadog-instrumentations/src/genai.js @@ -0,0 +1,203 @@ +'use strict' + +const { addHook } = require('./helpers/instrument') +const shimmer = require('../../datadog-shimmer') + +const tracingChannel = require('dc-polyfill').tracingChannel + +const genaiChannel = tracingChannel('apm:google:genai:request') + +function wrapGenerateContent (method) { + return function wrappedGenerateContentInternal (func) { + console.log('where are we even') + return function (...args) { + console.log('where are we even', args) + if (genaiChannel.hasSubscribers) { + const inputs = args[0] + const promptText = inputs?.contents || '' + const normalizedName = normalizeMethodName(method) + console.log('normalized', normalizedName) + + const ctx = { + methodName: normalizedName, + inputs, + promptText, + model: args[0].model || 'unknown' + } + return genaiChannel.tracePromise(func, ctx, this, ...args) + } + + return func.apply(this, args) + } + } +} + +// Hook the main package entry point +addHook({ + name: '@google/genai', + versions: ['>=1.19.0'] +}, exports => { + // Wrap GoogleGenAI to intercept when it creates Models instances + if (exports.GoogleGenAI) { + shimmer.wrap(exports, 'GoogleGenAI', GoogleGenAI => { + return class extends GoogleGenAI { + constructor (...args) { + super(...args) + + // Wrap the models property after it's created + if (this.models) { + if (this.models.generateContent) { + shimmer.wrap(this.models, 'generateContent', wrapGenerateContent('generateContent')) + } + if (this.models.generateContentStream) { + shimmer.wrap(this.models, 'generateContentStream', wrapGenerateContent('generateContentStream')) + } + if (this.models.embedContent) { + shimmer.wrap(this.models, 'embedContent', wrapGenerateContent('embedContent')) + } + } + } + } + }) + } + + return exports +}) +function normalizeMethodName (methodName) { + // using regex and built-in method less verbose only slightly slower than a more + // verbose nested loop + return 'Models.' + methodName + .replaceAll(/([a-z0-9])([A-Z])/g, '$1_$2') // insert underscore before capitals + .toLowerCase() +} + +// const extensions = ['cjs', 'mjs'] +// const paths = [ +// 'dist/index', // Main entry point +// 'dist/node/index' // Node-specific entry point +// ] + +// for (const extension of extensions) { +// for (const path of paths) { +// const fullPath = `${path}.${extension}` +// console.log('=== REGISTERING HOOK ===') +// console.log('Extension:', extension) +// console.log('Path:', path) +// console.log('Full path:', fullPath) +// console.log('Expected moduleName:', `@google/genai/${fullPath}`) + +// addHook({ +// name: '@google/genai', +// file: fullPath, +// versions: ['>=1.19.0'] +// }, exports => { +// console.log('=== HOOK TRIGGERED ===') +// console.log('Extension:', extension) +// console.log('Path:', path) +// console.log('Expected fullFilename:', `@google/genai/${path}.${extension}`) +// console.log('in the hook', extension, path) +// // if (extension === 'cjs') { +// shimmer.wrap(exports, 'Models', Models => { +// console.log('=== WRAPPING Models CLASS ===') +// console.log('Models:', Models) +// console.log('Models.prototype:', Models.prototype) +// console.log('Models.prototype.generateContent:', typeof Models.prototype.generateContent) +// return class extends Models { +// constructor (...args) { +// super(...args) +// console.log('this.constructor.name', this.constructor.name) +// if (this.constructor.name) {} +// console.log('=== ABOUT TO WRAP generateContent ===') +// shimmer.wrap(Models.prototype, 'generateContent', +// wrapGenerateContent +// ('generateContent')) +// console.log('=== FINISHED WRAPPING generateContent ===') +// } +// } +// }) +// // } +// return exports +// }) +// } +// } +// function wrap (obj, name, channelName, namespace) { +// const channel = tracingChannel(channelName) +// shimmer.wrap(obj, name, function (original) { +// console.log('functio wrap') +// return function () { +// if (!channel.start.hasSubscribers) { +// return original.apply(this, arguments) +// } +// const ctx = { self: this, arguments } +// if (namespace) { +// ctx.namespace = namespace +// } +// return channel.tracePromise(original, ctx, this, ...arguments) +// } +// }) +// } +// function normalizeGenAIResourceName (resource) { +// switch (resource) { +// // completions +// case 'completions.create': +// return 'createCompletion' + +// // chat completions +// case 'generateContentStreamInternal': +// return 'createChatCompletion' + +// // embeddings +// case 'embeddings.create': +// return 'createEmbedding' +// default: +// return resource +// } +// } +// } +// const { addHook } = require('./helpers/instrument') +// const shimmer = require('../../datadog-shimmer') + +// const dc = require('dc-polyfill') +// const genRequest = dc.tracingChannel('apm:gemini:request') +// console.log('in gen instru') + +// function wrapGenerate (that) { +// console.log('in generate!', arguments, that.constructor) +// return function (...args) { +// console.log('GENERATE', args) +// that.constructor.apply(this, args) +// } +// } + +// addHook({ +// name: '@google/genai', +// versions: ['>=1.19.0'] +// }, gemini => { +// // Wrap generateContent directly on the prototype +// console.log('gemini.Models.prototype.generateContent', gemini.Models.prototype.generateContent) +// if (gemini.Models && gemini.Models.prototype && typeof gemini.Models.prototype.generateContent === 'function') { +// shimmer.wrap(gemini.Models.prototype, 'generateContent', function (original) { +// return async function (...args) { +// console.log('generateContent called with:', args) +// const result = await original.apply(this, args) +// console.log('generateContent returned:', result) +// return result +// } +// }) +// } +// }) + +// if (gemini.Models && +// gemini.Models.prototype && +// typeof gemini.Models.prototype.generateContentInternal === 'function') { +// shimmer.wrap(gemini.Models.prototype, 'generateContentInternal', +// wrapGenerateContent +// ('generateContentInternal')) +// } +// if (gemini.Models && +// gemini.Models.prototype && +// typeof gemini.Models.prototype.generateContentStreamInternal === 'function') { +// shimmer.wrap(gemini.Models.prototype, 'generateContentStreamInternal', +// wrapGenerateContent +// ('generateContentStreamInternal')) +// } diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index c46827b3537..53f1c008f38 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -13,6 +13,7 @@ module.exports = { '@playwright/test': () => require('../playwright'), '@elastic/elasticsearch': () => require('../elasticsearch'), '@elastic/transport': () => require('../elasticsearch'), + '@google/genai': () => require('../genai'), '@google-cloud/pubsub': () => require('../google-cloud-pubsub'), '@google-cloud/vertexai': () => require('../google-cloud-vertexai'), '@graphql-tools/executor': () => require('../graphql'), diff --git a/packages/datadog-plugin-google-genai/src/index.js b/packages/datadog-plugin-google-genai/src/index.js new file mode 100644 index 00000000000..f8c2a01079c --- /dev/null +++ b/packages/datadog-plugin-google-genai/src/index.js @@ -0,0 +1,17 @@ +'use strict' + +const CompositePlugin = require('../../dd-trace/src/plugins/composite') +const GenAiTracingPlugin = require('./tracing') +const GenAiLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/openai') + +class GenAiPlugin extends CompositePlugin { + static id = 'genai' + static get plugins () { + return { + llmobs: GenAiLLMObsPlugin, + tracing: GenAiTracingPlugin + } + } +} + +module.exports = GenAiPlugin diff --git a/packages/datadog-plugin-google-genai/src/tracing.js b/packages/datadog-plugin-google-genai/src/tracing.js new file mode 100644 index 00000000000..7d2e1528765 --- /dev/null +++ b/packages/datadog-plugin-google-genai/src/tracing.js @@ -0,0 +1,49 @@ +'use strict' + +const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') +const tags = require('../../../ext/tags.js') + +// const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE +console.log('in the plugin') +class GenAiTracingPlugin extends TracingPlugin { + static id = 'genai' + static operation = 'request' + + static prefix = 'tracing:apm:google:genai:request' + + static get type () { return 'web' } + static get kind () { return 'client' } + bindStart (ctx) { + const { methodName, inputs, promptText, model } = ctx + + const service = this.serviceName({ pluginConfig: this.config }) + + const span = this.startSpan('google_genai.request', { + service, + resource: methodName, + type: 'genai', + kind: 'client', + meta: { + 'google_genai.request.model': model + } + }, ctx) + ctx.span = span + + return ctx.currentStore + } + + bindAsyncStart (ctx) { + // console.log('bind async return') + return ctx.parentStore + } + + asyncStart (ctx) { + ctx.span.finish() + } + + end (ctx) { + ctx.span.finish() + } +} + +module.exports = GenAiTracingPlugin diff --git a/packages/dd-trace/src/llmobs/plugins/genai.js b/packages/dd-trace/src/llmobs/plugins/genai.js new file mode 100644 index 00000000000..7fea3f35521 --- /dev/null +++ b/packages/dd-trace/src/llmobs/plugins/genai.js @@ -0,0 +1,191 @@ +'use strict' + +const LLMObsPlugin = require('./base') +function isIterable (obj) { + if (obj == null) { + return false + } + return typeof obj[Symbol.iterator] === 'function' +} + +class GenAiLLMObsPlugin extends LLMObsPlugin { + static id = 'genai' + static integration = 'genai' + + static prefix = 'tracing:apm:google:genai:request' + + getLLMObsSpanRegisterOptions (ctx) { + const resource = ctx.methodName + console.log('resource', resource) + // // const methodName = gateResource(normalizeOpenAIResourceName(resource)) + // if (!methodName) return // we will not trace all openai methods for llmobs + + // const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument + // const operation = getOperation(methodName) + // const kind = operation === 'embedding' ? 'embedding' : 'llm' + + // const { modelProvider, client } = this._getModelProviderAndClient(ctx.basePath) + + // const name = `${client}.${methodName}` + + // return { + // modelProvider, + // modelName: inputs.model, + // kind, + // name + // } + } + + // setLLMObsTags (ctx) { + // const span = ctx.currentStore?.span + // const resource = ctx.methodName + // const methodName = gateResource(normalizeOpenAIResourceName(resource)) + // if (!methodName) return // we will not trace all openai methods for llmobs + + // const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument + // const response = ctx.result?.data // no result if error + // const error = !!span.context()._tags.error + + // const operation = getOperation(methodName) + + // if (operation === 'completion') { + // this._tagCompletion(span, inputs, response, error) + // } else if (operation === 'chat') { + // this._tagChatCompletion(span, inputs, response, error) + // } else if (operation === 'embedding') { + // this._tagEmbedding(span, inputs, response, error) + // } + + // if (!error) { + // const metrics = this._extractMetrics(response) + // this._tagger.tagMetrics(span, metrics) + // } + // } + + // _getModelProviderAndClient (baseUrl = '') { + // if (baseUrl.includes('azure')) { + // return { modelProvider: 'azure_openai', client: 'AzureOpenAI' } + // } else if (baseUrl.includes('deepseek')) { + // return { modelProvider: 'deepseek', client: 'DeepSeek' } + // } + // return { modelProvider: 'openai', client: 'OpenAI' } + // } + + // _extractMetrics (response) { + // const metrics = {} + // const tokenUsage = response.usage + + // if (tokenUsage) { + // const inputTokens = tokenUsage.prompt_tokens + // if (inputTokens) metrics.inputTokens = inputTokens + + // const outputTokens = tokenUsage.completion_tokens + // if (outputTokens) metrics.outputTokens = outputTokens + + // const totalTokens = tokenUsage.total_toksn || (inputTokens + outputTokens) + // if (totalTokens) metrics.totalTokens = totalTokens + // } + + // return metrics + // } + + // _tagEmbedding (span, inputs, response, error) { + // const { model, ...parameters } = inputs + + // const metadata = { + // encoding_format: parameters.encoding_format || 'float' + // } + // if (inputs.dimensions) metadata.dimensions = inputs.dimensions + // this._tagger.tagMetadata(span, metadata) + + // let embeddingInputs = inputs.input + // if (!Array.isArray(embeddingInputs)) embeddingInputs = [embeddingInputs] + // const embeddingInput = embeddingInputs.map(input => ({ text: input })) + + // if (error) { + // this._tagger.tagEmbeddingIO(span, embeddingInput) + // return + // } + + // const float = Array.isArray(response.data[0].embedding) + // let embeddingOutput + // if (float) { + // const embeddingDim = response.data[0].embedding.length + // embeddingOutput = `[${response.data.length} embedding(s) returned with size ${embeddingDim}]` + // } else { + // embeddingOutput = `[${response.data.length} embedding(s) returned]` + // } + + // this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) + // } + + // _tagCompletion (span, inputs, response, error) { + // let { prompt, model, ...parameters } = inputs + // if (!Array.isArray(prompt)) prompt = [prompt] + + // const completionInput = prompt.map(p => ({ content: p })) + + // const completionOutput = error ? [{ content: '' }] : response.choices.map(choice => ({ content: choice.text })) + + // this._tagger.tagLLMIO(span, completionInput, completionOutput) + // this._tagger.tagMetadata(span, parameters) + // } + + // _tagChatCompletion (span, inputs, response, error) { + // const { messages, model, ...parameters } = inputs + + // if (error) { + // this._tagger.tagLLMIO(span, messages, [{ content: '' }]) + // return + // } + + // const outputMessages = [] + // const { choices } = response + // if (!isIterable(choices)) { + // this._tagger.tagLLMIO(span, messages, [{ content: '' }]) + // return + // } + + // for (const choice of choices) { + // const message = choice.message || choice.delta + // const content = message.content || '' + // const role = message.role + + // if (message.function_call) { + // const functionCallInfo = { + // name: message.function_call.name, + // arguments: JSON.parse(message.function_call.arguments) + // } + // outputMessages.push({ content, role, toolCalls: [functionCallInfo] }) + // } else if (message.tool_calls) { + // const toolCallsInfo = [] + // for (const toolCall of message.tool_calls) { + // const toolCallInfo = { + // arguments: JSON.parse(toolCall.function.arguments), + // name: toolCall.function.name, + // toolId: toolCall.id, + // type: toolCall.type + // } + // toolCallsInfo.push(toolCallInfo) + // } + // outputMessages.push({ content, role, toolCalls: toolCallsInfo }) + // } else { + // outputMessages.push({ content, role }) + // } + // } + + // this._tagger.tagLLMIO(span, messages, outputMessages) + + // const metadata = Object.entries(parameters).reduce((obj, [key, value]) => { + // if (!['tools', 'functions'].includes(key)) { + // obj[key] = value + // } + + // return obj + // }, {}) + + // this._tagger.tagMetadata(span, metadata) + // } +} + +module.exports = GenAiLLMObsPlugin diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 091fb24f9ed..d30e31a68d4 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -13,6 +13,7 @@ module.exports = { get '@elastic/transport' () { return require('../../../datadog-plugin-elasticsearch/src') }, get '@google-cloud/pubsub' () { return require('../../../datadog-plugin-google-cloud-pubsub/src') }, get '@google-cloud/vertexai' () { return require('../../../datadog-plugin-google-cloud-vertexai/src') }, + get '@google/genai' () { return require('../../../datadog-plugin-google-genai/src') }, get '@grpc/grpc-js' () { return require('../../../datadog-plugin-grpc/src') }, get '@hapi/hapi' () { return require('../../../datadog-plugin-hapi/src') }, get '@happy-dom/jest-environment' () { return require('../../../datadog-plugin-jest/src') }, diff --git a/packages/dd-trace/src/service-naming/schemas/v0/web.js b/packages/dd-trace/src/service-naming/schemas/v0/web.js index 23046f8ce8d..24cbe1b004d 100644 --- a/packages/dd-trace/src/service-naming/schemas/v0/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v0/web.js @@ -24,6 +24,10 @@ const web = { opName: () => 'http.request', serviceName: httpPluginClientService }, + genai: { + opName: () => 'google_genai.request', + serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService + }, aws: { opName: () => 'aws.request', serviceName: awsServiceV0 diff --git a/packages/dd-trace/src/service-naming/schemas/v1/web.js b/packages/dd-trace/src/service-naming/schemas/v1/web.js index 66b1afee22f..39f3bf33c81 100644 --- a/packages/dd-trace/src/service-naming/schemas/v1/web.js +++ b/packages/dd-trace/src/service-naming/schemas/v1/web.js @@ -16,6 +16,10 @@ const web = { opName: () => 'http.client.request', serviceName: httpPluginClientService }, + genai: { + opName: () => 'google_genai.request', + serviceName: ({ pluginConfig, tracerService }) => pluginConfig.service || tracerService + }, fetch: { opName: () => 'http.client.request', serviceName: httpPluginClientService diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 3600de13c1e..764dae9c92f 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -287,6 +287,7 @@ "DD_TRACE_FLUSH_INTERVAL": ["A"], "DD_TRACE_FS_ENABLED": ["A"], "DD_TRACE_GENERIC_POOL_ENABLED": ["A"], + "DD_TRACE_GENAI_ENABLED": ["A"], "DD_TRACE_GIT_METADATA_ENABLED": ["A"], "DD_TRACE_GLOBAL_TAGS": ["A"], "DD_TRACE_GOOGLE_CLOUD_PUBSUB_ENABLED": ["A"], diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index 867773788c5..2be21d0236f 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -201,6 +201,12 @@ "versions": ["3.5.7"] } ], + "genai": [ + { + "name": "@google/genai", + "versions": [">=1.19.0"] + } + ], "graphql": [ { "name": "apollo-server-core", From 14eab82449d4c4954511dec00b22262278b398f4 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Thu, 25 Sep 2025 14:12:32 -0400 Subject: [PATCH 02/16] genai plugin --- packages/datadog-plugin-google-genai/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datadog-plugin-google-genai/src/index.js b/packages/datadog-plugin-google-genai/src/index.js index f8c2a01079c..3f1243d9454 100644 --- a/packages/datadog-plugin-google-genai/src/index.js +++ b/packages/datadog-plugin-google-genai/src/index.js @@ -2,7 +2,7 @@ const CompositePlugin = require('../../dd-trace/src/plugins/composite') const GenAiTracingPlugin = require('./tracing') -const GenAiLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/openai') +const GenAiLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/genai') class GenAiPlugin extends CompositePlugin { static id = 'genai' From ed56c5a7a062745f116d180c7654fae73a9dad5e Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 17 Nov 2025 12:27:11 -0500 Subject: [PATCH 03/16] gemini llm plugin --- .../datadog-instrumentations/src/genai.js | 228 ++---- .../src/tracing.js | 30 +- packages/dd-trace/src/llmobs/plugins/genai.js | 680 +++++++++++++----- 3 files changed, 593 insertions(+), 345 deletions(-) diff --git a/packages/datadog-instrumentations/src/genai.js b/packages/datadog-instrumentations/src/genai.js index 09f3ae34b5a..c9db26a46fe 100644 --- a/packages/datadog-instrumentations/src/genai.js +++ b/packages/datadog-instrumentations/src/genai.js @@ -2,36 +2,89 @@ const { addHook } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') - const tracingChannel = require('dc-polyfill').tracingChannel +const channel = require('dc-polyfill').channel -const genaiChannel = tracingChannel('apm:google:genai:request') +const genaiTracingChannel = tracingChannel('apm:google:genai:request') +const onStreamedChunkCh = channel('apm:google:genai:request:chunk') function wrapGenerateContent (method) { - return function wrappedGenerateContentInternal (func) { - console.log('where are we even') + return function wrappedGenerateContent (original) { return function (...args) { - console.log('where are we even', args) - if (genaiChannel.hasSubscribers) { - const inputs = args[0] - const promptText = inputs?.contents || '' - const normalizedName = normalizeMethodName(method) - console.log('normalized', normalizedName) - - const ctx = { - methodName: normalizedName, - inputs, - promptText, - model: args[0].model || 'unknown' - } - return genaiChannel.tracePromise(func, ctx, this, ...args) + if (!genaiTracingChannel.start.hasSubscribers) { + return original.apply(this, args) + } + + const inputs = args[0] + const normalizedName = normalizeMethodName(method) + + const ctx = { + methodName: normalizedName, + inputs, + args, + model: inputs?.model || 'unknown' } - return func.apply(this, args) + return genaiTracingChannel.start.runStores(ctx, () => { + let result + try { + result = original.apply(this, arguments) + } catch (error) { + finish(ctx, null, error) + } + + return result.then(response => { + if (method === 'generateContentStream') { + shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx)) + } else { + finish(ctx, response, null) + genaiTracingChannel.end.publish(ctx) + } + return response + }).catch(error => { + finish(ctx, null, error) + throw error + }) + }) } } } +function wrapStreamIterator (iterator, ctx) { + return function () { + const itr = iterator.apply(this, arguments) + shimmer.wrap(itr, 'next', next => function () { + return next.apply(this, arguments) + .then(res => { + const { done, value: chunk } = res + onStreamedChunkCh.publish({ ctx, chunk, done }) + + if (done) { + finish(ctx) + } + + return res + }) + .catch(error => { + finish(ctx, null, error) + throw error + }) + }) + + return itr + } +} +function finish (ctx, result, error) { + if (error) { + ctx.error = error + genaiTracingChannel.error.publish(ctx) + } + + // streamed responses are handled and set separately + ctx.result ??= result + + genaiTracingChannel.asyncEnd.publish(ctx) +} // Hook the main package entry point addHook({ name: '@google/genai', @@ -63,141 +116,10 @@ addHook({ return exports }) + function normalizeMethodName (methodName) { - // using regex and built-in method less verbose only slightly slower than a more - // verbose nested loop + // Convert camelCase to snake_case and add Models prefix return 'Models.' + methodName - .replaceAll(/([a-z0-9])([A-Z])/g, '$1_$2') // insert underscore before capitals + .replaceAll(/([a-z0-9])([A-Z])/g, '$1_$2') .toLowerCase() } - -// const extensions = ['cjs', 'mjs'] -// const paths = [ -// 'dist/index', // Main entry point -// 'dist/node/index' // Node-specific entry point -// ] - -// for (const extension of extensions) { -// for (const path of paths) { -// const fullPath = `${path}.${extension}` -// console.log('=== REGISTERING HOOK ===') -// console.log('Extension:', extension) -// console.log('Path:', path) -// console.log('Full path:', fullPath) -// console.log('Expected moduleName:', `@google/genai/${fullPath}`) - -// addHook({ -// name: '@google/genai', -// file: fullPath, -// versions: ['>=1.19.0'] -// }, exports => { -// console.log('=== HOOK TRIGGERED ===') -// console.log('Extension:', extension) -// console.log('Path:', path) -// console.log('Expected fullFilename:', `@google/genai/${path}.${extension}`) -// console.log('in the hook', extension, path) -// // if (extension === 'cjs') { -// shimmer.wrap(exports, 'Models', Models => { -// console.log('=== WRAPPING Models CLASS ===') -// console.log('Models:', Models) -// console.log('Models.prototype:', Models.prototype) -// console.log('Models.prototype.generateContent:', typeof Models.prototype.generateContent) -// return class extends Models { -// constructor (...args) { -// super(...args) -// console.log('this.constructor.name', this.constructor.name) -// if (this.constructor.name) {} -// console.log('=== ABOUT TO WRAP generateContent ===') -// shimmer.wrap(Models.prototype, 'generateContent', -// wrapGenerateContent -// ('generateContent')) -// console.log('=== FINISHED WRAPPING generateContent ===') -// } -// } -// }) -// // } -// return exports -// }) -// } -// } -// function wrap (obj, name, channelName, namespace) { -// const channel = tracingChannel(channelName) -// shimmer.wrap(obj, name, function (original) { -// console.log('functio wrap') -// return function () { -// if (!channel.start.hasSubscribers) { -// return original.apply(this, arguments) -// } -// const ctx = { self: this, arguments } -// if (namespace) { -// ctx.namespace = namespace -// } -// return channel.tracePromise(original, ctx, this, ...arguments) -// } -// }) -// } -// function normalizeGenAIResourceName (resource) { -// switch (resource) { -// // completions -// case 'completions.create': -// return 'createCompletion' - -// // chat completions -// case 'generateContentStreamInternal': -// return 'createChatCompletion' - -// // embeddings -// case 'embeddings.create': -// return 'createEmbedding' -// default: -// return resource -// } -// } -// } -// const { addHook } = require('./helpers/instrument') -// const shimmer = require('../../datadog-shimmer') - -// const dc = require('dc-polyfill') -// const genRequest = dc.tracingChannel('apm:gemini:request') -// console.log('in gen instru') - -// function wrapGenerate (that) { -// console.log('in generate!', arguments, that.constructor) -// return function (...args) { -// console.log('GENERATE', args) -// that.constructor.apply(this, args) -// } -// } - -// addHook({ -// name: '@google/genai', -// versions: ['>=1.19.0'] -// }, gemini => { -// // Wrap generateContent directly on the prototype -// console.log('gemini.Models.prototype.generateContent', gemini.Models.prototype.generateContent) -// if (gemini.Models && gemini.Models.prototype && typeof gemini.Models.prototype.generateContent === 'function') { -// shimmer.wrap(gemini.Models.prototype, 'generateContent', function (original) { -// return async function (...args) { -// console.log('generateContent called with:', args) -// const result = await original.apply(this, args) -// console.log('generateContent returned:', result) -// return result -// } -// }) -// } -// }) - -// if (gemini.Models && -// gemini.Models.prototype && -// typeof gemini.Models.prototype.generateContentInternal === 'function') { -// shimmer.wrap(gemini.Models.prototype, 'generateContentInternal', -// wrapGenerateContent -// ('generateContentInternal')) -// } -// if (gemini.Models && -// gemini.Models.prototype && -// typeof gemini.Models.prototype.generateContentStreamInternal === 'function') { -// shimmer.wrap(gemini.Models.prototype, 'generateContentStreamInternal', -// wrapGenerateContent -// ('generateContentStreamInternal')) -// } diff --git a/packages/datadog-plugin-google-genai/src/tracing.js b/packages/datadog-plugin-google-genai/src/tracing.js index 7d2e1528765..456da0ecf89 100644 --- a/packages/datadog-plugin-google-genai/src/tracing.js +++ b/packages/datadog-plugin-google-genai/src/tracing.js @@ -1,48 +1,48 @@ 'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') -const tags = require('../../../ext/tags.js') -// const HTTP_STATUS_CODE = tags.HTTP_STATUS_CODE -console.log('in the plugin') class GenAiTracingPlugin extends TracingPlugin { static id = 'genai' static operation = 'request' - static prefix = 'tracing:apm:google:genai:request' static get type () { return 'web' } static get kind () { return 'client' } + bindStart (ctx) { const { methodName, inputs, promptText, model } = ctx const service = this.serviceName({ pluginConfig: this.config }) - const span = this.startSpan('google_genai.request', { + this.startSpan('google_genai.request', { service, resource: methodName, type: 'genai', kind: 'client', meta: { - 'google_genai.request.model': model + 'google_genai.request.model': model, + 'google_genai.request.provider': 'google' } }, ctx) - ctx.span = span return ctx.currentStore } - bindAsyncStart (ctx) { - // console.log('bind async return') - return ctx.parentStore - } - - asyncStart (ctx) { - ctx.span.finish() + asyncEnd (ctx) { + if (ctx.result) { + ctx.currentStore.span.setTag('google_genai.response.model', ctx.result.modelVersion || ctx.inputs?.model) + } + if (ctx.currentStore.span) { + ctx.currentStore.span.finish() + } } end (ctx) { - ctx.span.finish() + const span = ctx.currentStore?.span + if (!span) return + + span.finish() } } diff --git a/packages/dd-trace/src/llmobs/plugins/genai.js b/packages/dd-trace/src/llmobs/plugins/genai.js index 7fea3f35521..a7ad5b85c6c 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai.js +++ b/packages/dd-trace/src/llmobs/plugins/genai.js @@ -1,191 +1,517 @@ 'use strict' const LLMObsPlugin = require('./base') -function isIterable (obj) { - if (obj == null) { - return false - } - return typeof obj[Symbol.iterator] === 'function' + +// Constants for role mapping +const ROLES = { + MODEL: 'model', + ASSISTANT: 'assistant', + USER: 'user', + REASONING: 'reasoning' } class GenAiLLMObsPlugin extends LLMObsPlugin { static id = 'genai' static integration = 'genai' - static prefix = 'tracing:apm:google:genai:request' + constructor () { + super(...arguments) + + // Subscribe to streaming chunk events + this.addSub('apm:google:genai:request:chunk', ({ ctx, chunk, done }) => { + ctx.isStreaming = true + ctx.chunks = ctx.chunks || [] + + if (chunk) ctx.chunks.push(chunk) + if (!done) return + + // Aggregate streaming chunks into a single response + ctx.result = this._aggregateStreamingChunks(ctx.chunks) + }) + } + + // ============================================================================ + // Public API Methods + // ============================================================================ + getLLMObsSpanRegisterOptions (ctx) { - const resource = ctx.methodName - console.log('resource', resource) - // // const methodName = gateResource(normalizeOpenAIResourceName(resource)) - // if (!methodName) return // we will not trace all openai methods for llmobs - - // const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument - // const operation = getOperation(methodName) - // const kind = operation === 'embedding' ? 'embedding' : 'llm' - - // const { modelProvider, client } = this._getModelProviderAndClient(ctx.basePath) - - // const name = `${client}.${methodName}` - - // return { - // modelProvider, - // modelName: inputs.model, - // kind, - // name - // } - } - - // setLLMObsTags (ctx) { - // const span = ctx.currentStore?.span - // const resource = ctx.methodName - // const methodName = gateResource(normalizeOpenAIResourceName(resource)) - // if (!methodName) return // we will not trace all openai methods for llmobs - - // const inputs = ctx.args[0] // completion, chat completion, and embeddings take one argument - // const response = ctx.result?.data // no result if error - // const error = !!span.context()._tags.error - - // const operation = getOperation(methodName) - - // if (operation === 'completion') { - // this._tagCompletion(span, inputs, response, error) - // } else if (operation === 'chat') { - // this._tagChatCompletion(span, inputs, response, error) - // } else if (operation === 'embedding') { - // this._tagEmbedding(span, inputs, response, error) - // } - - // if (!error) { - // const metrics = this._extractMetrics(response) - // this._tagger.tagMetrics(span, metrics) - // } - // } - - // _getModelProviderAndClient (baseUrl = '') { - // if (baseUrl.includes('azure')) { - // return { modelProvider: 'azure_openai', client: 'AzureOpenAI' } - // } else if (baseUrl.includes('deepseek')) { - // return { modelProvider: 'deepseek', client: 'DeepSeek' } - // } - // return { modelProvider: 'openai', client: 'OpenAI' } - // } - - // _extractMetrics (response) { - // const metrics = {} - // const tokenUsage = response.usage - - // if (tokenUsage) { - // const inputTokens = tokenUsage.prompt_tokens - // if (inputTokens) metrics.inputTokens = inputTokens - - // const outputTokens = tokenUsage.completion_tokens - // if (outputTokens) metrics.outputTokens = outputTokens - - // const totalTokens = tokenUsage.total_toksn || (inputTokens + outputTokens) - // if (totalTokens) metrics.totalTokens = totalTokens - // } - - // return metrics - // } - - // _tagEmbedding (span, inputs, response, error) { - // const { model, ...parameters } = inputs - - // const metadata = { - // encoding_format: parameters.encoding_format || 'float' - // } - // if (inputs.dimensions) metadata.dimensions = inputs.dimensions - // this._tagger.tagMetadata(span, metadata) - - // let embeddingInputs = inputs.input - // if (!Array.isArray(embeddingInputs)) embeddingInputs = [embeddingInputs] - // const embeddingInput = embeddingInputs.map(input => ({ text: input })) - - // if (error) { - // this._tagger.tagEmbeddingIO(span, embeddingInput) - // return - // } - - // const float = Array.isArray(response.data[0].embedding) - // let embeddingOutput - // if (float) { - // const embeddingDim = response.data[0].embedding.length - // embeddingOutput = `[${response.data.length} embedding(s) returned with size ${embeddingDim}]` - // } else { - // embeddingOutput = `[${response.data.length} embedding(s) returned]` - // } - - // this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) - // } - - // _tagCompletion (span, inputs, response, error) { - // let { prompt, model, ...parameters } = inputs - // if (!Array.isArray(prompt)) prompt = [prompt] - - // const completionInput = prompt.map(p => ({ content: p })) - - // const completionOutput = error ? [{ content: '' }] : response.choices.map(choice => ({ content: choice.text })) - - // this._tagger.tagLLMIO(span, completionInput, completionOutput) - // this._tagger.tagMetadata(span, parameters) - // } - - // _tagChatCompletion (span, inputs, response, error) { - // const { messages, model, ...parameters } = inputs - - // if (error) { - // this._tagger.tagLLMIO(span, messages, [{ content: '' }]) - // return - // } - - // const outputMessages = [] - // const { choices } = response - // if (!isIterable(choices)) { - // this._tagger.tagLLMIO(span, messages, [{ content: '' }]) - // return - // } - - // for (const choice of choices) { - // const message = choice.message || choice.delta - // const content = message.content || '' - // const role = message.role - - // if (message.function_call) { - // const functionCallInfo = { - // name: message.function_call.name, - // arguments: JSON.parse(message.function_call.arguments) - // } - // outputMessages.push({ content, role, toolCalls: [functionCallInfo] }) - // } else if (message.tool_calls) { - // const toolCallsInfo = [] - // for (const toolCall of message.tool_calls) { - // const toolCallInfo = { - // arguments: JSON.parse(toolCall.function.arguments), - // name: toolCall.function.name, - // toolId: toolCall.id, - // type: toolCall.type - // } - // toolCallsInfo.push(toolCallInfo) - // } - // outputMessages.push({ content, role, toolCalls: toolCallsInfo }) - // } else { - // outputMessages.push({ content, role }) - // } - // } - - // this._tagger.tagLLMIO(span, messages, outputMessages) - - // const metadata = Object.entries(parameters).reduce((obj, [key, value]) => { - // if (!['tools', 'functions'].includes(key)) { - // obj[key] = value - // } - - // return obj - // }, {}) - - // this._tagger.tagMetadata(span, metadata) - // } + const methodName = ctx.methodName + if (!methodName) return + + const inputs = ctx.inputs + const operation = getOperation(methodName) + const kind = operation === 'embedding' ? 'embedding' : 'llm' + + return { + modelProvider: 'google', + modelName: inputs.model, + kind, + name: 'google_genai.request' + } + } + + setLLMObsTags (ctx) { + const span = ctx.currentStore?.span + const methodName = ctx.methodName + if (!methodName) return + + const inputs = ctx.inputs + const response = ctx.result + const error = !!span.context()._tags.error + + const operation = getOperation(methodName) + + if (operation === 'chat') { + this._tagGenerateContent(span, inputs, response, error, ctx.isStreaming) + } else if (operation === 'embedding') { + this._tagEmbedding(span, inputs, response, error) + } + + if (!error && response) { + const metrics = this._extractMetrics(response) + this._tagger.tagMetrics(span, metrics) + } + } + + // ============================================================================ + // Streaming Utilities + // ============================================================================ + + _aggregateStreamingChunks (chunks) { + const response = { candidates: [] } + + for (const chunk of chunks) { + if (chunk.candidates) { + // Flatten candidates array + response.candidates.push(...chunk.candidates) + } + if (chunk.usageMetadata) { + response.usageMetadata = chunk.usageMetadata + } + } + + return response + } + + // ============================================================================ + // Tagging Methods + // ============================================================================ + + _tagGenerateContent (span, inputs, response, error, isStreaming = false) { + const { config = {} } = inputs + + const inputMessages = this._formatInputMessages(inputs.contents) + + if (error) { + this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }]) + return + } + + const outputMessages = this._formatOutputMessages(response, isStreaming) + this._tagger.tagLLMIO(span, inputMessages, outputMessages) + + const metadata = this._extractMetadata(config) + this._tagger.tagMetadata(span, metadata) + } + + _tagEmbedding (span, inputs, response, error) { + const embeddingInput = this._formatEmbeddingInput(inputs.contents) + + if (error) { + this._tagger.tagEmbeddingIO(span, embeddingInput) + return + } + + const embeddingOutput = this._formatEmbeddingOutput(response) + this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) + this._tagger.tagMetadata(span, {}) + } + + // ============================================================================ + // Input Formatting + // ============================================================================ + + _formatInputMessages (contents) { + if (!contents) return [] + + const contentArray = Array.isArray(contents) ? contents : [contents] + const messages = [] + + for (const content of contentArray) { + if (typeof content === 'string') { + messages.push({ role: ROLES.USER, content }) + } else if (content.text) { + messages.push({ role: ROLES.USER, content: content.text }) + } else if (content.parts) { + const message = this._formatContentObject(content) + if (message) messages.push(message) + } else { + messages.push({ role: ROLES.USER, content: JSON.stringify(content) }) + } + } + + return messages + } + + _formatContentObject (content) { + const parts = content.parts || [] + const role = this._determineRole(content, parts) + + // Check if this is a thought/reasoning part + if (this._hasThoughtParts(parts)) { + return { + role: ROLES.REASONING, + content: this._extractTextParts(parts).join('\n') + } + } + + // Check for function calls + const functionCalls = parts.filter(part => part.functionCall) + if (functionCalls.length > 0) { + return this._formatFunctionCallMessage(parts, functionCalls, role) + } + + // Check for function responses + const functionResponses = parts.filter(part => part.functionResponse) + if (functionResponses.length > 0) { + return this._formatFunctionResponseMessage(functionResponses, role) + } + + // Regular text content + return { + role, + content: this._extractTextParts(parts).join('\n') + } + } + + _formatEmbeddingInput (contents) { + if (!contents) return [] + + const contentArray = Array.isArray(contents) ? contents : [contents] + const documents = [] + + for (const content of contentArray) { + if (typeof content === 'string') { + documents.push({ text: content }) + } else if (content.text) { + documents.push({ text: content.text }) + } else if (content.parts) { + for (const part of content.parts) { + if (typeof part === 'string') { + documents.push({ text: part }) + } else if (part.text) { + documents.push({ text: part.text }) + } + } + } + } + + return documents + } + + // ============================================================================ + // Output Formatting + // ============================================================================ + + _formatOutputMessages (response, isStreaming = false) { + if (!response?.candidates?.length) { + return [{ content: '' }] + } + + if (isStreaming) { + return this._formatStreamingOutput(response) + } + + return this._formatNonStreamingOutput(response) + } + + _formatStreamingOutput (response) { + const messages = [] + const messagesByRole = new Map() + + for (const candidate of response.candidates) { + const content = Array.isArray(candidate) ? candidate[0].content : candidate.content + if (!content?.parts) continue + + // Skip function calls in streaming (they're not accumulated) + if (content.parts.some(part => part.functionCall)) { + messages.push(...this._formatNonStreamingCandidate(candidate)) + continue + } + + // Accumulate text parts by role + const partsByRole = this._groupPartsByRole(content.parts) + + for (const [partRole, textContent] of Object.entries(partsByRole)) { + if (!textContent) continue + + if (messagesByRole.has(partRole)) { + const index = messagesByRole.get(partRole) + messages[index].content += textContent + } else { + const messageIndex = messages.length + messages.push({ role: partRole, content: textContent }) + messagesByRole.set(partRole, messageIndex) + } + } + } + + return messages.length > 0 ? messages : [{ content: '' }] + } + + _formatNonStreamingOutput (response) { + const messages = [] + + for (const candidate of response.candidates) { + messages.push(...this._formatNonStreamingCandidate(candidate)) + } + + return messages.length > 0 ? messages : [{ content: '' }] + } + + _formatNonStreamingCandidate (candidate) { + const messages = [] + const content = Array.isArray(candidate) ? candidate[0].content : candidate.content + + if (!content?.parts) return messages + + const { parts } = content + + // Check for function calls + const functionCalls = parts.filter(part => part.functionCall) + if (functionCalls.length > 0) { + messages.push(this._formatFunctionCallMessage(parts, functionCalls, ROLES.ASSISTANT)) + return messages + } + + // Check for executable code + const executableCode = parts.find(part => part.executableCode) + if (executableCode) { + messages.push({ + role: ROLES.ASSISTANT, + content: JSON.stringify({ + language: executableCode.executableCode.language, + code: executableCode.executableCode.code + }) + }) + return messages + } + + // Check for code execution result + const codeExecutionResult = parts.find(part => part.codeExecutionResult) + if (codeExecutionResult) { + messages.push({ + role: ROLES.ASSISTANT, + content: JSON.stringify({ + outcome: codeExecutionResult.codeExecutionResult.outcome, + output: codeExecutionResult.codeExecutionResult.output + }) + }) + return messages + } + + // Regular text content - may contain both reasoning and assistant parts + const partsByRole = this._groupPartsByRole(parts) + + if (partsByRole.reasoning) { + messages.push({ + role: ROLES.REASONING, + content: partsByRole.reasoning + }) + } + + if (partsByRole.assistant) { + messages.push({ + role: ROLES.ASSISTANT, + content: partsByRole.assistant + }) + } + + return messages + } + + _formatEmbeddingOutput (response) { + if (!response?.embeddings?.length) { + return '' + } + + const embeddingCount = response.embeddings.length + const firstEmbedding = response.embeddings[0] + + if (firstEmbedding.values && Array.isArray(firstEmbedding.values)) { + const embeddingDim = firstEmbedding.values.length + return `[${embeddingCount} embedding(s) returned with size ${embeddingDim}]` + } + + return `[${embeddingCount} embedding(s) returned]` + } + + // ============================================================================ + // Function Call/Response Formatting + // ============================================================================ + + _formatFunctionCallMessage (parts, functionCalls, role) { + const toolCalls = functionCalls.map(part => ({ + name: part.functionCall.name, + arguments: part.functionCall.args, + toolId: part.functionCall.id, + type: 'function_call' + })) + + const textParts = this._extractTextParts(parts) + const content = textParts.length > 0 ? textParts.join('\n') : undefined + + return { + role, + ...(content && { content }), + toolCalls + } + } + + _formatFunctionResponseMessage (functionResponses, role) { + const toolResults = functionResponses.map(part => ({ + name: part.functionResponse.name, + result: JSON.stringify(part.functionResponse.response), + toolId: part.functionResponse.id, + type: 'function_response' + })) + + return { + role, + toolResults + } + } + + // ============================================================================ + // Part Processing Utilities + // ============================================================================ + + _extractTextParts (parts) { + return parts + .filter(part => part.text) + .map(part => part.text) + } + + _groupPartsByRole (parts) { + const grouped = { + reasoning: '', + assistant: '' + } + + for (const part of parts) { + if (!part.text) continue + + if (part.thought === true) { + grouped.reasoning += part.text + } else { + grouped.assistant += part.text + } + } + + return grouped + } + + _hasThoughtParts (parts) { + return parts.some(part => part.thought === true) + } + + // ============================================================================ + // Role Utilities + // ============================================================================ + + _determineRole (candidate, parts = []) { + // Check parts for thought indicators + if (this._hasThoughtParts(parts)) { + return ROLES.REASONING + } + + // Extract role from various possible locations + const rawRole = candidate.role || + candidate.content?.role || + candidate[0]?.content?.role + + return this._normalizeRole(rawRole) + } + + _normalizeRole (role) { + if (role === ROLES.MODEL) return ROLES.ASSISTANT + if (role === ROLES.ASSISTANT) return ROLES.ASSISTANT + if (role === ROLES.USER) return ROLES.USER + if (role === ROLES.REASONING) return ROLES.REASONING + return ROLES.USER // default + } + + // ============================================================================ + // Extraction Utilities + // ============================================================================ + + _extractMetrics (response) { + const metrics = {} + const tokenUsage = response.usageMetadata + + if (!tokenUsage) return metrics + + if (tokenUsage.promptTokenCount) { + metrics.inputTokens = tokenUsage.promptTokenCount + } + + if (tokenUsage.candidatesTokenCount) { + metrics.outputTokens = tokenUsage.candidatesTokenCount + } + + const totalTokens = tokenUsage.totalTokenCount || + (tokenUsage.promptTokenCount || 0) + (tokenUsage.candidatesTokenCount || 0) + if (totalTokens) { + metrics.totalTokens = totalTokens + } + + return metrics + } + + _extractMetadata (config) { + if (!config) return {} + + const fieldMap = { + temperature: 'temperature', + top_p: 'topP', + top_k: 'topK', + candidate_count: 'candidateCount', + max_output_tokens: 'maxOutputTokens', + stop_sequences: 'stopSequences', + response_logprobs: 'responseLogprobs', + logprobs: 'logprobs', + presence_penalty: 'presencePenalty', + frequency_penalty: 'frequencyPenalty', + seed: 'seed', + response_mime_type: 'responseMimeType', + safety_settings: 'safetySettings', + automatic_function_calling: 'automaticFunctionCalling' + } + + const metadata = {} + for (const [metadataKey, configKey] of Object.entries(fieldMap)) { + metadata[metadataKey] = config[configKey] ?? null + } + + return metadata + } +} + +// ============================================================================ +// Module-level Utilities +// ============================================================================ + +function getOperation (methodName) { + if (!methodName) return 'unknown' + + if (methodName.includes('embed')) { + return 'embedding' + } else if (methodName.includes('generate')) { + return 'chat' + } + + return 'unknown' } module.exports = GenAiLLMObsPlugin From f392c8788b47d247b2c244f92b2d2ff26bfb8b14 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 17 Nov 2025 12:54:43 -0500 Subject: [PATCH 04/16] pan porcessor changes" --- packages/dd-trace/src/llmobs/span_processor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dd-trace/src/llmobs/span_processor.js b/packages/dd-trace/src/llmobs/span_processor.js index e68f4e976b2..f415e84392e 100644 --- a/packages/dd-trace/src/llmobs/span_processor.js +++ b/packages/dd-trace/src/llmobs/span_processor.js @@ -207,7 +207,7 @@ class LLMObsSpanProcessor { const seenObjects = new WeakSet([obj]) const isCircular = value => { - if (typeof value !== 'object') return false + if (value == null || typeof value !== 'object') return false if (seenObjects.has(value)) return true seenObjects.add(value) return false From 007b7cafcc927ea474e6e1478e35edf15ad6632c Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 17 Nov 2025 15:03:41 -0500 Subject: [PATCH 05/16] updating tagger to check data as null --- packages/dd-trace/src/llmobs/plugins/genai.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/dd-trace/src/llmobs/plugins/genai.js b/packages/dd-trace/src/llmobs/plugins/genai.js index a7ad5b85c6c..4d6dba82a74 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai.js +++ b/packages/dd-trace/src/llmobs/plugins/genai.js @@ -352,10 +352,11 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { // ============================================================================ _formatFunctionCallMessage (parts, functionCalls, role) { + console.log('formatFunctionCallMessage called!!!!!!!!!!', functionCalls) const toolCalls = functionCalls.map(part => ({ name: part.functionCall.name, arguments: part.functionCall.args, - toolId: part.functionCall.id, + toolId: part.functionCall.id || '', type: 'function_call' })) From 88446e53008e4075981e63ec6a93615907f2eab9 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Thu, 20 Nov 2025 15:04:12 -0500 Subject: [PATCH 06/16] adding logic to cover executable code --- packages/dd-trace/src/llmobs/plugins/genai.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/dd-trace/src/llmobs/plugins/genai.js b/packages/dd-trace/src/llmobs/plugins/genai.js index 4d6dba82a74..fa874969b46 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai.js +++ b/packages/dd-trace/src/llmobs/plugins/genai.js @@ -234,8 +234,10 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { const content = Array.isArray(candidate) ? candidate[0].content : candidate.content if (!content?.parts) continue - // Skip function calls in streaming (they're not accumulated) - if (content.parts.some(part => part.functionCall)) { + // Skip special cases in streaming (handle them as non-streaming) + if (content.parts.some(part => part.functionCall || + part.executableCode || + part.codeExecutionResult)) { messages.push(...this._formatNonStreamingCandidate(candidate)) continue } @@ -352,7 +354,6 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { // ============================================================================ _formatFunctionCallMessage (parts, functionCalls, role) { - console.log('formatFunctionCallMessage called!!!!!!!!!!', functionCalls) const toolCalls = functionCalls.map(part => ({ name: part.functionCall.name, arguments: part.functionCall.args, From 16c8542edf14410eb2621728b16ad3d81ff7c030 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Fri, 21 Nov 2025 10:53:13 -0500 Subject: [PATCH 07/16] removing end call in tracing plugin --- packages/datadog-plugin-google-genai/src/tracing.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/datadog-plugin-google-genai/src/tracing.js b/packages/datadog-plugin-google-genai/src/tracing.js index 456da0ecf89..3fca67a987e 100644 --- a/packages/datadog-plugin-google-genai/src/tracing.js +++ b/packages/datadog-plugin-google-genai/src/tracing.js @@ -37,13 +37,6 @@ class GenAiTracingPlugin extends TracingPlugin { ctx.currentStore.span.finish() } } - - end (ctx) { - const span = ctx.currentStore?.span - if (!span) return - - span.finish() - } } module.exports = GenAiTracingPlugin From 3f4280e63d19732e95aeb4e449903606f6796a7d Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Mon, 24 Nov 2025 10:24:27 -0500 Subject: [PATCH 08/16] pr review changes --- .../datadog-instrumentations/src/genai.js | 55 +++++++++---------- .../src/tracing.js | 14 +++-- packages/dd-trace/src/llmobs/plugins/genai.js | 36 +++++------- 3 files changed, 48 insertions(+), 57 deletions(-) diff --git a/packages/datadog-instrumentations/src/genai.js b/packages/datadog-instrumentations/src/genai.js index c9db26a46fe..8b80982dbbc 100644 --- a/packages/datadog-instrumentations/src/genai.js +++ b/packages/datadog-instrumentations/src/genai.js @@ -15,15 +15,9 @@ function wrapGenerateContent (method) { return original.apply(this, args) } - const inputs = args[0] const normalizedName = normalizeMethodName(method) - const ctx = { - methodName: normalizedName, - inputs, - args, - model: inputs?.model || 'unknown' - } + const ctx = { args, methodName: normalizedName } return genaiTracingChannel.start.runStores(ctx, () => { let result @@ -31,14 +25,15 @@ function wrapGenerateContent (method) { result = original.apply(this, arguments) } catch (error) { finish(ctx, null, error) + throw error + } finally { + genaiTracingChannel.end.publish(ctx) } - return result.then(response => { - if (method === 'generateContentStream') { + if (response[Symbol.asyncIterator]) { shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx)) } else { finish(ctx, response, null) - genaiTracingChannel.end.publish(ctx) } return response }).catch(error => { @@ -91,29 +86,29 @@ addHook({ versions: ['>=1.19.0'] }, exports => { // Wrap GoogleGenAI to intercept when it creates Models instances - if (exports.GoogleGenAI) { - shimmer.wrap(exports, 'GoogleGenAI', GoogleGenAI => { - return class extends GoogleGenAI { - constructor (...args) { - super(...args) - - // Wrap the models property after it's created - if (this.models) { - if (this.models.generateContent) { - shimmer.wrap(this.models, 'generateContent', wrapGenerateContent('generateContent')) - } - if (this.models.generateContentStream) { - shimmer.wrap(this.models, 'generateContentStream', wrapGenerateContent('generateContentStream')) - } - if (this.models.embedContent) { - shimmer.wrap(this.models, 'embedContent', wrapGenerateContent('embedContent')) - } + if (!exports.GoogleGenAI) return exports + + shimmer.wrap(exports, 'GoogleGenAI', GoogleGenAI => { + return class extends GoogleGenAI { + constructor (...args) { + super(...args) + + // We are patching the instance instead of the prototype because when it is compiled form + // typescript, the models property is not available on the prototype. + if (this.models) { + if (this.models.generateContent) { + shimmer.wrap(this.models, 'generateContent', wrapGenerateContent('generateContent')) + } + if (this.models.generateContentStream) { + shimmer.wrap(this.models, 'generateContentStream', wrapGenerateContent('generateContentStream')) + } + if (this.models.embedContent) { + shimmer.wrap(this.models, 'embedContent', wrapGenerateContent('embedContent')) } } } - }) - } - + } + }) return exports }) diff --git a/packages/datadog-plugin-google-genai/src/tracing.js b/packages/datadog-plugin-google-genai/src/tracing.js index 3fca67a987e..791cba28529 100644 --- a/packages/datadog-plugin-google-genai/src/tracing.js +++ b/packages/datadog-plugin-google-genai/src/tracing.js @@ -11,7 +11,10 @@ class GenAiTracingPlugin extends TracingPlugin { static get kind () { return 'client' } bindStart (ctx) { - const { methodName, inputs, promptText, model } = ctx + const { args, methodName } = ctx + + const inputs = args[0] + const model = inputs?.model || 'unknown' const service = this.serviceName({ pluginConfig: this.config }) @@ -30,12 +33,13 @@ class GenAiTracingPlugin extends TracingPlugin { } asyncEnd (ctx) { + const { span } = ctx.currentStore + if (!span) return + if (ctx.result) { - ctx.currentStore.span.setTag('google_genai.response.model', ctx.result.modelVersion || ctx.inputs?.model) - } - if (ctx.currentStore.span) { - ctx.currentStore.span.finish() + span.setTag('google_genai.response.model', ctx.result.modelVersion || ctx.inputs?.model) } + span.finish() } } diff --git a/packages/dd-trace/src/llmobs/plugins/genai.js b/packages/dd-trace/src/llmobs/plugins/genai.js index fa874969b46..b138286852c 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai.js +++ b/packages/dd-trace/src/llmobs/plugins/genai.js @@ -27,7 +27,7 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { if (!done) return // Aggregate streaming chunks into a single response - ctx.result = this._aggregateStreamingChunks(ctx.chunks) + ctx.result = this.#aggregateStreamingChunks(ctx.chunks) }) } @@ -36,12 +36,12 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { // ============================================================================ getLLMObsSpanRegisterOptions (ctx) { - const methodName = ctx.methodName + const { args, methodName } = ctx if (!methodName) return - const inputs = ctx.inputs + const inputs = args[0] const operation = getOperation(methodName) - const kind = operation === 'embedding' ? 'embedding' : 'llm' + const kind = operation return { modelProvider: 'google', @@ -52,17 +52,17 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { } setLLMObsTags (ctx) { + const { args, methodName } = ctx const span = ctx.currentStore?.span - const methodName = ctx.methodName if (!methodName) return - const inputs = ctx.inputs + const inputs = args[0] const response = ctx.result const error = !!span.context()._tags.error const operation = getOperation(methodName) - if (operation === 'chat') { + if (operation === 'llm') { this._tagGenerateContent(span, inputs, response, error, ctx.isStreaming) } else if (operation === 'embedding') { this._tagEmbedding(span, inputs, response, error) @@ -78,7 +78,7 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { // Streaming Utilities // ============================================================================ - _aggregateStreamingChunks (chunks) { + #aggregateStreamingChunks (chunks) { const response = { candidates: [] } for (const chunk of chunks) { @@ -103,6 +103,9 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { const inputMessages = this._formatInputMessages(inputs.contents) + const metadata = this._extractMetadata(config) + this._tagger.tagMetadata(span, metadata) + if (error) { this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }]) return @@ -111,8 +114,6 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { const outputMessages = this._formatOutputMessages(response, isStreaming) this._tagger.tagLLMIO(span, inputMessages, outputMessages) - const metadata = this._extractMetadata(config) - this._tagger.tagMetadata(span, metadata) } _tagEmbedding (span, inputs, response, error) { @@ -125,7 +126,6 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { const embeddingOutput = this._formatEmbeddingOutput(response) this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) - this._tagger.tagMetadata(span, {}) } // ============================================================================ @@ -235,8 +235,8 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { if (!content?.parts) continue // Skip special cases in streaming (handle them as non-streaming) - if (content.parts.some(part => part.functionCall || - part.executableCode || + if (content.parts.some(part => part.functionCall || + part.executableCode || part.codeExecutionResult)) { messages.push(...this._formatNonStreamingCandidate(candidate)) continue @@ -505,15 +505,7 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { // ============================================================================ function getOperation (methodName) { - if (!methodName) return 'unknown' - - if (methodName.includes('embed')) { - return 'embedding' - } else if (methodName.includes('generate')) { - return 'chat' - } - - return 'unknown' + return methodName.includes('embed') ? 'embedding' : 'llm' } module.exports = GenAiLLMObsPlugin From 17b652b3ddf8d54ef792ff82ef57fc4c3bea0eb7 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 3 Dec 2025 09:39:00 -0500 Subject: [PATCH 09/16] creating a util file for function helpers --- packages/dd-trace/src/llmobs/plugins/genai.js | 511 ------------------ .../src/llmobs/plugins/genai/index.js | 113 ++++ .../dd-trace/src/llmobs/plugins/genai/util.js | 499 +++++++++++++++++ 3 files changed, 612 insertions(+), 511 deletions(-) delete mode 100644 packages/dd-trace/src/llmobs/plugins/genai.js create mode 100644 packages/dd-trace/src/llmobs/plugins/genai/index.js create mode 100644 packages/dd-trace/src/llmobs/plugins/genai/util.js diff --git a/packages/dd-trace/src/llmobs/plugins/genai.js b/packages/dd-trace/src/llmobs/plugins/genai.js deleted file mode 100644 index b138286852c..00000000000 --- a/packages/dd-trace/src/llmobs/plugins/genai.js +++ /dev/null @@ -1,511 +0,0 @@ -'use strict' - -const LLMObsPlugin = require('./base') - -// Constants for role mapping -const ROLES = { - MODEL: 'model', - ASSISTANT: 'assistant', - USER: 'user', - REASONING: 'reasoning' -} - -class GenAiLLMObsPlugin extends LLMObsPlugin { - static id = 'genai' - static integration = 'genai' - static prefix = 'tracing:apm:google:genai:request' - - constructor () { - super(...arguments) - - // Subscribe to streaming chunk events - this.addSub('apm:google:genai:request:chunk', ({ ctx, chunk, done }) => { - ctx.isStreaming = true - ctx.chunks = ctx.chunks || [] - - if (chunk) ctx.chunks.push(chunk) - if (!done) return - - // Aggregate streaming chunks into a single response - ctx.result = this.#aggregateStreamingChunks(ctx.chunks) - }) - } - - // ============================================================================ - // Public API Methods - // ============================================================================ - - getLLMObsSpanRegisterOptions (ctx) { - const { args, methodName } = ctx - if (!methodName) return - - const inputs = args[0] - const operation = getOperation(methodName) - const kind = operation - - return { - modelProvider: 'google', - modelName: inputs.model, - kind, - name: 'google_genai.request' - } - } - - setLLMObsTags (ctx) { - const { args, methodName } = ctx - const span = ctx.currentStore?.span - if (!methodName) return - - const inputs = args[0] - const response = ctx.result - const error = !!span.context()._tags.error - - const operation = getOperation(methodName) - - if (operation === 'llm') { - this._tagGenerateContent(span, inputs, response, error, ctx.isStreaming) - } else if (operation === 'embedding') { - this._tagEmbedding(span, inputs, response, error) - } - - if (!error && response) { - const metrics = this._extractMetrics(response) - this._tagger.tagMetrics(span, metrics) - } - } - - // ============================================================================ - // Streaming Utilities - // ============================================================================ - - #aggregateStreamingChunks (chunks) { - const response = { candidates: [] } - - for (const chunk of chunks) { - if (chunk.candidates) { - // Flatten candidates array - response.candidates.push(...chunk.candidates) - } - if (chunk.usageMetadata) { - response.usageMetadata = chunk.usageMetadata - } - } - - return response - } - - // ============================================================================ - // Tagging Methods - // ============================================================================ - - _tagGenerateContent (span, inputs, response, error, isStreaming = false) { - const { config = {} } = inputs - - const inputMessages = this._formatInputMessages(inputs.contents) - - const metadata = this._extractMetadata(config) - this._tagger.tagMetadata(span, metadata) - - if (error) { - this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }]) - return - } - - const outputMessages = this._formatOutputMessages(response, isStreaming) - this._tagger.tagLLMIO(span, inputMessages, outputMessages) - - } - - _tagEmbedding (span, inputs, response, error) { - const embeddingInput = this._formatEmbeddingInput(inputs.contents) - - if (error) { - this._tagger.tagEmbeddingIO(span, embeddingInput) - return - } - - const embeddingOutput = this._formatEmbeddingOutput(response) - this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) - } - - // ============================================================================ - // Input Formatting - // ============================================================================ - - _formatInputMessages (contents) { - if (!contents) return [] - - const contentArray = Array.isArray(contents) ? contents : [contents] - const messages = [] - - for (const content of contentArray) { - if (typeof content === 'string') { - messages.push({ role: ROLES.USER, content }) - } else if (content.text) { - messages.push({ role: ROLES.USER, content: content.text }) - } else if (content.parts) { - const message = this._formatContentObject(content) - if (message) messages.push(message) - } else { - messages.push({ role: ROLES.USER, content: JSON.stringify(content) }) - } - } - - return messages - } - - _formatContentObject (content) { - const parts = content.parts || [] - const role = this._determineRole(content, parts) - - // Check if this is a thought/reasoning part - if (this._hasThoughtParts(parts)) { - return { - role: ROLES.REASONING, - content: this._extractTextParts(parts).join('\n') - } - } - - // Check for function calls - const functionCalls = parts.filter(part => part.functionCall) - if (functionCalls.length > 0) { - return this._formatFunctionCallMessage(parts, functionCalls, role) - } - - // Check for function responses - const functionResponses = parts.filter(part => part.functionResponse) - if (functionResponses.length > 0) { - return this._formatFunctionResponseMessage(functionResponses, role) - } - - // Regular text content - return { - role, - content: this._extractTextParts(parts).join('\n') - } - } - - _formatEmbeddingInput (contents) { - if (!contents) return [] - - const contentArray = Array.isArray(contents) ? contents : [contents] - const documents = [] - - for (const content of contentArray) { - if (typeof content === 'string') { - documents.push({ text: content }) - } else if (content.text) { - documents.push({ text: content.text }) - } else if (content.parts) { - for (const part of content.parts) { - if (typeof part === 'string') { - documents.push({ text: part }) - } else if (part.text) { - documents.push({ text: part.text }) - } - } - } - } - - return documents - } - - // ============================================================================ - // Output Formatting - // ============================================================================ - - _formatOutputMessages (response, isStreaming = false) { - if (!response?.candidates?.length) { - return [{ content: '' }] - } - - if (isStreaming) { - return this._formatStreamingOutput(response) - } - - return this._formatNonStreamingOutput(response) - } - - _formatStreamingOutput (response) { - const messages = [] - const messagesByRole = new Map() - - for (const candidate of response.candidates) { - const content = Array.isArray(candidate) ? candidate[0].content : candidate.content - if (!content?.parts) continue - - // Skip special cases in streaming (handle them as non-streaming) - if (content.parts.some(part => part.functionCall || - part.executableCode || - part.codeExecutionResult)) { - messages.push(...this._formatNonStreamingCandidate(candidate)) - continue - } - - // Accumulate text parts by role - const partsByRole = this._groupPartsByRole(content.parts) - - for (const [partRole, textContent] of Object.entries(partsByRole)) { - if (!textContent) continue - - if (messagesByRole.has(partRole)) { - const index = messagesByRole.get(partRole) - messages[index].content += textContent - } else { - const messageIndex = messages.length - messages.push({ role: partRole, content: textContent }) - messagesByRole.set(partRole, messageIndex) - } - } - } - - return messages.length > 0 ? messages : [{ content: '' }] - } - - _formatNonStreamingOutput (response) { - const messages = [] - - for (const candidate of response.candidates) { - messages.push(...this._formatNonStreamingCandidate(candidate)) - } - - return messages.length > 0 ? messages : [{ content: '' }] - } - - _formatNonStreamingCandidate (candidate) { - const messages = [] - const content = Array.isArray(candidate) ? candidate[0].content : candidate.content - - if (!content?.parts) return messages - - const { parts } = content - - // Check for function calls - const functionCalls = parts.filter(part => part.functionCall) - if (functionCalls.length > 0) { - messages.push(this._formatFunctionCallMessage(parts, functionCalls, ROLES.ASSISTANT)) - return messages - } - - // Check for executable code - const executableCode = parts.find(part => part.executableCode) - if (executableCode) { - messages.push({ - role: ROLES.ASSISTANT, - content: JSON.stringify({ - language: executableCode.executableCode.language, - code: executableCode.executableCode.code - }) - }) - return messages - } - - // Check for code execution result - const codeExecutionResult = parts.find(part => part.codeExecutionResult) - if (codeExecutionResult) { - messages.push({ - role: ROLES.ASSISTANT, - content: JSON.stringify({ - outcome: codeExecutionResult.codeExecutionResult.outcome, - output: codeExecutionResult.codeExecutionResult.output - }) - }) - return messages - } - - // Regular text content - may contain both reasoning and assistant parts - const partsByRole = this._groupPartsByRole(parts) - - if (partsByRole.reasoning) { - messages.push({ - role: ROLES.REASONING, - content: partsByRole.reasoning - }) - } - - if (partsByRole.assistant) { - messages.push({ - role: ROLES.ASSISTANT, - content: partsByRole.assistant - }) - } - - return messages - } - - _formatEmbeddingOutput (response) { - if (!response?.embeddings?.length) { - return '' - } - - const embeddingCount = response.embeddings.length - const firstEmbedding = response.embeddings[0] - - if (firstEmbedding.values && Array.isArray(firstEmbedding.values)) { - const embeddingDim = firstEmbedding.values.length - return `[${embeddingCount} embedding(s) returned with size ${embeddingDim}]` - } - - return `[${embeddingCount} embedding(s) returned]` - } - - // ============================================================================ - // Function Call/Response Formatting - // ============================================================================ - - _formatFunctionCallMessage (parts, functionCalls, role) { - const toolCalls = functionCalls.map(part => ({ - name: part.functionCall.name, - arguments: part.functionCall.args, - toolId: part.functionCall.id || '', - type: 'function_call' - })) - - const textParts = this._extractTextParts(parts) - const content = textParts.length > 0 ? textParts.join('\n') : undefined - - return { - role, - ...(content && { content }), - toolCalls - } - } - - _formatFunctionResponseMessage (functionResponses, role) { - const toolResults = functionResponses.map(part => ({ - name: part.functionResponse.name, - result: JSON.stringify(part.functionResponse.response), - toolId: part.functionResponse.id, - type: 'function_response' - })) - - return { - role, - toolResults - } - } - - // ============================================================================ - // Part Processing Utilities - // ============================================================================ - - _extractTextParts (parts) { - return parts - .filter(part => part.text) - .map(part => part.text) - } - - _groupPartsByRole (parts) { - const grouped = { - reasoning: '', - assistant: '' - } - - for (const part of parts) { - if (!part.text) continue - - if (part.thought === true) { - grouped.reasoning += part.text - } else { - grouped.assistant += part.text - } - } - - return grouped - } - - _hasThoughtParts (parts) { - return parts.some(part => part.thought === true) - } - - // ============================================================================ - // Role Utilities - // ============================================================================ - - _determineRole (candidate, parts = []) { - // Check parts for thought indicators - if (this._hasThoughtParts(parts)) { - return ROLES.REASONING - } - - // Extract role from various possible locations - const rawRole = candidate.role || - candidate.content?.role || - candidate[0]?.content?.role - - return this._normalizeRole(rawRole) - } - - _normalizeRole (role) { - if (role === ROLES.MODEL) return ROLES.ASSISTANT - if (role === ROLES.ASSISTANT) return ROLES.ASSISTANT - if (role === ROLES.USER) return ROLES.USER - if (role === ROLES.REASONING) return ROLES.REASONING - return ROLES.USER // default - } - - // ============================================================================ - // Extraction Utilities - // ============================================================================ - - _extractMetrics (response) { - const metrics = {} - const tokenUsage = response.usageMetadata - - if (!tokenUsage) return metrics - - if (tokenUsage.promptTokenCount) { - metrics.inputTokens = tokenUsage.promptTokenCount - } - - if (tokenUsage.candidatesTokenCount) { - metrics.outputTokens = tokenUsage.candidatesTokenCount - } - - const totalTokens = tokenUsage.totalTokenCount || - (tokenUsage.promptTokenCount || 0) + (tokenUsage.candidatesTokenCount || 0) - if (totalTokens) { - metrics.totalTokens = totalTokens - } - - return metrics - } - - _extractMetadata (config) { - if (!config) return {} - - const fieldMap = { - temperature: 'temperature', - top_p: 'topP', - top_k: 'topK', - candidate_count: 'candidateCount', - max_output_tokens: 'maxOutputTokens', - stop_sequences: 'stopSequences', - response_logprobs: 'responseLogprobs', - logprobs: 'logprobs', - presence_penalty: 'presencePenalty', - frequency_penalty: 'frequencyPenalty', - seed: 'seed', - response_mime_type: 'responseMimeType', - safety_settings: 'safetySettings', - automatic_function_calling: 'automaticFunctionCalling' - } - - const metadata = {} - for (const [metadataKey, configKey] of Object.entries(fieldMap)) { - metadata[metadataKey] = config[configKey] ?? null - } - - return metadata - } -} - -// ============================================================================ -// Module-level Utilities -// ============================================================================ - -function getOperation (methodName) { - return methodName.includes('embed') ? 'embedding' : 'llm' -} - -module.exports = GenAiLLMObsPlugin diff --git a/packages/dd-trace/src/llmobs/plugins/genai/index.js b/packages/dd-trace/src/llmobs/plugins/genai/index.js new file mode 100644 index 00000000000..6832a884ca3 --- /dev/null +++ b/packages/dd-trace/src/llmobs/plugins/genai/index.js @@ -0,0 +1,113 @@ +'use strict' + +const LLMObsPlugin = require('../base') +const { + getOperation, + extractMetrics, + extractMetadata, + aggregateStreamingChunks, + formatInputMessages, + formatEmbeddingInput, + formatOutputMessages, + formatEmbeddingOutput +} = require('./util') + +class GenAiLLMObsPlugin extends LLMObsPlugin { + static id = 'genai' + static integration = 'genai' + static prefix = 'tracing:apm:google:genai:request' + + constructor () { + super(...arguments) + + // Subscribe to streaming chunk events + this.addSub('apm:google:genai:request:chunk', ({ ctx, chunk, done }) => { + ctx.isStreaming = true + ctx.chunks = ctx.chunks || [] + + if (chunk) ctx.chunks.push(chunk) + if (!done) return + + // Aggregate streaming chunks into a single response + ctx.result = aggregateStreamingChunks(ctx.chunks) + }) + } + + // ============================================================================ + // Public API Methods + // ============================================================================ + + getLLMObsSpanRegisterOptions (ctx) { + const { args, methodName } = ctx + if (!methodName) return + + const inputs = args[0] + const operation = getOperation(methodName) + const kind = operation + + return { + modelProvider: 'google', + modelName: inputs.model, + kind, + name: 'google_genai.request' + } + } + + setLLMObsTags (ctx) { + const { args, methodName } = ctx + const span = ctx.currentStore?.span + if (!methodName) return + + const inputs = args[0] + const response = ctx.result + const error = !!span.context()._tags.error + + const operation = getOperation(methodName) + + if (operation === 'llm') { + this.#tagGenerateContent(span, inputs, response, error, ctx.isStreaming) + } else if (operation === 'embedding') { + this.#tagEmbedding(span, inputs, response, error) + } + + if (!error && response) { + const metrics = extractMetrics(response) + this._tagger.tagMetrics(span, metrics) + } + } + + // ============================================================================ + // Tagging Methods + // ============================================================================ + + #tagGenerateContent (span, inputs, response, error, isStreaming = false) { + const { config = {} } = inputs + + const inputMessages = formatInputMessages(inputs.contents) + + const metadata = extractMetadata(config) + this._tagger.tagMetadata(span, metadata) + + if (error) { + this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }]) + return + } + + const outputMessages = formatOutputMessages(response, isStreaming) + this._tagger.tagLLMIO(span, inputMessages, outputMessages) + } + + #tagEmbedding (span, inputs, response, error) { + const embeddingInput = formatEmbeddingInput(inputs.contents) + + if (error) { + this._tagger.tagEmbeddingIO(span, embeddingInput) + return + } + + const embeddingOutput = formatEmbeddingOutput(response) + this._tagger.tagEmbeddingIO(span, embeddingInput, embeddingOutput) + } +} + +module.exports = GenAiLLMObsPlugin diff --git a/packages/dd-trace/src/llmobs/plugins/genai/util.js b/packages/dd-trace/src/llmobs/plugins/genai/util.js new file mode 100644 index 00000000000..df61a35287a --- /dev/null +++ b/packages/dd-trace/src/llmobs/plugins/genai/util.js @@ -0,0 +1,499 @@ +'use strict' + +// Constants for role mapping +const ROLES = { + MODEL: 'model', + ASSISTANT: 'assistant', + USER: 'user', + REASONING: 'reasoning' +} + +/** + * Get the operation type from the method name + * @param {string} methodName + * @returns {'embedding' | 'llm'} + */ +function getOperation (methodName) { + return methodName.includes('embed') ? 'embedding' : 'llm' +} + +/** + * Extract text parts from an array of parts + * @param {Array<{text?: string}>} parts + * @returns {string[]} + */ +function extractTextParts (parts) { + return parts + .filter(part => part.text) + .map(part => part.text) +} + +/** + * Group parts by role (reasoning vs assistant) + * @param {Array<{text?: string, thought?: boolean}>} parts + * @returns {{reasoning: string, assistant: string}} + */ +function groupPartsByRole (parts) { + const grouped = { + reasoning: '', + assistant: '' + } + + for (const part of parts) { + if (!part.text) continue + + if (part.thought === true) { + grouped.reasoning += part.text + } else { + grouped.assistant += part.text + } + } + + return grouped +} + +/** + * Check if parts contain thought/reasoning content + * @param {Array<{thought?: boolean}>} parts + * @returns {boolean} + */ +function hasThoughtParts (parts) { + return parts.some(part => part.thought === true) +} + +/** + * Determine the role from a candidate and its parts + * @param {object} candidate + * @param {Array<{thought?: boolean}>} parts + * @returns {string} + */ +function determineRole (candidate, parts = []) { + // Check parts for thought indicators + if (hasThoughtParts(parts)) { + return ROLES.REASONING + } + + // Extract role from various possible locations + const rawRole = candidate.role || + candidate.content?.role || + candidate[0]?.content?.role + + return normalizeRole(rawRole) +} + +/** + * Normalize role to standard values + * @param {string} role + * @returns {string} + */ +function normalizeRole (role) { + if (role === ROLES.MODEL) return ROLES.ASSISTANT + if (role === ROLES.ASSISTANT) return ROLES.ASSISTANT + if (role === ROLES.USER) return ROLES.USER + if (role === ROLES.REASONING) return ROLES.REASONING + return ROLES.USER // default +} + +/** + * Extract metrics from response + * @param {object} response + * @returns {object} + */ +function extractMetrics (response) { + const metrics = {} + const tokenUsage = response.usageMetadata + + if (!tokenUsage) return metrics + + if (tokenUsage.promptTokenCount) { + metrics.inputTokens = tokenUsage.promptTokenCount + } + + if (tokenUsage.candidatesTokenCount) { + metrics.outputTokens = tokenUsage.candidatesTokenCount + } + + const totalTokens = tokenUsage.totalTokenCount || + (tokenUsage.promptTokenCount || 0) + (tokenUsage.candidatesTokenCount || 0) + if (totalTokens) { + metrics.totalTokens = totalTokens + } + + return metrics +} + +/** + * Extract metadata from config + * @param {object} config + * @returns {object} + */ +function extractMetadata (config) { + if (!config) return {} + + const fieldMap = { + temperature: 'temperature', + top_p: 'topP', + top_k: 'topK', + candidate_count: 'candidateCount', + max_output_tokens: 'maxOutputTokens', + stop_sequences: 'stopSequences', + response_logprobs: 'responseLogprobs', + logprobs: 'logprobs', + presence_penalty: 'presencePenalty', + frequency_penalty: 'frequencyPenalty', + seed: 'seed', + response_mime_type: 'responseMimeType', + safety_settings: 'safetySettings', + automatic_function_calling: 'automaticFunctionCalling' + } + + const metadata = {} + for (const [metadataKey, configKey] of Object.entries(fieldMap)) { + metadata[metadataKey] = config[configKey] ?? null + } + + return metadata +} + +/** + * Format function call message + * @param {Array} parts + * @param {Array} functionCalls + * @param {string} role + * @returns {object} + */ +function formatFunctionCallMessage (parts, functionCalls, role) { + const toolCalls = functionCalls.map(part => ({ + name: part.functionCall.name, + arguments: part.functionCall.args, + toolId: part.functionCall.id || '', + type: 'function_call' + })) + + const textParts = extractTextParts(parts) + const content = textParts.length > 0 ? textParts.join('\n') : undefined + + return { + role, + ...(content && { content }), + toolCalls + } +} + +/** + * Format function response message + * @param {Array} functionResponses + * @param {string} role + * @returns {object} + */ +function formatFunctionResponseMessage (functionResponses, role) { + const toolResults = functionResponses.map(part => ({ + name: part.functionResponse.name, + result: JSON.stringify(part.functionResponse.response), + toolId: part.functionResponse.id, + type: 'function_response' + })) + + return { + role, + toolResults + } +} + +/** + * Aggregate streaming chunks into a single response + * @param {Array} chunks + * @returns {object} + */ +function aggregateStreamingChunks (chunks) { + const response = { candidates: [] } + + for (const chunk of chunks) { + if (chunk.candidates) { + // Flatten candidates array + response.candidates.push(...chunk.candidates) + } + if (chunk.usageMetadata) { + response.usageMetadata = chunk.usageMetadata + } + } + + return response +} + +/** + * Format a content object into a message + * @param {object} content + * @returns {object} + */ +function formatContentObject (content) { + const parts = content.parts || [] + const role = determineRole(content, parts) + + // Check if this is a thought/reasoning part + if (hasThoughtParts(parts)) { + return { + role: ROLES.REASONING, + content: extractTextParts(parts).join('\n') + } + } + + // Check for function calls + const functionCalls = parts.filter(part => part.functionCall) + if (functionCalls.length > 0) { + return formatFunctionCallMessage(parts, functionCalls, role) + } + + // Check for function responses + const functionResponses = parts.filter(part => part.functionResponse) + if (functionResponses.length > 0) { + return formatFunctionResponseMessage(functionResponses, role) + } + + // Regular text content + return { + role, + content: extractTextParts(parts).join('\n') + } +} + +/** + * Format input messages from contents + * @param {*} contents + * @returns {Array} + */ +function formatInputMessages (contents) { + if (!contents) return [] + + const contentArray = Array.isArray(contents) ? contents : [contents] + const messages = [] + + for (const content of contentArray) { + if (typeof content === 'string') { + messages.push({ role: ROLES.USER, content }) + } else if (content.text) { + messages.push({ role: ROLES.USER, content: content.text }) + } else if (content.parts) { + const message = formatContentObject(content) + if (message) messages.push(message) + } else { + messages.push({ role: ROLES.USER, content: JSON.stringify(content) }) + } + } + + return messages +} + +/** + * Format embedding input from contents + * @param {*} contents + * @returns {Array} + */ +function formatEmbeddingInput (contents) { + if (!contents) return [] + + const contentArray = Array.isArray(contents) ? contents : [contents] + const documents = [] + + for (const content of contentArray) { + if (typeof content === 'string') { + documents.push({ text: content }) + } else if (content.text) { + documents.push({ text: content.text }) + } else if (content.parts) { + for (const part of content.parts) { + if (typeof part === 'string') { + documents.push({ text: part }) + } else if (part.text) { + documents.push({ text: part.text }) + } + } + } + } + + return documents +} + +/** + * Format a non-streaming candidate into messages + * @param {object} candidate + * @returns {Array} + */ +function formatNonStreamingCandidate (candidate) { + const messages = [] + const content = Array.isArray(candidate) ? candidate[0].content : candidate.content + + if (!content?.parts) return messages + + const { parts } = content + + // Check for function calls + const functionCalls = parts.filter(part => part.functionCall) + if (functionCalls.length > 0) { + messages.push(formatFunctionCallMessage(parts, functionCalls, ROLES.ASSISTANT)) + return messages + } + + // Check for executable code + const executableCode = parts.find(part => part.executableCode) + if (executableCode) { + messages.push({ + role: ROLES.ASSISTANT, + content: JSON.stringify({ + language: executableCode.executableCode.language, + code: executableCode.executableCode.code + }) + }) + return messages + } + + // Check for code execution result + const codeExecutionResult = parts.find(part => part.codeExecutionResult) + if (codeExecutionResult) { + messages.push({ + role: ROLES.ASSISTANT, + content: JSON.stringify({ + outcome: codeExecutionResult.codeExecutionResult.outcome, + output: codeExecutionResult.codeExecutionResult.output + }) + }) + return messages + } + + // Regular text content - may contain both reasoning and assistant parts + const partsByRole = groupPartsByRole(parts) + + if (partsByRole.reasoning) { + messages.push({ + role: ROLES.REASONING, + content: partsByRole.reasoning + }) + } + + if (partsByRole.assistant) { + messages.push({ + role: ROLES.ASSISTANT, + content: partsByRole.assistant + }) + } + + return messages +} + +/** + * Format streaming output from response + * @param {object} response + * @returns {Array} + */ +function formatStreamingOutput (response) { + const messages = [] + const messagesByRole = new Map() + + for (const candidate of response.candidates) { + const content = Array.isArray(candidate) ? candidate[0].content : candidate.content + if (!content?.parts) continue + + // Skip special cases in streaming (handle them as non-streaming) + if (content.parts.some(part => part.functionCall || + part.executableCode || + part.codeExecutionResult)) { + messages.push(...formatNonStreamingCandidate(candidate)) + continue + } + + // Accumulate text parts by role + const partsByRole = groupPartsByRole(content.parts) + + for (const [partRole, textContent] of Object.entries(partsByRole)) { + if (!textContent) continue + + if (messagesByRole.has(partRole)) { + const index = messagesByRole.get(partRole) + messages[index].content += textContent + } else { + const messageIndex = messages.length + messages.push({ role: partRole, content: textContent }) + messagesByRole.set(partRole, messageIndex) + } + } + } + + return messages.length > 0 ? messages : [{ content: '' }] +} + +/** + * Format non-streaming output from response + * @param {object} response + * @returns {Array} + */ +function formatNonStreamingOutput (response) { + const messages = [] + + for (const candidate of response.candidates) { + messages.push(...formatNonStreamingCandidate(candidate)) + } + + return messages.length > 0 ? messages : [{ content: '' }] +} + +/** + * Format output messages from response + * @param {object} response + * @param {boolean} isStreaming + * @returns {Array} + */ +function formatOutputMessages (response, isStreaming = false) { + if (!response?.candidates?.length) { + return [{ content: '' }] + } + + if (isStreaming) { + return formatStreamingOutput(response) + } + + return formatNonStreamingOutput(response) +} + +/** + * Format embedding output from response + * @param {object} response + * @returns {string} + */ +function formatEmbeddingOutput (response) { + if (!response?.embeddings?.length) { + return '' + } + + const embeddingCount = response.embeddings.length + const firstEmbedding = response.embeddings[0] + + if (firstEmbedding.values && Array.isArray(firstEmbedding.values)) { + const embeddingDim = firstEmbedding.values.length + return `[${embeddingCount} embedding(s) returned with size ${embeddingDim}]` + } + + return `[${embeddingCount} embedding(s) returned]` +} + +module.exports = { + ROLES, + getOperation, + extractTextParts, + groupPartsByRole, + hasThoughtParts, + determineRole, + normalizeRole, + extractMetrics, + extractMetadata, + formatFunctionCallMessage, + formatFunctionResponseMessage, + aggregateStreamingChunks, + formatContentObject, + formatInputMessages, + formatEmbeddingInput, + formatNonStreamingCandidate, + formatStreamingOutput, + formatNonStreamingOutput, + formatOutputMessages, + formatEmbeddingOutput +} From c2d3f8e3b30637ce630a6f434a2bbf33174b2912 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 3 Dec 2025 09:58:32 -0500 Subject: [PATCH 10/16] updating genai plugin name --- .../datadog-instrumentations/src/{genai.js => google-genai.js} | 2 +- packages/datadog-instrumentations/src/helpers/hooks.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/datadog-instrumentations/src/{genai.js => google-genai.js} (99%) diff --git a/packages/datadog-instrumentations/src/genai.js b/packages/datadog-instrumentations/src/google-genai.js similarity index 99% rename from packages/datadog-instrumentations/src/genai.js rename to packages/datadog-instrumentations/src/google-genai.js index 8b80982dbbc..5943448f255 100644 --- a/packages/datadog-instrumentations/src/genai.js +++ b/packages/datadog-instrumentations/src/google-genai.js @@ -93,7 +93,7 @@ addHook({ constructor (...args) { super(...args) - // We are patching the instance instead of the prototype because when it is compiled form + // We are patching the instance instead of the prototype because when it is compiled from // typescript, the models property is not available on the prototype. if (this.models) { if (this.models.generateContent) { diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 53f1c008f38..6006cce3c34 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -13,7 +13,7 @@ module.exports = { '@playwright/test': () => require('../playwright'), '@elastic/elasticsearch': () => require('../elasticsearch'), '@elastic/transport': () => require('../elasticsearch'), - '@google/genai': () => require('../genai'), + '@google/genai': () => require('../google-genai'), '@google-cloud/pubsub': () => require('../google-cloud-pubsub'), '@google-cloud/vertexai': () => require('../google-cloud-vertexai'), '@graphql-tools/executor': () => require('../graphql'), From c596e068e9cfd02bf8a8c78bcfdc1938a02b5a6f Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 3 Dec 2025 10:11:28 -0500 Subject: [PATCH 11/16] fixing plugin id --- packages/datadog-plugin-google-genai/src/tracing.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/datadog-plugin-google-genai/src/tracing.js b/packages/datadog-plugin-google-genai/src/tracing.js index 791cba28529..36070769fbb 100644 --- a/packages/datadog-plugin-google-genai/src/tracing.js +++ b/packages/datadog-plugin-google-genai/src/tracing.js @@ -3,7 +3,7 @@ const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js') class GenAiTracingPlugin extends TracingPlugin { - static id = 'genai' + static id = 'google-genai' static operation = 'request' static prefix = 'tracing:apm:google:genai:request' From 14566d39f3d088d40c15ab61364d33655b2e9389 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 3 Dec 2025 14:44:22 -0500 Subject: [PATCH 12/16] updating unit tests --- .github/workflows/llmobs.yml | 27 +++ .../datadog-plugin-google-genai/src/index.js | 2 +- .../src/tracing.js | 7 +- .../test/index.spec.js | 160 ++++++++++++++++++ .../test/integration-test/client.spec.js | 50 ++++++ .../test/integration-test/server.mjs | 38 +++++ .../src/supported-configurations.json | 1 + .../test/plugins/versions/package.json | 1 + 8 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 packages/datadog-plugin-google-genai/test/index.spec.js create mode 100644 packages/datadog-plugin-google-genai/test/integration-test/client.spec.js create mode 100644 packages/datadog-plugin-google-genai/test/integration-test/server.mjs diff --git a/.github/workflows/llmobs.yml b/.github/workflows/llmobs.yml index df5f352573c..f2d956522ae 100644 --- a/.github/workflows/llmobs.yml +++ b/.github/workflows/llmobs.yml @@ -205,3 +205,30 @@ jobs: with: api_key: ${{ secrets.DD_API_KEY }} service: dd-trace-js-tests + + google-genai: + runs-on: ubuntu-latest + env: + PLUGINS: google-genai + steps: + - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: ./.github/actions/testagent/start + - uses: ./.github/actions/node/oldest-maintenance-lts + - uses: ./.github/actions/install + - run: yarn test:plugins:ci + - run: yarn test:llmobs:plugins:ci + shell: bash + - uses: ./.github/actions/node/latest + - run: yarn test:plugins:ci + - run: yarn test:llmobs:plugins:ci + shell: bash + - uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 + - if: always() + uses: ./.github/actions/testagent/logs + with: + suffix: llmobs-${{ github.job }} + - uses: DataDog/junit-upload-github-action@762867566348d59ac9bcf479ebb4ec040db8940a # v2.0.0 + if: always() + with: + api_key: ${{ secrets.DD_API_KEY }} + service: dd-trace-js-tests diff --git a/packages/datadog-plugin-google-genai/src/index.js b/packages/datadog-plugin-google-genai/src/index.js index 3f1243d9454..9d6a957098f 100644 --- a/packages/datadog-plugin-google-genai/src/index.js +++ b/packages/datadog-plugin-google-genai/src/index.js @@ -5,7 +5,7 @@ const GenAiTracingPlugin = require('./tracing') const GenAiLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/genai') class GenAiPlugin extends CompositePlugin { - static id = 'genai' + static id = 'google-genai' static get plugins () { return { llmobs: GenAiLLMObsPlugin, diff --git a/packages/datadog-plugin-google-genai/src/tracing.js b/packages/datadog-plugin-google-genai/src/tracing.js index 36070769fbb..a7d77239a01 100644 --- a/packages/datadog-plugin-google-genai/src/tracing.js +++ b/packages/datadog-plugin-google-genai/src/tracing.js @@ -16,14 +16,9 @@ class GenAiTracingPlugin extends TracingPlugin { const inputs = args[0] const model = inputs?.model || 'unknown' - const service = this.serviceName({ pluginConfig: this.config }) - this.startSpan('google_genai.request', { - service, - resource: methodName, - type: 'genai', - kind: 'client', meta: { + 'resource.name': methodName, 'google_genai.request.model': model, 'google_genai.request.provider': 'google' } diff --git a/packages/datadog-plugin-google-genai/test/index.spec.js b/packages/datadog-plugin-google-genai/test/index.spec.js new file mode 100644 index 00000000000..24a8c439ba3 --- /dev/null +++ b/packages/datadog-plugin-google-genai/test/index.spec.js @@ -0,0 +1,160 @@ +'use strict' + +const http = require('http') +const { describe, before, after, it } = require('mocha') +const { withVersions } = require('../../dd-trace/test/setup/mocha') +const agent = require('../../dd-trace/test/plugins/agent') +const assert = require('node:assert') +const { useEnv } = require('../../../integration-tests/helpers') + +const generateContentResponse = { + candidates: [{ + content: { + parts: [{ text: 'Hello! How can I help you today?' }], + role: 'model' + }, + finishReason: 'STOP' + }], + usageMetadata: { + promptTokenCount: 5, + candidatesTokenCount: 10, + totalTokenCount: 15 + }, + modelVersion: 'gemini-2.0-flash' +} + +const embedContentResponse = { + embedding: { + values: Array(768).fill(0.1) + } +} + +function createMockServer (port, callback) { + const server = http.createServer((req, res) => { + // Consume request body + req.on('data', () => {}) + req.on('end', () => { + res.setHeader('Content-Type', 'application/json') + + // Handle all API endpoints + if (req.url.includes(':embedContent')) { + res.end(JSON.stringify(embedContentResponse)) + } else if (req.url.includes(':streamGenerateContent')) { + // Google GenAI streaming uses Server-Sent Events format + res.setHeader('Content-Type', 'text/event-stream') + res.write('data: ' + JSON.stringify(generateContentResponse) + '\n\n') + res.end() + } else if (req.url.includes(':generateContent')) { + res.end(JSON.stringify(generateContentResponse)) + } else { + // Default response for any other endpoint + res.end(JSON.stringify(generateContentResponse)) + } + }) + }) + + server.listen(port, '127.0.0.1', () => callback(server)) + return server +} + +describe('Plugin', () => { + useEnv({ + GOOGLE_API_KEY: '' + }) + + withVersions('google-genai', '@google/genai', (version) => { + let client + let mockServer + let mockPort + + before(async () => { + await agent.load('google-genai') + + // Find an available port and start mock server + await new Promise((resolve) => { + mockServer = createMockServer(0, (server) => { + mockPort = server.address().port + resolve() + }) + }) + + const { GoogleGenAI } = require(`../../../versions/@google/genai@${version}`).get() + client = new GoogleGenAI({ + apiKey: '', + httpOptions: { baseUrl: `http://127.0.0.1:${mockPort}` } + }) + }) + + after(async () => { + if (mockServer) { + mockServer.close() + } + await agent.close({ ritmReset: false }) + }) + + describe('models.generateContent', () => { + it('creates a span', async () => { + const tracesPromise = agent.assertSomeTraces(traces => { + const span = traces[0][0] + + assert.equal(span.name, 'google_genai.request') + assert.equal(span.resource, 'Models.generate_content') + assert.equal(span.meta['google_genai.request.model'], 'gemini-2.0-flash') + }) + + const result = await client.models.generateContent({ + model: 'gemini-2.0-flash', + contents: 'Hello, world!' + }) + + assert.ok(result) + + await tracesPromise + }) + }) + + describe('models.generateContentStream', () => { + it('creates a span', async () => { + const tracesPromise = agent.assertSomeTraces(traces => { + const span = traces[0][0] + + assert.equal(span.name, 'google_genai.request') + assert.equal(span.resource, 'Models.generate_content_stream') + assert.equal(span.meta['google_genai.request.model'], 'gemini-2.0-flash') + }) + + const stream = await client.models.generateContentStream({ + model: 'gemini-2.0-flash', + contents: 'Hello, world!' + }) + + for await (const chunk of stream) { + assert.ok(chunk) + } + + await tracesPromise + }) + }) + + describe('models.embedContent', () => { + it('creates a span', async () => { + const tracesPromise = agent.assertSomeTraces(traces => { + const span = traces[0][0] + + assert.equal(span.name, 'google_genai.request') + assert.equal(span.resource, 'Models.embed_content') + assert.equal(span.meta['google_genai.request.model'], 'text-embedding-004') + }) + + const result = await client.models.embedContent({ + model: 'text-embedding-004', + contents: 'Hello, world!' + }) + + assert.ok(result) + + await tracesPromise + }) + }) + }) +}) diff --git a/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js b/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js new file mode 100644 index 00000000000..261752bf7f0 --- /dev/null +++ b/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js @@ -0,0 +1,50 @@ +'use strict' + +const { + FakeAgent, + sandboxCwd, + useSandbox, + checkSpansForServiceName, + spawnPluginIntegrationTestProc +} = require('../../../../integration-tests/helpers') +const { withVersions } = require('../../../dd-trace/test/setup/mocha') +const { assert } = require('chai') +const { describe, it, beforeEach, afterEach } = require('mocha') + +describe('esm', () => { + let agent + let proc + + withVersions('google-genai', ['@google/genai'], version => { + useSandbox([ + `@google/genai@${version}`, + ], false, [ + './packages/datadog-plugin-google-genai/test/integration-test/*' + ]) + + beforeEach(async () => { + agent = await new FakeAgent().start() + }) + + afterEach(async () => { + proc?.kill() + await agent.stop() + }) + + it('is instrumented', async () => { + const res = agent.assertMessageReceived(({ headers, payload }) => { + assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`) + assert.isArray(payload) + assert.strictEqual(checkSpansForServiceName(payload, 'google_genai.request'), true) + }) + + proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, null, { + NODE_OPTIONS: '--import dd-trace/initialize.mjs', + GOOGLE_API_KEY: '' + }) + + await res + }).timeout(20000) + }) +}) + diff --git a/packages/datadog-plugin-google-genai/test/integration-test/server.mjs b/packages/datadog-plugin-google-genai/test/integration-test/server.mjs new file mode 100644 index 00000000000..7276b522790 --- /dev/null +++ b/packages/datadog-plugin-google-genai/test/integration-test/server.mjs @@ -0,0 +1,38 @@ +import http from 'http' +import { GoogleGenAI } from '@google/genai' + +const mockResponse = { + candidates: [{ + content: { + parts: [{ text: 'Hello!' }], + role: 'model' + }, + finishReason: 'STOP' + }], + usageMetadata: { + promptTokenCount: 5, + candidatesTokenCount: 2, + totalTokenCount: 7 + } +} + +// Create a mock server +const mockServer = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(mockResponse)) +}) + +await new Promise(resolve => mockServer.listen(0, '127.0.0.1', resolve)) +const mockPort = mockServer.address().port + +const client = new GoogleGenAI({ + apiKey: '', + httpOptions: { baseUrl: `http://127.0.0.1:${mockPort}` } +}) + +await client.models.generateContent({ + model: 'gemini-2.0-flash', + contents: 'Hello, world!' +}) + +mockServer.close() diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 764dae9c92f..70b7ddb50c1 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -293,6 +293,7 @@ "DD_TRACE_GOOGLE_CLOUD_PUBSUB_ENABLED": ["A"], "DD_TRACE_GOOGLE_CLOUD_VERTEXAI_ENABLED": ["A"], "DD_TRACE_GOOGLE_GAX_ENABLED": ["A"], + "DD_TRACE_GOOGLE_GENAI_ENABLED": ["A"], "DD_TRACE_GRAPHQL_ENABLED": ["A"], "DD_TRACE_GRAPHQL_ERROR_EXTENSIONS": ["A"], "DD_TRACE_GRAPHQL_TAG_ENABLED": ["A"], diff --git a/packages/dd-trace/test/plugins/versions/package.json b/packages/dd-trace/test/plugins/versions/package.json index 684c09b5f7d..0c98c3260a0 100644 --- a/packages/dd-trace/test/plugins/versions/package.json +++ b/packages/dd-trace/test/plugins/versions/package.json @@ -32,6 +32,7 @@ "@fastify/cookie": "11.0.2", "@fastify/multipart": "9.3.0", "@fast-check/jest": "2.1.1", + "@google/genai": "1.19.0", "@google-cloud/pubsub": "5.2.0", "@google-cloud/vertexai": "1.10.0", "@graphql-tools/executor": "1.4.11", From c0cfdaf1e049c411e3dc468c069b0c2b0e14524d Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 3 Dec 2025 15:41:33 -0500 Subject: [PATCH 13/16] adding llm obs tests --- .../test/index.spec.js | 73 +------------------ .../test/integration-test/client.spec.js | 3 +- .../test/integration-test/server.mjs | 31 +------- packages/dd-trace/test/llmobs/util.js | 2 +- 4 files changed, 6 insertions(+), 103 deletions(-) diff --git a/packages/datadog-plugin-google-genai/test/index.spec.js b/packages/datadog-plugin-google-genai/test/index.spec.js index 24a8c439ba3..8e1c0433d15 100644 --- a/packages/datadog-plugin-google-genai/test/index.spec.js +++ b/packages/datadog-plugin-google-genai/test/index.spec.js @@ -1,94 +1,25 @@ 'use strict' -const http = require('http') const { describe, before, after, it } = require('mocha') const { withVersions } = require('../../dd-trace/test/setup/mocha') const agent = require('../../dd-trace/test/plugins/agent') const assert = require('node:assert') -const { useEnv } = require('../../../integration-tests/helpers') - -const generateContentResponse = { - candidates: [{ - content: { - parts: [{ text: 'Hello! How can I help you today?' }], - role: 'model' - }, - finishReason: 'STOP' - }], - usageMetadata: { - promptTokenCount: 5, - candidatesTokenCount: 10, - totalTokenCount: 15 - }, - modelVersion: 'gemini-2.0-flash' -} - -const embedContentResponse = { - embedding: { - values: Array(768).fill(0.1) - } -} - -function createMockServer (port, callback) { - const server = http.createServer((req, res) => { - // Consume request body - req.on('data', () => {}) - req.on('end', () => { - res.setHeader('Content-Type', 'application/json') - - // Handle all API endpoints - if (req.url.includes(':embedContent')) { - res.end(JSON.stringify(embedContentResponse)) - } else if (req.url.includes(':streamGenerateContent')) { - // Google GenAI streaming uses Server-Sent Events format - res.setHeader('Content-Type', 'text/event-stream') - res.write('data: ' + JSON.stringify(generateContentResponse) + '\n\n') - res.end() - } else if (req.url.includes(':generateContent')) { - res.end(JSON.stringify(generateContentResponse)) - } else { - // Default response for any other endpoint - res.end(JSON.stringify(generateContentResponse)) - } - }) - }) - - server.listen(port, '127.0.0.1', () => callback(server)) - return server -} describe('Plugin', () => { - useEnv({ - GOOGLE_API_KEY: '' - }) - withVersions('google-genai', '@google/genai', (version) => { let client - let mockServer - let mockPort before(async () => { await agent.load('google-genai') - // Find an available port and start mock server - await new Promise((resolve) => { - mockServer = createMockServer(0, (server) => { - mockPort = server.address().port - resolve() - }) - }) - const { GoogleGenAI } = require(`../../../versions/@google/genai@${version}`).get() client = new GoogleGenAI({ - apiKey: '', - httpOptions: { baseUrl: `http://127.0.0.1:${mockPort}` } + apiKey: process.env.GOOGLE_API_KEY || '', + httpOptions: { baseUrl: 'http://127.0.0.1:9126/vcr/genai' } }) }) after(async () => { - if (mockServer) { - mockServer.close() - } await agent.close({ ritmReset: false }) }) diff --git a/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js b/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js index 261752bf7f0..9a7764ad55c 100644 --- a/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js +++ b/packages/datadog-plugin-google-genai/test/integration-test/client.spec.js @@ -40,11 +40,10 @@ describe('esm', () => { proc = await spawnPluginIntegrationTestProc(sandboxCwd(), 'server.mjs', agent.port, null, { NODE_OPTIONS: '--import dd-trace/initialize.mjs', - GOOGLE_API_KEY: '' + GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || '' }) await res }).timeout(20000) }) }) - diff --git a/packages/datadog-plugin-google-genai/test/integration-test/server.mjs b/packages/datadog-plugin-google-genai/test/integration-test/server.mjs index 7276b522790..2bf4d1f7a91 100644 --- a/packages/datadog-plugin-google-genai/test/integration-test/server.mjs +++ b/packages/datadog-plugin-google-genai/test/integration-test/server.mjs @@ -1,38 +1,11 @@ -import http from 'http' import { GoogleGenAI } from '@google/genai' -const mockResponse = { - candidates: [{ - content: { - parts: [{ text: 'Hello!' }], - role: 'model' - }, - finishReason: 'STOP' - }], - usageMetadata: { - promptTokenCount: 5, - candidatesTokenCount: 2, - totalTokenCount: 7 - } -} - -// Create a mock server -const mockServer = http.createServer((req, res) => { - res.setHeader('Content-Type', 'application/json') - res.end(JSON.stringify(mockResponse)) -}) - -await new Promise(resolve => mockServer.listen(0, '127.0.0.1', resolve)) -const mockPort = mockServer.address().port - const client = new GoogleGenAI({ - apiKey: '', - httpOptions: { baseUrl: `http://127.0.0.1:${mockPort}` } + apiKey: process.env.GOOGLE_API_KEY, + httpOptions: { baseUrl: 'http://127.0.0.1:9126/vcr/genai' } }) await client.models.generateContent({ model: 'gemini-2.0-flash', contents: 'Hello, world!' }) - -mockServer.close() diff --git a/packages/dd-trace/test/llmobs/util.js b/packages/dd-trace/test/llmobs/util.js index 8e660d4e7e0..b05a0b9649a 100644 --- a/packages/dd-trace/test/llmobs/util.js +++ b/packages/dd-trace/test/llmobs/util.js @@ -65,7 +65,7 @@ function assertWithMockValues (actual, expected, key) { for (let i = 0; i < expected.length; i++) { assertWithMockValues(actual[i], expected[i], `${key}.${i}`) } - } else if (typeof expected === 'object') { + } else if (typeof expected === 'object' && expected !== null) { if (typeof actual !== 'object') { assert.fail(`${actualWithName} is not an object`) } From 2a8a13635ffb7cbcfda48f6f38edbb9a38f31f37 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Wed, 3 Dec 2025 15:48:25 -0500 Subject: [PATCH 14/16] adding in cassesttes for ci tests --- ...0-flash_generateContent_post_4504cf7c.yaml | 73 +++++ ...GenerateContent_alt_sse_post_06304696.yaml | 73 +++++ ...-004_batchEmbedContents_post_d02b671a.yaml | 257 ++++++++++++++++++ .../llmobs/plugins/google-genai/index.spec.js | 141 ++++++++++ 4 files changed, 544 insertions(+) create mode 100644 packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_generateContent_post_4504cf7c.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_streamGenerateContent_alt_sse_post_06304696.yaml create mode 100644 packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_text-embedding-004_batchEmbedContents_post_d02b671a.yaml create mode 100644 packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js diff --git a/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_generateContent_post_4504cf7c.yaml b/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_generateContent_post_4504cf7c.yaml new file mode 100644 index 00000000000..7a269294b81 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_generateContent_post_4504cf7c.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '{"contents":[{"parts":[{"text":"Hello, world!"}],"role":"user"}]}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '65' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - google-genai-sdk/1.19.0 gl-node/v23.10.0 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + ? !!python/object/apply:multidict._multidict.istr + - x-goog-api-client + : - google-genai-sdk/1.19.0 gl-node/v23.10.0 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent + response: + body: + string: "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": + [\n {\n \"text\": \"Hello there! How can I help you today?\\n\"\n + \ }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": + \"STOP\",\n \"avgLogprobs\": -0.014444851062514565\n }\n ],\n \"usageMetadata\": + {\n \"promptTokenCount\": 4,\n \"candidatesTokenCount\": 11,\n \"totalTokenCount\": + 15,\n \"promptTokensDetails\": [\n {\n \"modality\": \"TEXT\",\n + \ \"tokenCount\": 4\n }\n ],\n \"candidatesTokensDetails\": + [\n {\n \"modality\": \"TEXT\",\n \"tokenCount\": 11\n + \ }\n ]\n },\n \"modelVersion\": \"gemini-2.0-flash\",\n \"responseId\": + \"Xpcwab__A73f_uMPs9qB0A0\"\n}\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Wed, 03 Dec 2025 20:02:38 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=372 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_streamGenerateContent_alt_sse_post_06304696.yaml b/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_streamGenerateContent_alt_sse_post_06304696.yaml new file mode 100644 index 00000000000..35bd9c89243 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_gemini-2.0-flash_streamGenerateContent_alt_sse_post_06304696.yaml @@ -0,0 +1,73 @@ +interactions: +- request: + body: '{"contents":[{"parts":[{"text":"Hello, world!"}],"role":"user"}]}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '65' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - google-genai-sdk/1.19.0 gl-node/v23.10.0 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + ? !!python/object/apply:multidict._multidict.istr + - x-goog-api-client + : - google-genai-sdk/1.19.0 gl-node/v23.10.0 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"Hello\"}],\"role\": + \"model\"}}],\"usageMetadata\": {\"promptTokenCount\": 5,\"totalTokenCount\": + 5,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 5}]},\"modelVersion\": + \"gemini-2.0-flash\",\"responseId\": \"Xpcwac3qI-GSmNAPkaHBkA4\"}\r\n\r\ndata: + {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \" there! How can + I help you today?\\n\"}],\"role\": \"model\"},\"finishReason\": \"STOP\"}],\"usageMetadata\": + {\"promptTokenCount\": 4,\"candidatesTokenCount\": 11,\"totalTokenCount\": + 15,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 4}],\"candidatesTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 11}]},\"modelVersion\": \"gemini-2.0-flash\",\"responseId\": + \"Xpcwac3qI-GSmNAPkaHBkA4\"}\r\n\r\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Wed, 03 Dec 2025 20:02:38 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=359 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_text-embedding-004_batchEmbedContents_post_d02b671a.yaml b/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_text-embedding-004_batchEmbedContents_post_d02b671a.yaml new file mode 100644 index 00000000000..a31f8c7eb07 --- /dev/null +++ b/packages/dd-trace/test/llmobs/cassettes/genai/genai_v1beta_models_text-embedding-004_batchEmbedContents_post_d02b671a.yaml @@ -0,0 +1,257 @@ +interactions: +- request: + body: '{"requests":[{"content":{"role":"user","parts":[{"text":"Hello, world!"}]},"model":"models/text-embedding-004"}]}' + headers: + ? !!python/object/apply:multidict._multidict.istr + - Accept + : - '*/*' + ? !!python/object/apply:multidict._multidict.istr + - Accept-Encoding + : - gzip, deflate + ? !!python/object/apply:multidict._multidict.istr + - Accept-Language + : - '*' + ? !!python/object/apply:multidict._multidict.istr + - Connection + : - keep-alive + Content-Length: + - '113' + ? !!python/object/apply:multidict._multidict.istr + - Content-Type + : - application/json + ? !!python/object/apply:multidict._multidict.istr + - User-Agent + : - google-genai-sdk/1.19.0 gl-node/v23.10.0 + ? !!python/object/apply:multidict._multidict.istr + - sec-fetch-mode + : - cors + ? !!python/object/apply:multidict._multidict.istr + - x-goog-api-client + : - google-genai-sdk/1.19.0 gl-node/v23.10.0 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:batchEmbedContents + response: + body: + string: "{\n \"embeddings\": [\n {\n \"values\": [\n 0.004205973,\n + \ -0.016312873,\n -0.061666932,\n 0.0038516768,\n -0.031506833,\n + \ -0.008476874,\n 0.061710205,\n 0.041481,\n 0.029845009,\n + \ 0.06339856,\n -0.04829267,\n -0.0057464475,\n 0.02853662,\n + \ -0.00067719404,\n -0.01982539,\n -0.028879909,\n -0.017067183,\n + \ 0.013300819,\n -0.10784503,\n 0.004566127,\n 0.018048378,\n + \ -0.0033129563,\n -0.032339156,\n -0.054119483,\n -0.015690967,\n + \ 0.012721628,\n 0.008894882,\n 0.019235175,\n 0.032604784,\n + \ 0.041423164,\n -0.04288113,\n 0.029799856,\n -0.000990207,\n + \ -0.036238972,\n 0.005078651,\n 0.0066818376,\n -0.05019998,\n + \ 0.014920429,\n 0.024570564,\n -0.059284475,\n -0.04540703,\n + \ 0.029311873,\n 0.011216985,\n 0.0072560576,\n 0.038508393,\n + \ -0.04825037,\n 0.0074585564,\n -0.004730172,\n -0.00021895087,\n + \ -0.0022934792,\n 0.055414923,\n 0.0006981154,\n -0.085721664,\n + \ 0.040854443,\n -0.009195356,\n 0.009331835,\n -0.044866383,\n + \ -0.059136376,\n 0.11389645,\n -0.03477655,\n -0.024747934,\n + \ -0.038420096,\n 0.049240272,\n 0.022916041,\n -0.05213237,\n + \ -0.020155657,\n -0.0023636185,\n 0.015872937,\n -0.034240246,\n + \ 0.013288109,\n -0.05101197,\n 0.0070543024,\n -0.020990252,\n + \ 0.0047948738,\n -0.04771661,\n -0.0006722376,\n -0.0055680475,\n + \ 0.020979479,\n 0.022700988,\n 0.003845046,\n -0.0247121,\n + \ 0.03936633,\n 0.03079961,\n 0.002335929,\n 0.0011387892,\n + \ 0.0018941747,\n -0.00032697074,\n 0.036204435,\n -0.031172337,\n + \ 0.021590877,\n 0.046685494,\n -0.009836748,\n -0.049871277,\n + \ 0.005595991,\n 0.045333162,\n 0.0084727155,\n -0.10301441,\n + \ -0.06325182,\n 0.039191935,\n 0.052909143,\n -0.0024383187,\n + \ -0.014915896,\n 0.005968231,\n -0.03918348,\n 0.010619176,\n + \ 0.062261496,\n -0.059786044,\n 0.005090154,\n -0.019575192,\n + \ -0.019374913,\n 0.030562257,\n -0.051513527,\n 0.046008095,\n + \ 0.010574833,\n -0.031173913,\n -0.06892709,\n -0.07095727,\n + \ 0.027689459,\n -0.036213033,\n 0.008408732,\n 0.017413298,\n + \ 0.03506942,\n -0.003794752,\n 0.026242761,\n 0.066815406,\n + \ -0.005045174,\n 0.03041062,\n -0.006260145,\n -0.018869529,\n + \ -0.03942436,\n 0.047625143,\n -0.047600307,\n 0.024927897,\n + \ 0.03879261,\n -0.05970621,\n -0.0379696,\n 0.028409017,\n + \ 0.008874748,\n 0.012503816,\n -0.011915192,\n 0.013804029,\n + \ -0.0030166768,\n -0.027856905,\n -0.0020341608,\n 0.02860032,\n + \ -0.05334414,\n 0.024839012,\n 0.033154972,\n -0.010805432,\n + \ 0.004324338,\n 0.0005952076,\n 0.011112127,\n 0.039604533,\n + \ -0.035114687,\n -0.030173805,\n 0.017969301,\n 0.04852495,\n + \ -0.06405933,\n 0.04667602,\n 0.018810133,\n -0.019097675,\n + \ -0.049341165,\n 0.012371521,\n -0.024564745,\n -0.008540307,\n + \ 0.026326513,\n 0.009895023,\n -0.032596003,\n 0.0040107435,\n + \ -0.05494834,\n -0.014879786,\n -0.04815298,\n -0.018839112,\n + \ 0.03063704,\n 0.015276285,\n -0.009214081,\n -0.029687732,\n + \ 0.00059397327,\n -0.028817762,\n -0.07114892,\n 0.1080341,\n + \ 0.03594177,\n -0.01430219,\n -0.053013667,\n 0.0115162395,\n + \ 0.033372205,\n 0.009355491,\n 0.051958505,\n 0.08293827,\n + \ 0.041363347,\n -0.0452443,\n 0.027627422,\n 0.0043328684,\n + \ -0.009582696,\n 0.03221606,\n -0.011477449,\n 0.09688274,\n + \ -0.027512548,\n -0.016386677,\n 0.00024350957,\n 0.023900848,\n + \ 0.03520865,\n 0.0035055738,\n -0.028697288,\n 0.003451687,\n + \ 0.03381891,\n -0.0634803,\n -0.06762874,\n 0.037505984,\n + \ 0.057855956,\n 0.006297851,\n -0.020863367,\n 0.0016547053,\n + \ -0.035092402,\n -0.007615867,\n 0.04484872,\n 0.08058618,\n + \ -0.035654154,\n 0.025599768,\n 0.006224549,\n 0.036032613,\n + \ 0.031348288,\n 0.023570517,\n -0.006384024,\n 0.069066875,\n + \ 0.031970646,\n -0.027895756,\n -0.029149402,\n -0.010716533,\n + \ -0.0069897254,\n -0.052813217,\n 0.025956662,\n -0.031100623,\n + \ 0.066179894,\n 0.018908856,\n 0.01424652,\n 0.077878274,\n + \ 0.009223539,\n -0.020687966,\n 0.003970892,\n -0.005487444,\n + \ -0.08915062,\n 0.036114093,\n -0.005832911,\n -0.017647268,\n + \ -0.03858553,\n 0.016804693,\n 0.029245134,\n 0.046392433,\n + \ -0.04945462,\n -0.0728092,\n 0.018480351,\n -0.041829072,\n + \ -0.0029883033,\n -0.031664986,\n -0.040646516,\n 0.003529433,\n + \ -0.033337522,\n 0.0070584784,\n 0.006441496,\n 0.06181225,\n + \ -0.05469857,\n -0.0064712246,\n -0.10011969,\n -0.047741424,\n + \ -0.040743742,\n 0.04112387,\n 0.0054030335,\n 0.0026730893,\n + \ -0.07985,\n 0.0066897017,\n -0.049540967,\n -0.029849887,\n + \ -0.013969279,\n -0.033564497,\n 0.016682865,\n -0.03699766,\n + \ 0.038670152,\n -0.081527166,\n -0.034146223,\n -0.0107111335,\n + \ -0.011444175,\n -0.007594636,\n 0.014812076,\n -0.027027046,\n + \ -0.020546163,\n 0.04476656,\n 0.04470556,\n -0.01335712,\n + \ -0.010013991,\n 0.026073564,\n -0.014830306,\n 0.0041586724,\n + \ -0.017787643,\n 0.023707034,\n 0.011151964,\n 0.03568321,\n + \ 0.011893182,\n 0.05915248,\n 0.010436221,\n 0.0055539315,\n + \ 0.063254565,\n -0.04898677,\n -0.009095629,\n -0.023155494,\n + \ -0.0013976494,\n 0.021534195,\n -0.035029322,\n -0.02507941,\n + \ -0.00050131197,\n -0.003259757,\n 0.025257505,\n 0.0076381816,\n + \ -0.021577695,\n -0.05757716,\n -0.0073500243,\n -0.10515289,\n + \ 0.007892859,\n -0.058430407,\n -0.037536036,\n 0.07427845,\n + \ 0.028240994,\n -0.06576924,\n -0.026322033,\n 0.01353631,\n + \ -0.041844003,\n 0.039330274,\n 0.015589292,\n 0.0067811892,\n + \ -0.006580828,\n 0.02086032,\n 0.017355869,\n 0.012432623,\n + \ -0.06735846,\n 0.0016752067,\n 0.06839084,\n -0.02947169,\n + \ 0.044365544,\n 0.05067977,\n 0.08033458,\n -0.016249835,\n + \ 0.043770116,\n 0.04976311,\n 0.03345613,\n 0.022467917,\n + \ -0.0058555524,\n 0.020422196,\n 0.0037736665,\n -0.00756715,\n + \ -0.052446432,\n -0.0007312344,\n 0.030098774,\n 0.02459433,\n + \ -0.030943898,\n -0.08287694,\n 0.011459479,\n 0.075235605,\n + \ -0.011307589,\n -0.006290694,\n -0.030203698,\n 0.012677551,\n + \ 0.026538933,\n -0.020882022,\n 0.036342204,\n -0.049244165,\n + \ 0.02663577,\n -0.007846865,\n 0.061808918,\n 0.021650165,\n + \ -0.043990206,\n 0.0285376,\n 0.07289369,\n -0.022907868,\n + \ -0.029012937,\n -0.013767754,\n -0.047331512,\n -0.059237823,\n + \ -0.0020641396,\n 0.05182951,\n -0.07690168,\n -0.03245034,\n + \ -0.005666466,\n -0.04984638,\n 0.005529405,\n -0.06555291,\n + \ 0.06698345,\n -0.0418254,\n 0.012513282,\n 0.0011105621,\n + \ -0.009717694,\n -0.024308348,\n 0.055588868,\n 0.04628913,\n + \ 0.041808803,\n 0.02794931,\n 0.02732405,\n -0.066000834,\n + \ -0.005150664,\n 0.018345756,\n 0.023847211,\n -0.018272178,\n + \ 0.035040338,\n 0.033942293,\n -0.024249913,\n 0.00763284,\n + \ -0.05335428,\n 0.04864637,\n 0.02960081,\n 0.064901724,\n + \ -0.0023786456,\n -0.032623503,\n 0.046493173,\n 0.0006576968,\n + \ 0.007322684,\n -0.017462896,\n -0.022226907,\n -0.052381817,\n + \ 0.01688546,\n 0.013324216,\n 0.0145995375,\n -0.0358662,\n + \ -0.041509487,\n 0.02794248,\n -0.029141847,\n -0.021788232,\n + \ -0.05904505,\n -0.02808259,\n 0.045615677,\n 0.03144279,\n + \ -0.015064247,\n 0.0067944764,\n 0.032485023,\n -0.002170979,\n + \ -0.018444821,\n -0.03269699,\n 0.006853717,\n -0.024455357,\n + \ 0.03292642,\n -0.00041346464,\n 0.037025258,\n -0.03587559,\n + \ -0.007597823,\n 0.022579424,\n 0.04899338,\n -0.029097313,\n + \ 0.044881966,\n 0.038841918,\n -0.03211907,\n -0.023040516,\n + \ 0.0031120775,\n 0.008279419,\n -0.017130572,\n -0.018776419,\n + \ 0.019200122,\n -0.035441846,\n 0.039078955,\n -0.028619248,\n + \ 0.03928744,\n 0.023180272,\n 0.024951767,\n -0.0704647,\n + \ 0.02594692,\n 0.07809838,\n -0.031581376,\n -0.022708923,\n + \ -0.001233032,\n -0.03446854,\n 0.0037916913,\n -0.015588428,\n + \ 0.07429779,\n 0.051948328,\n 0.023505136,\n -0.0075604985,\n + \ -0.0002590995,\n 0.026860112,\n 0.024947494,\n -0.010264379,\n + \ -0.012957809,\n 0.006424303,\n 0.02913176,\n -0.0060591996,\n + \ 0.0038780663,\n -0.0064971102,\n 0.015549097,\n 0.08566424,\n + \ -0.010922413,\n -0.035491887,\n 0.0020788722,\n 0.023043428,\n + \ -0.008216521,\n -0.012952355,\n 0.048266612,\n 0.024019672,\n + \ 0.0038070348,\n -0.088460326,\n 0.048348013,\n 0.033691145,\n + \ 0.022758802,\n -0.018643081,\n 0.005612269,\n 0.03837845,\n + \ 0.008866436,\n 0.00939469,\n -0.0061267344,\n -0.051060926,\n + \ -0.016344797,\n -0.026933229,\n -0.05008254,\n 0.019435178,\n + \ -0.01454566,\n 0.04921377,\n -0.018773045,\n 0.050041538,\n + \ 0.022409586,\n 0.0043891883,\n 0.014402388,\n -0.09410028,\n + \ 0.03635216,\n -0.046440735,\n 0.03598327,\n -0.059955645,\n + \ -0.022674307,\n 0.016306596,\n -0.03817508,\n 0.02362014,\n + \ -0.014847408,\n -0.0259713,\n -0.025125576,\n 0.05328285,\n + \ -0.019383436,\n -0.005734266,\n 0.010698763,\n 0.016100759,\n + \ -0.0049040634,\n -0.025632894,\n 0.038413998,\n -0.022957422,\n + \ 0.03995291,\n -0.08297453,\n 0.04647362,\n -0.012544588,\n + \ -0.018220656,\n 0.025310911,\n 0.016276522,\n 0.00039164652,\n + \ 0.0321545,\n -0.021668661,\n 0.018579911,\n -0.03426598,\n + \ 0.040248886,\n 0.009121092,\n -0.052474868,\n -0.036573708,\n + \ -0.00094880234,\n 0.0060124816,\n -0.018234486,\n -0.015054711,\n + \ -0.011899458,\n -0.02409248,\n 0.0011585018,\n 0.047415026,\n + \ -0.042477418,\n 0.021756932,\n -0.010064959,\n 0.053240646,\n + \ 0.0282404,\n -0.01895807,\n 0.060994457,\n 0.007993827,\n + \ 0.015996676,\n 0.016646126,\n -0.0008367462,\n 0.028885722,\n + \ -0.041827403,\n 0.037379995,\n 0.019031309,\n -0.079835005,\n + \ -0.024234585,\n -0.012215742,\n 0.0016275856,\n 0.028532341,\n + \ -0.015355102,\n 0.113295004,\n 0.034008488,\n -0.03277381,\n + \ 0.016553167,\n -0.006526783,\n -0.052816905,\n 0.043612823,\n + \ -0.0029451444,\n 0.013427477,\n 0.029594712,\n -0.03581951,\n + \ 0.013643761,\n 0.0065641394,\n 0.0040422995,\n 0.012209712,\n + \ 0.03674972,\n -0.01073071,\n -0.010516684,\n -0.0080520995,\n + \ -0.03648011,\n 0.008127181,\n -0.039706416,\n 0.005753058,\n + \ -0.00058037404,\n -0.057124168,\n -0.027605284,\n 0.02640069,\n + \ -0.012001156,\n 0.0117782485,\n -0.004288409,\n -0.019478858,\n + \ -0.0120856725,\n -0.10482622,\n 0.025188012,\n -0.028595854,\n + \ -0.023516586,\n -0.04453138,\n 0.014044837,\n 0.02232383,\n + \ 0.020531554,\n 0.027170412,\n 0.029625803,\n -0.045035437,\n + \ -0.012243475,\n 0.034472864,\n 0.021192802,\n 0.037941642,\n + \ 0.016204562,\n -0.024719847,\n 0.006171203,\n 0.040483315,\n + \ -0.0058864853,\n -0.013545996,\n -0.016888455,\n -0.014119367,\n + \ 0.021237966,\n -0.021421082,\n -0.066564955,\n 0.0120096905,\n + \ 0.016417608,\n 0.05091922,\n 0.081430994,\n -0.04070145,\n + \ 0.018179288,\n -0.009969509,\n -0.011278187,\n 0.040868327,\n + \ 0.025881711,\n -0.023881178,\n 0.03665466,\n -0.040669173,\n + \ 0.007911164,\n 0.029318213,\n 0.009926616,\n -0.031106265,\n + \ -0.053949352,\n 0.065630235,\n 0.008303589,\n 0.008851088,\n + \ -0.05582047,\n -0.029948996,\n -0.010525374,\n -0.018938867,\n + \ -0.051805064,\n -0.037701145,\n -0.0132036535,\n 0.0025784061,\n + \ -0.0020286,\n 0.0016768255,\n -0.015773151,\n -0.058250926,\n + \ -0.035149205,\n 0.007863294,\n -0.012248289,\n 0.032486275,\n + \ -0.029952822,\n 0.005225856,\n 0.035085406,\n -0.01249007,\n + \ -0.03463305,\n 0.07053904,\n -0.08083355,\n -0.02093617,\n + \ -0.024706474,\n -0.020064048,\n -0.024596047,\n 0.046577323,\n + \ -0.009821565,\n 0.025495747,\n 0.015658822,\n -0.0431637,\n + \ 0.020999197,\n -0.07537394,\n 0.026498487,\n -0.008119787,\n + \ 0.043449655,\n -0.029937785,\n 0.008430669,\n -0.010652065,\n + \ -0.019893516,\n -0.06390636,\n 0.026841016,\n 0.016849052,\n + \ 0.0126042,\n 0.10310701,\n 0.014941382,\n -0.04688159,\n + \ -0.015398615,\n -0.078716904,\n -0.020732436,\n 0.050092958,\n + \ 0.015555027,\n 0.028196853,\n 0.028334204,\n -0.0017706774,\n + \ 0.02256622,\n -0.01363517,\n 0.0344219,\n -0.02156013,\n + \ 0.01004779,\n 0.04031342,\n 0.046827916,\n 0.04411021,\n + \ -0.028954964,\n -0.021306546,\n 0.017671969,\n -0.03776797,\n + \ 0.0827947,\n 0.0013925948,\n -0.026933925,\n 0.006333444,\n + \ 0.0077707414,\n -0.006682045,\n -0.0007818541,\n -0.0121308565,\n + \ -0.041130785,\n 0.008729496,\n -0.009445522,\n -0.0006596247,\n + \ -0.025773503,\n -0.06154215,\n -0.004647535,\n 0.022809327,\n + \ 0.019543681,\n -0.04441063,\n 0.06122925,\n -0.026787452,\n + \ -0.07385349,\n -0.07415657,\n 0.031279948,\n -0.009888314,\n + \ 0.03453726,\n 0.021450317,\n -0.025728572,\n 0.033811953,\n + \ -0.028763222,\n 0.03246346,\n -0.003911357,\n 0.016180687,\n + \ 0.08972327,\n -0.011674024,\n 0.03866921,\n -0.030505827,\n + \ -0.0412172,\n -0.00810419,\n 0.049038906\n ]\n + \ }\n ]\n}\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=UTF-8 + Date: + - Wed, 03 Dec 2025 20:02:39 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=834 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js b/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js new file mode 100644 index 00000000000..7dd4498f1bb --- /dev/null +++ b/packages/dd-trace/test/llmobs/plugins/google-genai/index.spec.js @@ -0,0 +1,141 @@ +'use strict' + +const { describe, before, it } = require('mocha') +const { withVersions } = require('../../../setup/mocha') +const assert = require('node:assert') + +const { + useLlmObs, + MOCK_STRING, + MOCK_NUMBER, + assertLlmObsSpanEvent +} = require('../../util') + +describe('Plugin', () => { + const getEvents = useLlmObs({ plugin: 'google-genai' }) + + withVersions('google-genai', '@google/genai', (version) => { + let client + + before(async () => { + const { GoogleGenAI } = require(`../../../../../../versions/@google/genai@${version}`).get() + client = new GoogleGenAI({ + apiKey: process.env.GOOGLE_API_KEY || '', + httpOptions: { baseUrl: 'http://127.0.0.1:9126/vcr/genai' } + }) + }) + + describe('models.generateContent', () => { + it('creates a span', async () => { + const result = await client.models.generateContent({ + model: 'gemini-2.0-flash', + contents: 'Hello, world!' + }) + + assert.ok(result) + + const { apmSpans, llmobsSpans } = await getEvents() + assertLlmObsSpanEvent(llmobsSpans[0], { + span: apmSpans[0], + spanKind: 'llm', + name: 'google_genai.request', + modelName: 'gemini-2.0-flash', + modelProvider: 'google', + inputMessages: [{ role: 'user', content: 'Hello, world!' }], + outputMessages: [{ role: 'assistant', content: MOCK_STRING }], + metadata: { + temperature: null, + top_p: null, + top_k: null, + candidate_count: null, + max_output_tokens: null, + stop_sequences: null, + response_logprobs: null, + logprobs: null, + presence_penalty: null, + frequency_penalty: null, + seed: null, + response_mime_type: null, + safety_settings: null, + automatic_function_calling: null + }, + metrics: { + input_tokens: MOCK_NUMBER, + output_tokens: MOCK_NUMBER, + total_tokens: MOCK_NUMBER, + }, + tags: { ml_app: 'test', integration: 'genai' }, + }) + }) + }) + + describe('models.generateContentStream', () => { + it('creates a span', async () => { + const stream = await client.models.generateContentStream({ + model: 'gemini-2.0-flash', + contents: 'Hello, world!' + }) + + for await (const chunk of stream) { + assert.ok(chunk) + } + + const { apmSpans, llmobsSpans } = await getEvents() + assertLlmObsSpanEvent(llmobsSpans[0], { + span: apmSpans[0], + spanKind: 'llm', + name: 'google_genai.request', + modelName: 'gemini-2.0-flash', + modelProvider: 'google', + inputMessages: [{ role: 'user', content: 'Hello, world!' }], + outputMessages: [{ role: 'assistant', content: MOCK_STRING }], + metadata: { + temperature: null, + top_p: null, + top_k: null, + candidate_count: null, + max_output_tokens: null, + stop_sequences: null, + response_logprobs: null, + logprobs: null, + presence_penalty: null, + frequency_penalty: null, + seed: null, + response_mime_type: null, + safety_settings: null, + automatic_function_calling: null + }, + metrics: { + input_tokens: MOCK_NUMBER, + output_tokens: MOCK_NUMBER, + total_tokens: MOCK_NUMBER, + }, + tags: { ml_app: 'test', integration: 'genai' }, + }) + }) + }) + + describe('models.embedContent', () => { + it('creates a span', async () => { + const result = await client.models.embedContent({ + model: 'text-embedding-004', + contents: 'Hello, world!' + }) + + assert.ok(result) + + const { apmSpans, llmobsSpans } = await getEvents() + assertLlmObsSpanEvent(llmobsSpans[0], { + span: apmSpans[0], + spanKind: 'embedding', + name: 'google_genai.request', + modelName: 'text-embedding-004', + modelProvider: 'google', + inputDocuments: [{ text: 'Hello, world!' }], + outputValue: MOCK_STRING, + tags: { ml_app: 'test', integration: 'genai' }, + }) + }) + }) + }) +}) From 4a24cfb0e6e9cee99d60946e39986eae0d6701f5 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Fri, 5 Dec 2025 10:24:10 -0500 Subject: [PATCH 15/16] updating exports for util file --- .../src/llmobs/plugins/genai/index.js | 13 ++---------- .../dd-trace/src/llmobs/plugins/genai/util.js | 21 ++++--------------- .../src/supported-configurations.json | 1 - 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/packages/dd-trace/src/llmobs/plugins/genai/index.js b/packages/dd-trace/src/llmobs/plugins/genai/index.js index 6832a884ca3..0e61c40ca9c 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai/index.js +++ b/packages/dd-trace/src/llmobs/plugins/genai/index.js @@ -13,7 +13,7 @@ const { } = require('./util') class GenAiLLMObsPlugin extends LLMObsPlugin { - static id = 'genai' + static id = 'google-genai' static integration = 'genai' static prefix = 'tracing:apm:google:genai:request' @@ -33,22 +33,17 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { }) } - // ============================================================================ - // Public API Methods - // ============================================================================ - getLLMObsSpanRegisterOptions (ctx) { const { args, methodName } = ctx if (!methodName) return const inputs = args[0] const operation = getOperation(methodName) - const kind = operation return { modelProvider: 'google', modelName: inputs.model, - kind, + kind: operation, name: 'google_genai.request' } } @@ -76,10 +71,6 @@ class GenAiLLMObsPlugin extends LLMObsPlugin { } } - // ============================================================================ - // Tagging Methods - // ============================================================================ - #tagGenerateContent (span, inputs, response, error, isStreaming = false) { const { config = {} } = inputs diff --git a/packages/dd-trace/src/llmobs/plugins/genai/util.js b/packages/dd-trace/src/llmobs/plugins/genai/util.js index df61a35287a..47484e12b2b 100644 --- a/packages/dd-trace/src/llmobs/plugins/genai/util.js +++ b/packages/dd-trace/src/llmobs/plugins/genai/util.js @@ -172,12 +172,11 @@ function formatFunctionCallMessage (parts, functionCalls, role) { const textParts = extractTextParts(parts) const content = textParts.length > 0 ? textParts.join('\n') : undefined + const message = { role, toolCalls } - return { - role, - ...(content && { content }), - toolCalls - } + if (content) message.content = content + + return message } /** @@ -476,24 +475,12 @@ function formatEmbeddingOutput (response) { } module.exports = { - ROLES, getOperation, - extractTextParts, - groupPartsByRole, - hasThoughtParts, - determineRole, - normalizeRole, extractMetrics, extractMetadata, - formatFunctionCallMessage, - formatFunctionResponseMessage, aggregateStreamingChunks, - formatContentObject, formatInputMessages, formatEmbeddingInput, - formatNonStreamingCandidate, - formatStreamingOutput, - formatNonStreamingOutput, formatOutputMessages, formatEmbeddingOutput } diff --git a/packages/dd-trace/src/supported-configurations.json b/packages/dd-trace/src/supported-configurations.json index 70b7ddb50c1..ec382a3da30 100644 --- a/packages/dd-trace/src/supported-configurations.json +++ b/packages/dd-trace/src/supported-configurations.json @@ -287,7 +287,6 @@ "DD_TRACE_FLUSH_INTERVAL": ["A"], "DD_TRACE_FS_ENABLED": ["A"], "DD_TRACE_GENERIC_POOL_ENABLED": ["A"], - "DD_TRACE_GENAI_ENABLED": ["A"], "DD_TRACE_GIT_METADATA_ENABLED": ["A"], "DD_TRACE_GLOBAL_TAGS": ["A"], "DD_TRACE_GOOGLE_CLOUD_PUBSUB_ENABLED": ["A"], From 2bcab5a56ad0c35e470c8df7283cbb3310a22cc6 Mon Sep 17 00:00:00 2001 From: Crystal Luc-Magloire Date: Fri, 5 Dec 2025 10:36:02 -0500 Subject: [PATCH 16/16] adding genai to api docs --- docs/test.ts | 1 + index.d.ts | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/docs/test.ts b/docs/test.ts index 5a6edcbb219..349140dc661 100644 --- a/docs/test.ts +++ b/docs/test.ts @@ -332,6 +332,7 @@ tracer.use('fetch', httpClientOptions); tracer.use('generic-pool'); tracer.use('google-cloud-pubsub'); tracer.use('google-cloud-vertexai'); +tracer.use('google-genai'); tracer.use('graphql'); tracer.use('graphql', graphqlOptions); tracer.use('graphql', { variables: ['foo', 'bar'] }); diff --git a/index.d.ts b/index.d.ts index 83e8ded0b4c..013eba8bbd6 100644 --- a/index.d.ts +++ b/index.d.ts @@ -207,6 +207,7 @@ interface Plugins { "generic-pool": tracer.plugins.generic_pool; "google-cloud-pubsub": tracer.plugins.google_cloud_pubsub; "google-cloud-vertexai": tracer.plugins.google_cloud_vertexai; + "google-genai": tracer.plugins.google_genai; "graphql": tracer.plugins.graphql; "grpc": tracer.plugins.grpc; "hapi": tracer.plugins.hapi; @@ -1843,23 +1844,29 @@ declare namespace tracer { * [@google-cloud/pubsub](https://github.com/googleapis/nodejs-pubsub) module. */ interface google_cloud_pubsub extends Integration {} - + /** * This plugin automatically instruments the * [@google-cloud/vertexai](https://github.com/googleapis/nodejs-vertexai) module. - */ - interface google_cloud_vertexai extends Integration {} + */ + interface google_cloud_vertexai extends Integration {} - /** @hidden */ - interface ExecutionArgs { - schema: any, - document: any, - rootValue?: any, - contextValue?: any, - variableValues?: any, - operationName?: string, - fieldResolver?: any, - typeResolver?: any, + /** + * This plugin automatically instruments the + * [@google-genai](https://github.com/googleapis/js-genai) module. + */ + interface google_genai extends Integration {} + + /** @hidden */ + interface ExecutionArgs { + schema: any, + document: any, + rootValue?: any, + contextValue?: any, + variableValues?: any, + operationName?: string, + fieldResolver?: any, + typeResolver?: any, } /**