Skip to content

Commit 12671ed

Browse files
authored
fix(llmobs): address OpenAI Agents review feedback (#9526)
* fix(llmobs): address OpenAI Agents review feedback * fix(llmobs): complete chat completions metadata * fix(llmobs): avoid replacing agents model class * fix(llmobs): address agents review gaps
1 parent b78cd37 commit 12671ed

24 files changed

Lines changed: 966 additions & 1570 deletions

packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = [
1010
...require('./langgraph'),
1111
...require('./mercurius'),
1212
...require('./modelcontextprotocol-sdk'),
13+
...require('./openai-agents'),
1314
...require('./playwright'),
1415
...require('./aws-durable-execution-sdk-js'),
1516
]
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict'
2+
3+
const moduleName = '@openai/agents-openai'
4+
const versionRange = '>=0.7.0'
5+
const functionQuery = {
6+
methodName: 'constructor',
7+
className: 'OpenAIChatCompletionsModel',
8+
kind: 'Sync',
9+
}
10+
const channelName = 'OpenAIChatCompletionsModel_constructor'
11+
12+
module.exports = [
13+
{
14+
module: {
15+
name: moduleName,
16+
versionRange,
17+
filePath: 'dist/openaiChatCompletionsModel.js',
18+
},
19+
functionQuery,
20+
channelName,
21+
},
22+
{
23+
module: {
24+
name: moduleName,
25+
versionRange,
26+
filePath: 'dist/openaiChatCompletionsModel.mjs',
27+
},
28+
functionQuery,
29+
channelName,
30+
},
31+
]
Lines changed: 80 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,49 @@
11
'use strict'
22

3-
const { channel } = require('dc-polyfill')
3+
const { channel, tracingChannel } = require('dc-polyfill')
44
const shimmer = require('../../datadog-shimmer')
5-
const { addHook } = require('./helpers/instrument')
5+
const { addHook, getHooks } = require('./helpers/instrument')
66

77
// `WeakSet` keyed by module exports — replaces the underscored
88
// `mod._datadogPatched` flag while keeping dedupe semantics. Mods are kept
99
// alive by `require.cache` anyway, so this doesn't add lifetime to anything.
1010
const patchedMods = new WeakSet()
11+
const modelBaseURLs = new WeakMap()
12+
const chatCompletionsModelConstructorCh =
13+
tracingChannel('orchestrion:@openai/agents-openai:OpenAIChatCompletionsModel_constructor')
14+
15+
/**
16+
* Capture the client base URL before the SDK stores the client in a #private field.
17+
*
18+
* @param {{ arguments?: [{ baseURL?: string }], self?: object }} ctx
19+
*/
20+
function captureChatCompletionsModelBaseURL (ctx) {
21+
const baseURL = ctx.arguments?.[0]?.baseURL
22+
if (ctx.self && typeof baseURL === 'string') modelBaseURLs.set(ctx.self, baseURL)
23+
}
24+
25+
chatCompletionsModelConstructorCh.end.subscribe(captureChatCompletionsModelBaseURL)
26+
27+
for (const hook of getHooks('@openai/agents-openai')) {
28+
addHook(hook, moduleExports => moduleExports)
29+
}
1130

1231
// Plugin subscribes to this and registers its TracingProcessor when
1332
// `@openai/agents` loads. Publishing from here keeps this file free of
1433
// any cross-package import from the plugin.
1534
const agentsCoreLoadedCh = channel('apm:openai-agents:agents-core:loaded')
1635

17-
// Plugin subscribes here to keep track of the OpenAI-compatible client's
18-
// baseURL — used to resolve `model_provider` (openai / azure_openai /
19-
// deepseek / unknown).
20-
const responseClientCh = channel('apm:openai-agents:response:client')
21-
2236
// Plugin uses addBind on this channel so that legacyStorage.run(store, fn) wraps
2337
// the model call — including async iterator advancement for streaming responses.
2438
// This ensures the active dd-trace span is visible to the openai plugin when it
2539
// creates its openai.request span, correctly parenting it under the agent span.
2640
const modelStartCh = channel('apm:openai-agents:model:start')
2741

42+
// Tool.invoke runs inside agents-core's public function-span context. Bind the
43+
// corresponding dd-trace tool span around that public invocation boundary so
44+
// spans created by user tool code inherit it.
45+
const toolStartCh = channel('apm:openai-agents:tool:start')
46+
2847
// Reference to the loaded @openai/agents module, captured in the first hook
2948
// so that wrapResponseMethod can call getCurrentSpan() without an additional
3049
// require (and without triggering n/no-missing-require on agents-core internals).
@@ -36,60 +55,84 @@ let agentsMod
3655
// provider.getCurrentSpan() (replaces getCurrentSpan)
3756
// Both APIs are tried so this file works across the full supported version range.
3857
// The plugin subscriber (index.js) handles processor registration via the channel.
39-
function getCurrentSpanId (mod) {
40-
if (typeof mod?.getCurrentSpan === 'function') {
41-
return mod.getCurrentSpan()?.spanId
58+
function getCurrentSpan () {
59+
if (typeof agentsMod?.getCurrentSpan === 'function') {
60+
return agentsMod.getCurrentSpan()
4261
}
43-
if (typeof mod?.getGlobalTraceProvider === 'function') {
44-
return mod.getGlobalTraceProvider().getCurrentSpan()?.spanId
62+
if (typeof agentsMod?.getGlobalTraceProvider === 'function') {
63+
return agentsMod.getGlobalTraceProvider().getCurrentSpan()
4564
}
4665
}
4766

67+
function getCurrentSpanId () {
68+
return getCurrentSpan()?.spanId
69+
}
70+
4871
addHook({ name: '@openai/agents', versions: ['>=0.7.0'] }, (mod) => {
4972
if (patchedMods.has(mod)) return mod
5073
if (typeof mod?.addTraceProcessor !== 'function' && typeof mod?.getGlobalTraceProvider !== 'function') return mod
5174
patchedMods.add(mod)
5275
agentsMod = mod
76+
if (typeof mod.tool === 'function') {
77+
shimmer.wrap(mod, 'tool', wrapToolFactory, { replaceGetter: true })
78+
}
5379
agentsCoreLoadedCh.publish({ mod })
5480
return mod
5581
})
5682

83+
function wrapToolFactory (original) {
84+
return function (...args) {
85+
const tool = original.apply(this, args)
86+
if (typeof tool?.invoke === 'function') {
87+
shimmer.wrap(tool, 'invoke', wrapToolInvoke)
88+
}
89+
return tool
90+
}
91+
}
92+
93+
function wrapToolInvoke (original) {
94+
return function (...args) {
95+
const agentsCoreSpan = getCurrentSpan()
96+
return toolStartCh.runStores({ agentsCoreSpan }, () => original.apply(this, args))
97+
}
98+
}
99+
57100
function wrapResponseMethod (original) {
58101
return function (...args) {
59-
const agentsCoreSpanId = getCurrentSpanId(agentsMod)
60-
publishClientBaseURL(this)
61-
return modelStartCh.runStores({ agentsCoreSpanId }, () => original.apply(this, args))
102+
const agentsCoreSpanId = getCurrentSpanId()
103+
const baseURL = getClientBaseURL(this)
104+
return modelStartCh.runStores({ agentsCoreSpanId, baseURL }, () => original.apply(this, args))
62105
}
63106
}
64107

65108
function wrapStreamedResponseMethod (original) {
66109
return function (...args) {
67-
const agentsCoreSpanId = getCurrentSpanId(agentsMod)
68-
publishClientBaseURL(this)
69-
const iterator = modelStartCh.runStores({ agentsCoreSpanId }, () => original.apply(this, args))
70-
return wrapAsyncIterator(iterator, agentsCoreSpanId)
110+
const agentsCoreSpanId = getCurrentSpanId()
111+
const baseURL = getClientBaseURL(this)
112+
const context = { agentsCoreSpanId, baseURL }
113+
const iterator = modelStartCh.runStores(context, () => original.apply(this, args))
114+
return wrapAsyncIterator(iterator, context)
71115
}
72116
}
73117

74-
function publishClientBaseURL (model) {
75-
const baseURL = model?.client?.baseURL ?? model?._client?.baseURL
76-
if (baseURL) responseClientCh.publish({ baseURL })
118+
function getClientBaseURL (model) {
119+
return model?.client?.baseURL ?? model?._client?.baseURL ?? modelBaseURLs.get(model)
77120
}
78121

79-
function wrapAsyncIterator (iterator, agentsCoreSpanId) {
122+
function wrapAsyncIterator (iterator, context) {
80123
if (!iterator || typeof iterator !== 'object') return iterator
81124

82125
return {
83126
next () {
84-
return modelStartCh.runStores({ agentsCoreSpanId }, () => iterator.next.apply(iterator, arguments))
127+
return modelStartCh.runStores(context, () => iterator.next.apply(iterator, arguments))
85128
},
86129
throw () {
87130
if (typeof iterator.throw !== 'function') return Promise.reject(arguments[0])
88-
return modelStartCh.runStores({ agentsCoreSpanId }, () => iterator.throw.apply(iterator, arguments))
131+
return modelStartCh.runStores(context, () => iterator.throw.apply(iterator, arguments))
89132
},
90133
return () {
91134
if (typeof iterator.return !== 'function') return Promise.resolve({ done: true, value: arguments[0] })
92-
return modelStartCh.runStores({ agentsCoreSpanId }, () => iterator.return.apply(iterator, arguments))
135+
return modelStartCh.runStores(context, () => iterator.return.apply(iterator, arguments))
93136
},
94137
[Symbol.asyncIterator] () {
95138
return this
@@ -99,11 +142,18 @@ function wrapAsyncIterator (iterator, agentsCoreSpanId) {
99142

100143
addHook({ name: '@openai/agents-openai', versions: ['>=0.7.0'] }, (mod) => {
101144
if (patchedMods.has(mod)) return mod
102-
const proto = mod?.OpenAIResponsesModel?.prototype
103-
if (!proto) return mod
145+
const responseProto = mod?.OpenAIResponsesModel?.prototype
146+
const chatCompletionsProto = mod?.OpenAIChatCompletionsModel?.prototype
147+
if (!responseProto && !chatCompletionsProto) return mod
104148

105149
patchedMods.add(mod)
106-
shimmer.wrap(proto, 'getResponse', wrapResponseMethod)
107-
shimmer.wrap(proto, 'getStreamedResponse', wrapStreamedResponseMethod)
150+
for (const proto of [responseProto, chatCompletionsProto]) {
151+
if (typeof proto?.getResponse === 'function') {
152+
shimmer.wrap(proto, 'getResponse', wrapResponseMethod)
153+
}
154+
if (typeof proto?.getStreamedResponse === 'function') {
155+
shimmer.wrap(proto, 'getStreamedResponse', wrapStreamedResponseMethod)
156+
}
157+
}
108158
return mod
109159
})

packages/datadog-plugin-openai-agents/src/index.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
const { storage } = require('../../datadog-core')
44
const Plugin = require('../../dd-trace/src/plugins/plugin')
5-
const { OpenAIAgentsIntegration } = require('./integration')
5+
const { MODEL_BASE_URL_STORE_KEY, OpenAIAgentsIntegration } = require('./integration')
66
const { DDOpenAIAgentsProcessor } = require('./processor')
77

88
const legacyStorage = storage('legacy')
@@ -14,9 +14,6 @@ const legacyStorage = storage('legacy')
1414
* during its constructor (which runs synchronously between `loadChannel`'s
1515
* publish and the addHook callback) and registers the processor.
1616
*
17-
* The instrumentation also publishes the OpenAI-compatible client's baseURL on
18-
* each model response call, so the integration can resolve `model_provider`.
19-
*
2017
* The integration's `enabled` flag follows this plugin's configure()
2118
* lifecycle. Each loaded version of the agents package replaces all processors
2219
* via setTraceProcessors() on module load, so the plugin re-registers a
@@ -48,26 +45,29 @@ class OpenaiAgentsPlugin extends Plugin {
4845
}
4946
})
5047

51-
this.addSub('apm:openai-agents:response:client', ({ baseURL }) => {
52-
if (!this.#integration.enabled) return
53-
this.#integration.setClientBaseURL(baseURL)
54-
})
55-
5648
// Activate the current agent's dd-trace span in legacyStorage for the
5749
// duration of model response calls and stream iterator advancement. This
5850
// makes the openai plugin's shimmer see the correct parent when it creates its
5951
// openai.request span, so all spans land in the same trace.
60-
this.addBind('apm:openai-agents:model:start', ({ agentsCoreSpanId }) => {
61-
if (!this.#integration.enabled || !agentsCoreSpanId) return legacyStorage.getStore()
52+
this.addBind('apm:openai-agents:model:start', ({ agentsCoreSpanId, baseURL }) => {
53+
const store = legacyStorage.getStore()
54+
if (!this.#integration.enabled || !agentsCoreSpanId) return store
6255
const ddSpan = this.#integration.getDDSpan(agentsCoreSpanId)
63-
if (!ddSpan) return legacyStorage.getStore()
64-
return { ...legacyStorage.getStore(), span: ddSpan }
56+
if (!ddSpan) return store
57+
return { ...store, [MODEL_BASE_URL_STORE_KEY]: baseURL, span: ddSpan }
58+
})
59+
60+
this.addBind('apm:openai-agents:tool:start', ({ agentsCoreSpan }) => {
61+
const store = legacyStorage.getStore()
62+
if (!this.#integration.enabled || !agentsCoreSpan) return store
63+
const ddSpan = this.#integration.getOrStartToolSpan(agentsCoreSpan)
64+
return ddSpan ? { ...store, span: ddSpan } : store
6565
})
6666
}
6767

6868
configure (config) {
6969
super.configure(config)
70-
this.#integration.setEnabled(!!config?.enabled)
70+
this.#integration.configure(config)
7171
}
7272
}
7373

0 commit comments

Comments
 (0)