Skip to content

Commit d743fc7

Browse files
committed
fixup
1 parent 199b4e4 commit d743fc7

4 files changed

Lines changed: 123 additions & 43 deletions

File tree

packages/datadog-instrumentations/src/anthropic.js

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,38 @@ function waitForVerdict (promise, verdict) {
4747
/**
4848
* @param {object} response
4949
* @param {'json'|'text'} method
50+
* @param {object} ctx
5051
* @param {(body: object|string) => Promise<void>|undefined} getVerdict
5152
*/
52-
function wrapResponseReader (response, method, getVerdict) {
53+
function wrapResponseReader (response, method, ctx, getVerdict) {
5354
if (typeof response[method] !== 'function') return
5455

5556
shimmer.wrap(response, method, original => function (...args) {
56-
return original.apply(this, args).then(body => {
57-
const verdict = getVerdict(body)
58-
return verdict ? verdict.then(() => body) : body
59-
})
57+
return original.apply(this, args)
58+
.then(body => finishResult(ctx, body, getVerdict))
59+
.catch(error => {
60+
if (!ctx.finished) finish(ctx, null, error)
61+
throw error
62+
})
63+
})
64+
}
65+
66+
/**
67+
* @param {object} ctx
68+
* @param {object|string} result
69+
* @param {(body: object|string) => Promise<void>|undefined} getVerdict
70+
* @returns {object|string|Promise<object|string>}
71+
*/
72+
function finishResult (ctx, result, getVerdict) {
73+
const verdict = getVerdict(result)
74+
if (!verdict) {
75+
finish(ctx, result, null)
76+
return result
77+
}
78+
79+
return verdict.then(() => {
80+
finish(ctx, result, null)
81+
return result
6082
})
6183
}
6284

@@ -143,17 +165,7 @@ function wrapCreate (create) {
143165
shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx))
144166
return response
145167
}
146-
const verdict = getAfterVerdict(response)
147-
if (!verdict) {
148-
finish(ctx, response, null)
149-
return response
150-
}
151-
// Finish after evaluation so a block propagates the error to anthropic.request
152-
// and the span wraps its child instead of closing before it.
153-
return verdict.then(() => {
154-
finish(ctx, response, null)
155-
return response
156-
})
168+
return finishResult(ctx, response, getAfterVerdict)
157169
}).catch(error => {
158170
if (!ctx.finished) finish(ctx, null, error)
159171
throw error
@@ -162,19 +174,17 @@ function wrapCreate (create) {
162174
return parseResult
163175
})
164176

165-
// Gate `.asResponse()` callers on the before verdict so raw-response paths still block,
166-
// then evaluate output only if the caller consumes the JSON body.
177+
// Gate `.asResponse()` callers on the before verdict, then finish with a supported body reader's result.
167178
shimmer.wrap(apiPromise, 'asResponse', origAsResponse => function (...asResponseArgs) {
168179
return waitForVerdict(origAsResponse.apply(this, asResponseArgs), getBeforeVerdict())
169180
.then(response => {
170-
if (!stream && hasLifecycle && wrappedResponse !== response) {
181+
if (!stream && wrappedResponse !== response) {
171182
wrappedResponse = response
172-
wrapResponseReader(response, 'json', getAfterVerdict)
173-
wrapResponseReader(response, 'text', getAfterVerdict)
183+
wrapResponseReader(response, 'json', ctx, getAfterVerdict)
184+
wrapResponseReader(response, 'text', ctx, getAfterVerdict)
174185
}
175186

176187
if (afterVerdict) return afterVerdict.then(() => response)
177-
if (!stream && !ctx.finished && !parseResult) finish(ctx, null, null)
178188
return response
179189
})
180190
.catch(error => {

packages/datadog-instrumentations/test/anthropic-lifecycle.spec.js

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ describe('anthropic lifecycle instrumentation', () => {
260260
}
261261
})
262262

263-
it('finishes after a resolving before subscriber leaves', async () => {
263+
it('finishes after reading a raw response when the before subscriber leaves', async () => {
264264
const apmChannel = tracingChannel('apm:anthropic:request')
265265
const apmHandlers = { start () {} }
266266
let asyncEndCount = 0
@@ -282,18 +282,19 @@ describe('anthropic lifecycle instrumentation', () => {
282282
messages._nextApiPromise = apiPromise
283283

284284
try {
285-
assert.strictEqual(
286-
await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse(),
287-
apiPromise._rawResponse
288-
)
285+
const response = await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse()
286+
287+
assert.strictEqual(response, apiPromise._rawResponse)
288+
assert.strictEqual(asyncEndCount, 0)
289+
await response.json()
289290
assert.strictEqual(asyncEndCount, 1)
290291
} finally {
291292
apmChannel.unsubscribe(apmHandlers)
292293
unsubscribe()
293294
}
294295
})
295296

296-
it('finishes an unconsumed raw response and evaluates it when read', async () => {
297+
it('finishes a raw response with its result when read', async () => {
297298
const apmChannel = tracingChannel('apm:anthropic:request')
298299
const apmHandlers = { start () {} }
299300
let asyncEndCtx
@@ -312,18 +313,42 @@ describe('anthropic lifecycle instrumentation', () => {
312313
const response = await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse()
313314

314315
assert.strictEqual(calls.length, 1)
315-
assert.ok(asyncEndCtx, 'asyncEnd was not published')
316-
assert.strictEqual(asyncEndCtx.finished, true)
316+
assert.strictEqual(asyncEndCtx, undefined)
317317
assert.strictEqual(response.bodyUsed, false)
318318
assert.deepStrictEqual(await response.json(), body)
319319
assert.strictEqual(calls.length, 2)
320320
assert.deepStrictEqual(calls[1].body, body)
321+
assert.ok(asyncEndCtx, 'asyncEnd was not published')
322+
assert.strictEqual(asyncEndCtx.finished, true)
323+
assert.deepStrictEqual(asyncEndCtx.result, body)
321324
} finally {
322325
apmChannel.unsubscribe(apmHandlers)
323326
unsubscribe()
324327
}
325328
})
326329

330+
it('finishes a raw response reader when only tracing is active', async () => {
331+
const apmChannel = tracingChannel('apm:anthropic:request')
332+
const apmHandlers = { start () {} }
333+
let asyncEndCtx
334+
apmHandlers.asyncEnd = ctx => { asyncEndCtx = ctx }
335+
apmChannel.subscribe(apmHandlers)
336+
337+
const body = { role: 'assistant', content: [{ type: 'text', text: 'Hi' }] }
338+
const messages = new Messages()
339+
messages._nextApiPromise = new FakeAPIPromise(body)
340+
341+
try {
342+
const response = await messages.create({ messages: [{ role: 'user', content: 'Hi' }] }).asResponse()
343+
344+
assert.strictEqual(asyncEndCtx, undefined)
345+
assert.deepStrictEqual(await response.json(), body)
346+
assert.deepStrictEqual(asyncEndCtx.result, body)
347+
} finally {
348+
apmChannel.unsubscribe(apmHandlers)
349+
}
350+
})
351+
327352
it('evaluates repeated asResponse() calls once', async () => {
328353
const { calls, unsubscribe } = subscribeAutoResolve([
329354
messagesBeforeChannel,
@@ -377,11 +402,12 @@ describe('anthropic lifecycle instrumentation', () => {
377402
)
378403
sinon.assert.notCalled(response.clone)
379404
assert.strictEqual(calls.length, 0)
380-
assert.strictEqual(asyncEndCount, 1)
405+
assert.strictEqual(asyncEndCount, 0)
381406
assert.deepStrictEqual(await response.json(), body)
382407
sinon.assert.calledOnce(readJson)
383408
assert.strictEqual(calls.length, 1)
384409
assert.strictEqual(calls[0].body, body)
410+
assert.strictEqual(asyncEndCount, 1)
385411
} finally {
386412
apmChannel.unsubscribe(apmHandlers)
387413
unsubscribe()
@@ -508,22 +534,33 @@ describe('anthropic lifecycle instrumentation', () => {
508534
}
509535
})
510536

511-
it('keeps a raw-terminal span finished when parsing starts later', async () => {
537+
it('retains the parsed result when parsing starts after raw response access', async () => {
512538
const apmChannel = tracingChannel('apm:anthropic:request')
513539
const apmHandlers = { start () {} }
514540
let asyncEndCount = 0
515-
apmHandlers.asyncEnd = () => { asyncEndCount++ }
541+
let asyncEndCtx
542+
apmHandlers.asyncEnd = ctx => {
543+
asyncEndCount++
544+
asyncEndCtx = ctx
545+
}
516546
apmChannel.subscribe(apmHandlers)
517547

518548
const { unsubscribe } = subscribeAutoResolve([messagesAfterChannel])
549+
const body = {
550+
role: 'assistant',
551+
content: [{ type: 'text', text: 'Hi' }],
552+
usage: { input_tokens: 1, output_tokens: 2 },
553+
}
519554
const messages = new Messages()
520-
messages._nextApiPromise = new FakeAPIPromise({ role: 'assistant', content: [] })
555+
messages._nextApiPromise = new FakeAPIPromise(body)
521556
const apiPromise = messages.create({ messages: [{ role: 'user', content: 'Hi' }] })
522557

523558
try {
524559
await apiPromise.asResponse()
525-
await apiPromise.parse()
560+
assert.strictEqual(asyncEndCount, 0)
561+
assert.strictEqual(await apiPromise.parse(), body)
526562
assert.strictEqual(asyncEndCount, 1)
563+
assert.strictEqual(asyncEndCtx.result, body)
527564
} finally {
528565
apmChannel.unsubscribe(apmHandlers)
529566
unsubscribe()

packages/dd-trace/src/aiguard/messages/anthropic.js

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,10 @@ function convertServerToolResultContent (content) {
228228
/**
229229
* Converts a single Anthropic message to zero or more normalized messages.
230230
* Assistant `tool_use` blocks become an assistant `tool_calls` message.
231-
* User `tool_result` blocks become one `tool` message per block, emitted
232-
* before any accompanying text so the chat-style timeline is preserved.
233-
* Text/image blocks are merged into a single message per role.
231+
* Tool result blocks become one `tool` message per block. When the message also carries a tool call
232+
* (built-in server tools return the call and its result together), the assistant tool-call message
233+
* precedes the results; otherwise (user turns) the results precede any accompanying text so the
234+
* chat-style timeline is preserved. Text/image blocks are merged into a single message per role.
234235
*
235236
* @param {{role: string, content: string|Array<AnthropicContentBlock>}} message
236237
* @returns {Array<object>}
@@ -245,16 +246,21 @@ function convertAnthropicMessage (message) {
245246
if (!Array.isArray(content)) return []
246247

247248
const { parts, toolCalls, toolResults, hasImages } = walkContentBlocks(content)
248-
const messages = [...toolResults]
249249
const messageContent = partsToContent(parts, hasImages)
250250

251+
let assistantMessage
251252
if (messageContent != null || toolCalls.length) {
252-
const converted = { role }
253-
if (messageContent != null) converted.content = messageContent
254-
if (toolCalls.length) converted.tool_calls = toolCalls
255-
messages.push(converted)
253+
assistantMessage = { role }
254+
if (messageContent != null) assistantMessage.content = messageContent
255+
if (toolCalls.length) assistantMessage.tool_calls = toolCalls
256+
}
257+
258+
if (toolCalls.length) {
259+
return [assistantMessage, ...toolResults]
256260
}
257261

262+
const messages = [...toolResults]
263+
if (assistantMessage) messages.push(assistantMessage)
258264
return messages
259265
}
260266

packages/dd-trace/test/aiguard/messages/anthropic.spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,33 @@ describe('aiguard/messages/anthropic', () => {
207207
})
208208

209209
describe('built-in server-tool blocks', () => {
210+
it('emits the server tool call before its matching tool result', () => {
211+
const message = {
212+
role: 'assistant',
213+
content: [
214+
{ type: 'server_tool_use', id: 'srv_1', name: 'web_search', input: { query: 'datadog' } },
215+
{
216+
type: 'web_search_tool_result',
217+
tool_use_id: 'srv_1',
218+
content: [{ type: 'web_search_result', title: 'Datadog', url: 'https://datadoghq.com' }],
219+
},
220+
{ type: 'text', text: 'Based on the search, Datadog is an observability platform.' },
221+
],
222+
}
223+
assert.deepStrictEqual(convertAnthropicMessage(message), [
224+
{
225+
role: 'assistant',
226+
content: 'Based on the search, Datadog is an observability platform.',
227+
tool_calls: [{ id: 'srv_1', function: { name: 'web_search', arguments: '{"query":"datadog"}' } }],
228+
},
229+
{
230+
role: 'tool',
231+
tool_call_id: 'srv_1',
232+
content: 'Datadog\nhttps://datadoghq.com',
233+
},
234+
])
235+
})
236+
210237
it('converts server_tool_use blocks to tool_calls', () => {
211238
const message = {
212239
role: 'assistant',

0 commit comments

Comments
 (0)