Skip to content

Commit 0d9de6c

Browse files
committed
feat(ai): add vercel ai integration (#5858)
* add vercel ai integration with otel processing * add some typedocs and comments * fix tagger test * rename to 'ai' * try doing with a custom tracer * change up implementation slightly * codeowners * undo id changes * get rid of otel span start/end publishes * revert llmobs tagger test change * add better noop default tracer and esm support * delete util file * add initial test skeleton * fix duplicate wrapping * simplify patching * apm tests * write some tests * add rest of llmobs tests * add ci job * fix node version and import issue with check and externally defined version * fix metadata tagging * handle tool rolls * remove import * add default return and docstring for formatMessage * fix tool message tests * add model name and provider tags to apm tracing * some self review * address some review comments * do not stub for tests, instead use dummy test agent * move cassettes to local directory to fix tests * configurable flush interval for tests * use separate image for ai tests that have local cassettes attached * use different port * move test flush interval back local * change in esm test * Revert "move test flush interval back local" This reverts commit 39ccf68. * Revert "use different port" This reverts commit ac4071c. * Revert "use separate image for ai tests that have local cassettes attached" This reverts commit 72eca83. * Revert "move cassettes to local directory to fix tests" This reverts commit 20c3c63. * Revert "change in esm test" This reverts commit 32d75a6. * remove env var from llmobs workflow * fix type hint for test util * add test cassettes * re-trigger ci * more review fixes * try removing configuration * revert supported config undoing * add ai to versions package.json * add @ai-sdk/openai to versions package.json * fix tests * remove unrelated change * clean up tests & versions more to not use zod directly * require withVersions directly * forgotten withversions * change ci for ai job to use node latest
1 parent ad1b3ed commit 0d9de6c

38 files changed

+5409
-5
lines changed

.github/workflows/llmobs.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,25 @@ jobs:
119119
uses: ./.github/actions/testagent/logs
120120
with:
121121
suffix: llmobs-${{ github.job }}
122+
123+
ai:
124+
runs-on: ubuntu-latest
125+
env:
126+
PLUGINS: ai
127+
steps:
128+
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
129+
- uses: ./.github/actions/testagent/start
130+
- uses: ./.github/actions/node/oldest-maintenance-lts
131+
- uses: ./.github/actions/install
132+
- run: yarn test:plugins:ci
133+
- run: yarn test:llmobs:plugins:ci
134+
shell: bash
135+
- uses: ./.github/actions/node/latest
136+
- run: yarn test:plugins:ci
137+
- run: yarn test:llmobs:plugins:ci
138+
shell: bash
139+
- uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
140+
- if: always()
141+
uses: ./.github/actions/testagent/logs
142+
with:
143+
suffix: llmobs-${{ github.job }}

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,11 @@
6161
/packages/datadog-plugin-openai/ @DataDog/ml-observability
6262
/packages/datadog-plugin-langchain/ @DataDog/ml-observability
6363
/packages/datadog-plugin-google-cloud-vertexai/ @DataDog/ml-observability
64+
/packages/datadog-plugin-ai/ @DataDog/ml-observability
6465
/packages/datadog-instrumentations/src/openai.js @DataDog/ml-observability
6566
/packages/datadog-instrumentations/src/langchain.js @DataDog/ml-observability
6667
/packages/datadog-instrumentations/src/google-cloud-vertexai.js @DataDog/ml-observability
68+
/packages/datadog-instrumentations/src/ai.js @DataDog/ml-observability
6769
/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime @DataDog/ml-observability
6870
/packages/datadog-plugin-aws-sdk/test/bedrockruntime.spec.js @DataDog/ml-observability
6971

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
'use strict'
2+
3+
const { addHook } = require('./helpers/instrument')
4+
const shimmer = require('../../datadog-shimmer')
5+
6+
const { channel, tracingChannel } = require('dc-polyfill')
7+
const toolCreationChannel = channel('dd-trace:vercel-ai:tool')
8+
9+
const TRACED_FUNCTIONS = {
10+
generateText: wrapWithTracer,
11+
streamText: wrapWithTracer,
12+
generateObject: wrapWithTracer,
13+
streamObject: wrapWithTracer,
14+
embed: wrapWithTracer,
15+
embedMany: wrapWithTracer,
16+
tool: wrapTool
17+
}
18+
19+
const vercelAiTracingChannel = tracingChannel('dd-trace:vercel-ai')
20+
const vercelAiSpanSetAttributesChannel = channel('dd-trace:vercel-ai:span:setAttributes')
21+
22+
const noopTracer = {
23+
startActiveSpan () {
24+
const fn = arguments[arguments.length - 1]
25+
26+
const span = {
27+
spanContext () { return { traceId: '', spanId: '', traceFlags: 0 } },
28+
setAttribute () { return this },
29+
setAttributes () { return this },
30+
addEvent () { return this },
31+
addLink () { return this },
32+
addLinks () { return this },
33+
setStatus () { return this },
34+
updateName () { return this },
35+
end () { return this },
36+
isRecording () { return false },
37+
recordException () { return this }
38+
}
39+
40+
return fn(span)
41+
}
42+
}
43+
44+
function wrapTracer (tracer) {
45+
if (Object.hasOwn(tracer, Symbol.for('_dd.wrapped'))) return
46+
47+
shimmer.wrap(tracer, 'startActiveSpan', function (startActiveSpan) {
48+
return function () {
49+
const name = arguments[0]
50+
const options = arguments.length > 2 ? (arguments[1] ?? {}) : {} // startActiveSpan(name, fn)
51+
const cb = arguments[arguments.length - 1]
52+
53+
const ctx = {
54+
name,
55+
attributes: options.attributes ?? {}
56+
}
57+
58+
arguments[arguments.length - 1] = shimmer.wrapFunction(cb, function (originalCb) {
59+
return function (span) {
60+
shimmer.wrap(span, 'end', function (spanEnd) {
61+
return function () {
62+
vercelAiTracingChannel.asyncEnd.publish(ctx)
63+
return spanEnd.apply(this, arguments)
64+
}
65+
})
66+
67+
shimmer.wrap(span, 'setAttributes', function (setAttributes) {
68+
return function (attributes) {
69+
vercelAiSpanSetAttributesChannel.publish({ ctx, attributes })
70+
return setAttributes.apply(this, arguments)
71+
}
72+
})
73+
74+
shimmer.wrap(span, 'recordException', function (recordException) {
75+
return function (exception) {
76+
ctx.error = exception
77+
vercelAiTracingChannel.error.publish(ctx)
78+
return recordException.apply(this, arguments)
79+
}
80+
})
81+
82+
return originalCb.apply(this, arguments)
83+
}
84+
})
85+
86+
return vercelAiTracingChannel.start.runStores(ctx, () => {
87+
const result = startActiveSpan.apply(this, arguments)
88+
vercelAiTracingChannel.end.publish(ctx)
89+
return result
90+
})
91+
}
92+
})
93+
94+
Object.defineProperty(tracer, Symbol.for('_dd.wrapped'), { value: true })
95+
}
96+
97+
function wrapWithTracer (fn) {
98+
return function () {
99+
const options = arguments[0]
100+
101+
options.experimental_telemetry ??= { isEnabled: true, tracer: noopTracer }
102+
wrapTracer(options.experimental_telemetry.tracer)
103+
104+
return fn.apply(this, arguments)
105+
}
106+
}
107+
108+
function wrapTool (tool) {
109+
return function () {
110+
const args = arguments[0]
111+
toolCreationChannel.publish(args)
112+
113+
return tool.apply(this, arguments)
114+
}
115+
}
116+
117+
// CJS exports
118+
addHook({
119+
name: 'ai',
120+
versions: ['>=4.0.0'],
121+
}, exports => {
122+
for (const [fnName, patchingFn] of Object.entries(TRACED_FUNCTIONS)) {
123+
exports = shimmer.wrap(exports, fnName, patchingFn, { replaceGetter: true })
124+
}
125+
126+
return exports
127+
})
128+
129+
// ESM exports
130+
addHook({
131+
name: 'ai',
132+
versions: ['>=4.0.0'],
133+
file: 'dist/index.mjs'
134+
}, exports => {
135+
for (const [fnName, patchingFn] of Object.entries(TRACED_FUNCTIONS)) {
136+
exports = shimmer.wrap(exports, fnName, patchingFn, { replaceGetter: true })
137+
}
138+
139+
return exports
140+
})

packages/datadog-instrumentations/src/helpers/hooks.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ module.exports = {
3030
'@smithy/smithy-client': () => require('../aws-sdk'),
3131
'@vitest/runner': { esmFirst: true, fn: () => require('../vitest') },
3232
aerospike: () => require('../aerospike'),
33+
ai: () => require('../ai'),
3334
amqp10: () => require('../amqp10'),
3435
amqplib: () => require('../amqplib'),
3536
avsc: () => require('../avsc'),
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict'
2+
3+
const CompositePlugin = require('../../dd-trace/src/plugins/composite')
4+
const VercelAILLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/ai')
5+
const VercelAITracingPlugin = require('./tracing')
6+
7+
class VercelAIPlugin extends CompositePlugin {
8+
static get id () { return 'ai' }
9+
static get plugins () {
10+
return {
11+
llmobs: VercelAILLMObsPlugin,
12+
tracing: VercelAITracingPlugin
13+
}
14+
}
15+
}
16+
17+
module.exports = VercelAIPlugin
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use strict'
2+
3+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4+
const { getModelProvider } = require('./utils')
5+
6+
class VercelAITracingPlugin extends TracingPlugin {
7+
static id = 'ai'
8+
static prefix = 'tracing:dd-trace:vercel-ai'
9+
10+
bindStart (ctx) {
11+
const attributes = ctx.attributes
12+
13+
const model = attributes['ai.model.id']
14+
const modelProvider = getModelProvider(attributes)
15+
16+
this.startSpan(ctx.name, {
17+
meta: {
18+
'resource.name': ctx.name,
19+
'ai.request.model': model,
20+
'ai.request.model_provider': modelProvider
21+
}
22+
}, ctx)
23+
24+
return ctx.currentStore
25+
}
26+
27+
asyncEnd (ctx) {
28+
const span = ctx.currentStore?.span
29+
span?.finish()
30+
}
31+
}
32+
33+
module.exports = VercelAITracingPlugin
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use strict'
2+
3+
const { parseModelId } = require('../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
4+
5+
/**
6+
* Get the model provider from the span tags or attributes.
7+
* This is normalized to LLM Observability model provider standards.
8+
*
9+
* @param {Record<string, string>} tags
10+
* @returns {string}
11+
*/
12+
function getModelProvider (tags) {
13+
const modelProviderTag = tags['ai.model.provider']
14+
const providerParts = modelProviderTag?.split('.')
15+
const provider = providerParts?.[0]
16+
17+
if (provider === 'amazon-bedrock') {
18+
const modelId = tags['ai.model.id']
19+
const model = modelId && parseModelId(modelId)
20+
return model?.modelProvider ?? provider
21+
}
22+
23+
return provider
24+
}
25+
26+
module.exports = {
27+
getModelProvider
28+
}

0 commit comments

Comments
 (0)