Skip to content

Commit 821b4e1

Browse files
authored
feat(anthropic, llmobs): add support for @ai-anthropic/sdk (#6431)
* implementation without test changes * add apm tests * add llmobs basic tests * fix for tests * type docs * fix openai with new cache token metrics key * tweak tagger error message * add tagger tests * remove .only * cache tokens changes from merge * review comments
1 parent 33cf0e5 commit 821b4e1

File tree

21 files changed

+1213
-2
lines changed

21 files changed

+1213
-2
lines changed

.github/workflows/llmobs.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,25 @@ jobs:
141141
uses: ./.github/actions/testagent/logs
142142
with:
143143
suffix: llmobs-${{ github.job }}
144+
145+
anthropic:
146+
runs-on: ubuntu-latest
147+
env:
148+
PLUGINS: anthropic
149+
steps:
150+
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
151+
- uses: ./.github/actions/testagent/start
152+
- uses: ./.github/actions/node/oldest-maintenance-lts
153+
- uses: ./.github/actions/install
154+
- run: yarn test:plugins:ci
155+
- run: yarn test:llmobs:plugins:ci
156+
shell: bash
157+
- uses: ./.github/actions/node/latest
158+
- run: yarn test:plugins:ci
159+
- run: yarn test:llmobs:plugins:ci
160+
shell: bash
161+
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
162+
- if: always()
163+
uses: ./.github/actions/testagent/logs
164+
with:
165+
suffix: llmobs-${{ github.job }}

docs/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ const openSearchOptions: plugins.opensearch = {
305305

306306
tracer.use('amqp10');
307307
tracer.use('amqplib');
308+
tracer.use('anthropic');
308309
tracer.use('avsc');
309310
tracer.use('aws-sdk');
310311
tracer.use('aws-sdk', awsSdkOptions);

index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ interface Plugins {
168168
"aerospike": tracer.plugins.aerospike;
169169
"amqp10": tracer.plugins.amqp10;
170170
"amqplib": tracer.plugins.amqplib;
171+
"anthropic": tracer.plugins.anthropic;
171172
"apollo": tracer.plugins.apollo;
172173
"avsc": tracer.plugins.avsc;
173174
"aws-sdk": tracer.plugins.aws_sdk;
@@ -1530,6 +1531,12 @@ declare namespace tracer {
15301531
*/
15311532
interface amqplib extends Instrumentation {}
15321533

1534+
/**
1535+
* This plugin automatically instruments the
1536+
* [anthropic](https://www.npmjs.com/package/@anthropic-ai/sdk) module.
1537+
*/
1538+
interface anthropic extends Instrumentation {}
1539+
15331540
/**
15341541
* Currently this plugin automatically instruments
15351542
* [@apollo/gateway](https://github.com/apollographql/federation) for module versions >= v2.3.0.

initialize.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ ${result.source}`
3636
const [NODE_MAJOR, NODE_MINOR] = process.versions.node.split('.').map(Number)
3737

3838
const brokenLoaders = NODE_MAJOR === 18 && NODE_MINOR === 0
39-
const iitmExclusions = [/langsmith/, /openai\/_shims/, /openai\/resources\/chat\/completions\/messages/, /openai\/agents-core\/dist\/shims/]
39+
const iitmExclusions = [
40+
/langsmith/,
41+
/openai\/_shims/,
42+
/openai\/resources\/chat\/completions\/messages/,
43+
/openai\/agents-core\/dist\/shims/,
44+
/@anthropic-ai\/sdk\/_shims/
45+
]
4046

4147
export async function load (url, context, nextLoad) {
4248
const iitmExclusionsMatch = iitmExclusions.some((exclusion) => exclusion.test(url))
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
'use strict'
2+
3+
const { addHook } = require('./helpers/instrument')
4+
const shimmer = require('../../datadog-shimmer')
5+
const { channel, tracingChannel } = require('dc-polyfill')
6+
7+
const anthropicTracingChannel = tracingChannel('apm:anthropic:request')
8+
const onStreamedChunkCh = channel('apm:anthropic:request:chunk')
9+
10+
function wrapStreamIterator (iterator, ctx) {
11+
return function () {
12+
const itr = iterator.apply(this, arguments)
13+
shimmer.wrap(itr, 'next', next => function () {
14+
return next.apply(this, arguments)
15+
.then(res => {
16+
const { done, value: chunk } = res
17+
onStreamedChunkCh.publish({ ctx, chunk, done })
18+
19+
if (done) {
20+
finish(ctx)
21+
}
22+
23+
return res
24+
})
25+
.catch(error => {
26+
finish(ctx, null, error)
27+
throw error
28+
})
29+
})
30+
31+
return itr
32+
}
33+
}
34+
35+
function wrapCreate (create) {
36+
return function () {
37+
if (!anthropicTracingChannel.start.hasSubscribers) {
38+
return create.apply(this, arguments)
39+
}
40+
41+
const options = arguments[0]
42+
const stream = options.stream
43+
44+
const ctx = { options, resource: 'create' }
45+
46+
return anthropicTracingChannel.start.runStores(ctx, () => {
47+
let apiPromise
48+
try {
49+
apiPromise = create.apply(this, arguments)
50+
} catch (error) {
51+
finish(ctx, null, error)
52+
throw error
53+
}
54+
55+
shimmer.wrap(apiPromise, 'parse', parse => function () {
56+
return parse.apply(this, arguments)
57+
.then(response => {
58+
if (stream) {
59+
shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx))
60+
} else {
61+
finish(ctx, response, null)
62+
}
63+
64+
return response
65+
}).catch(error => {
66+
finish(ctx, null, error)
67+
throw error
68+
})
69+
})
70+
71+
anthropicTracingChannel.end.publish(ctx)
72+
73+
return apiPromise
74+
})
75+
}
76+
}
77+
78+
function finish (ctx, result, error) {
79+
if (error) {
80+
ctx.error = error
81+
anthropicTracingChannel.error.publish(ctx)
82+
}
83+
84+
// streamed responses are handled and set separately
85+
ctx.result ??= result
86+
87+
anthropicTracingChannel.asyncEnd.publish(ctx)
88+
}
89+
90+
const extensions = ['js', 'mjs']
91+
for (const extension of extensions) {
92+
addHook({
93+
name: '@anthropic-ai/sdk',
94+
file: `resources/messages.${extension}`,
95+
versions: ['>=0.14.0 <0.33.0']
96+
}, exports => {
97+
const Messages = exports.Messages
98+
99+
shimmer.wrap(Messages.prototype, 'create', wrapCreate)
100+
101+
return exports
102+
})
103+
104+
addHook({
105+
name: '@anthropic-ai/sdk',
106+
file: `resources/messages/messages.${extension}`,
107+
versions: ['>=0.33.0']
108+
}, exports => {
109+
const Messages = exports.Messages
110+
111+
shimmer.wrap(Messages.prototype, 'create', wrapCreate)
112+
113+
return exports
114+
})
115+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
module.exports = {
4+
'@anthropic-ai/sdk': { esmFirst: true, fn: () => require('../anthropic') },
45
'@apollo/server': () => require('../apollo-server'),
56
'@apollo/gateway': () => require('../apollo'),
67
'apollo-server-core': () => require('../apollo-server-core'),
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 AnthropicTracingPlugin = require('./tracing')
5+
const AnthropicLLMObsPlugin = require('../../dd-trace/src/llmobs/plugins/anthropic')
6+
7+
class AnthropicPlugin extends CompositePlugin {
8+
static id = 'anthropic'
9+
static get plugins () {
10+
return {
11+
llmobs: AnthropicLLMObsPlugin,
12+
tracing: AnthropicTracingPlugin
13+
}
14+
}
15+
}
16+
17+
module.exports = AnthropicPlugin
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict'
2+
3+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4+
5+
class AnthropicTracingPlugin extends TracingPlugin {
6+
static id = 'anthropic'
7+
static operation = 'request'
8+
static system = 'anthropic'
9+
static prefix = 'tracing:apm:anthropic:request'
10+
11+
bindStart (ctx) {
12+
const { resource, options } = ctx
13+
14+
this.startSpan('anthropic.request', {
15+
meta: {
16+
'resource.name': `Messages.${resource}`,
17+
'anthropic.request.model': options.model
18+
}
19+
}, ctx)
20+
21+
return ctx.currentStore
22+
}
23+
24+
asyncEnd (ctx) {
25+
const span = ctx.currentStore?.span
26+
span?.finish()
27+
}
28+
}
29+
30+
module.exports = AnthropicTracingPlugin

0 commit comments

Comments
 (0)