Skip to content

Commit d93e210

Browse files
RulaKhaledclaude
andauthored
fix(server-utils): Instrument the Vercel AI experimental_decide channel event (#25068)
Since `ai` 7.0.128, `experimental_evaluate` calls produce no `gen_ai.evaluate` span. The release renamed it to `experimental_decide`, and the tracing channel now publishes `type: "experimental_decide"`. Our subscriber only matched the old name. It now handles both, since 7.0.111 to 7.0.127 still publish `experimental_evaluate`. This broke the evaluate integration tests and the `node-eve` Jev e2e test on every PR, because the suite installs `ai@^7.0.111`. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 161bd94 commit d93e210

2 files changed

Lines changed: 62 additions & 2 deletions

File tree

‎packages/server-utils/src/integrations/vercel-ai/vercel-ai-dc-subscriber.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,8 @@ export type ChannelEventType =
217217
| 'embed'
218218
| 'embedMany'
219219
| 'rerank'
220-
| 'experimental_evaluate';
220+
| 'experimental_evaluate'
221+
| 'experimental_decide';
221222

222223
/**
223224
* The context object the AI SDK passes through one tracing-channel call. It is the same object
@@ -451,7 +452,9 @@ export function createSpanFromMessage(
451452
}
452453
case 'rerank':
453454
return startGenAiSpan('rerank', modelId, baseAttributes);
455+
// `ai` 7.0.128 renamed `experimental_evaluate` to `experimental_decide`; older 7.x still publishes the old name.
454456
case 'experimental_evaluate':
457+
case 'experimental_decide':
455458
return startGenAiSpan('evaluate', modelId, {
456459
...baseAttributes,
457460
...(recordInputs
@@ -654,7 +657,7 @@ function getOutputMessages(
654657
result: Record<string, unknown>,
655658
finishReason: string | undefined,
656659
): string | undefined {
657-
if (type === 'experimental_evaluate') {
660+
if (type === 'experimental_evaluate' || type === 'experimental_decide') {
658661
return stringify([{ type: 'evaluation', answers: withProviderConfidence(result) }]);
659662
}
660663
// `languageModelCall` exposes the response as a `content` parts array; top-level results expose
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { getMainCarrier, setCurrentClient, spanToStaticSpanJSON } from '@sentry/core';
3+
import type { Span } from '@sentry/core';
4+
import { createSpanFromMessage, enrichSpanOnEnd } from '../../../src/integrations/vercel-ai/vercel-ai-dc-subscriber';
5+
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
6+
7+
type Message = Parameters<typeof createSpanFromMessage>[0];
8+
9+
describe('Vercel AI evaluate spans', () => {
10+
let endedSpans: Span[];
11+
12+
beforeEach(() => {
13+
getMainCarrier().__SENTRY__ = undefined;
14+
const client = new TestClient(
15+
getDefaultTestClientOptions({
16+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
17+
tracesSampleRate: 1,
18+
}),
19+
);
20+
setCurrentClient(client);
21+
client.init();
22+
endedSpans = [];
23+
client.on('spanEnd', span => endedSpans.push(span));
24+
});
25+
26+
afterEach(() => {
27+
getMainCarrier().__SENTRY__ = undefined;
28+
});
29+
30+
// `ai` 7.0.128 renamed the channel event from `experimental_evaluate` to `experimental_decide`.
31+
it.each(['experimental_evaluate', 'experimental_decide'] as const)('creates an evaluate span for `%s`', type => {
32+
const message = {
33+
type,
34+
event: {
35+
provider: 'gateway',
36+
modelId: 'typesafe-ai/jev',
37+
state: 'I want a refund',
38+
questions: { wantsRefund: { type: 'boolean', instructions: 'Is a refund requested?' } },
39+
recordInputs: true,
40+
recordOutputs: true,
41+
},
42+
result: { answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } },
43+
} as unknown as Message;
44+
45+
const span = createSpanFromMessage(message, {} as Parameters<typeof createSpanFromMessage>[1])!;
46+
expect(span).toBeDefined();
47+
enrichSpanOnEnd(span, message, {} as Parameters<typeof enrichSpanOnEnd>[2]);
48+
span.end();
49+
50+
const json = spanToStaticSpanJSON(endedSpans[0]!);
51+
expect(json.description).toBe('evaluate typesafe-ai/jev');
52+
expect(json.op).toBe('gen_ai.evaluate');
53+
expect(JSON.parse(json.data?.['gen_ai.output.messages'] as string)).toEqual([
54+
{ type: 'evaluation', answers: { wantsRefund: { type: 'boolean', probability: 0.99 } } },
55+
]);
56+
});
57+
});

0 commit comments

Comments
 (0)