Skip to content

Commit 54eb671

Browse files
authored
feat(vercel-ai): Add rerank support and fix token attribute mapping (#19144)
1 parent ad0f2a0 commit 54eb671

7 files changed

Lines changed: 124 additions & 3 deletions

File tree

packages/core/src/tracing/ai/gen-ai-attributes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,11 @@ export const GEN_AI_EMBED_DO_EMBED_OPERATION_ATTRIBUTE = 'gen_ai.embed';
234234
*/
235235
export const GEN_AI_EMBED_MANY_DO_EMBED_OPERATION_ATTRIBUTE = 'gen_ai.embed_many';
236236

237+
/**
238+
* The span operation name for reranking
239+
*/
240+
export const GEN_AI_RERANK_DO_RERANK_OPERATION_ATTRIBUTE = 'gen_ai.rerank';
241+
237242
/**
238243
* The span operation name for executing a tool
239244
*/

packages/core/src/tracing/vercel-ai/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const INVOKE_AGENT_OPS = new Set([
1212
'ai.streamObject',
1313
'ai.embed',
1414
'ai.embedMany',
15+
'ai.rerank',
1516
]);
1617

1718
export const GENERATE_CONTENT_OPS = new Set([
@@ -22,3 +23,5 @@ export const GENERATE_CONTENT_OPS = new Set([
2223
]);
2324

2425
export const EMBEDDINGS_OPS = new Set(['ai.embed.doEmbed', 'ai.embedMany.doEmbed']);
26+
27+
export const RERANK_OPS = new Set(['ai.rerank.doRerank']);

packages/core/src/tracing/vercel-ai/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE,
2020
GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE,
2121
} from '../ai/gen-ai-attributes';
22-
import { EMBEDDINGS_OPS, GENERATE_CONTENT_OPS, INVOKE_AGENT_OPS, toolCallSpanMap } from './constants';
22+
import { EMBEDDINGS_OPS, GENERATE_CONTENT_OPS, INVOKE_AGENT_OPS, RERANK_OPS, toolCallSpanMap } from './constants';
2323
import type { TokenSummary } from './types';
2424
import {
2525
accumulateTokensForParent,
@@ -70,6 +70,9 @@ function mapVercelAiOperationName(operationName: string): string {
7070
if (EMBEDDINGS_OPS.has(operationName)) {
7171
return 'embeddings';
7272
}
73+
if (RERANK_OPS.has(operationName)) {
74+
return 'rerank';
75+
}
7376
if (operationName === 'ai.toolCall') {
7477
return 'execute_tool';
7578
}
@@ -149,6 +152,13 @@ function processEndedVercelAiSpan(span: SpanJSON): void {
149152
renameAttributeKey(attributes, AI_USAGE_PROMPT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE);
150153
renameAttributeKey(attributes, AI_USAGE_CACHED_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_INPUT_TOKENS_CACHED_ATTRIBUTE);
151154

155+
// Parent spans (ai.streamText, ai.streamObject, etc.) use inputTokens/outputTokens instead of promptTokens/completionTokens
156+
renameAttributeKey(attributes, 'ai.usage.inputTokens', GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE);
157+
renameAttributeKey(attributes, 'ai.usage.outputTokens', GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE);
158+
159+
// AI SDK uses avgOutputTokensPerSecond, map to our expected attribute name
160+
renameAttributeKey(attributes, 'ai.response.avgOutputTokensPerSecond', 'ai.response.avgCompletionTokensPerSecond');
161+
152162
// Input tokens is the sum of prompt tokens and cached input tokens
153163
if (
154164
typeof attributes[GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE] === 'number' &&
@@ -290,6 +300,9 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute
290300
case 'ai.embedMany.doEmbed':
291301
span.updateName(`embed_many ${modelId}`);
292302
break;
303+
case 'ai.rerank.doRerank':
304+
span.updateName(`rerank ${modelId}`);
305+
break;
293306
}
294307
}
295308
}

packages/core/src/tracing/vercel-ai/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
GEN_AI_INPUT_MESSAGES_ATTRIBUTE,
1010
GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE,
1111
GEN_AI_INVOKE_AGENT_OPERATION_ATTRIBUTE,
12+
GEN_AI_RERANK_DO_RERANK_OPERATION_ATTRIBUTE,
1213
GEN_AI_STREAM_OBJECT_DO_STREAM_OPERATION_ATTRIBUTE,
1314
GEN_AI_STREAM_TEXT_DO_STREAM_OPERATION_ATTRIBUTE,
1415
GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE,
@@ -190,6 +191,7 @@ export function getSpanOpFromName(name: string): string | undefined {
190191
case 'ai.streamObject':
191192
case 'ai.embed':
192193
case 'ai.embedMany':
194+
case 'ai.rerank':
193195
return GEN_AI_INVOKE_AGENT_OPERATION_ATTRIBUTE;
194196
case 'ai.generateText.doGenerate':
195197
return GEN_AI_GENERATE_TEXT_DO_GENERATE_OPERATION_ATTRIBUTE;
@@ -203,6 +205,8 @@ export function getSpanOpFromName(name: string): string | undefined {
203205
return GEN_AI_EMBED_DO_EMBED_OPERATION_ATTRIBUTE;
204206
case 'ai.embedMany.doEmbed':
205207
return GEN_AI_EMBED_MANY_DO_EMBED_OPERATION_ATTRIBUTE;
208+
case 'ai.rerank.doRerank':
209+
return GEN_AI_RERANK_DO_RERANK_OPERATION_ATTRIBUTE;
206210
case 'ai.toolCall':
207211
return GEN_AI_EXECUTE_TOOL_OPERATION_ATTRIBUTE;
208212
default:
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { addVercelAiProcessors } from '../../../src/tracing/vercel-ai';
3+
import type { SpanJSON } from '../../../src/types-hoist/span';
4+
import { getDefaultTestClientOptions, TestClient } from '../../mocks/client';
5+
6+
describe('vercel-ai parent span token attributes', () => {
7+
it('should map ai.usage.inputTokens to gen_ai.usage.input_tokens', () => {
8+
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 });
9+
const client = new TestClient(options);
10+
client.init();
11+
addVercelAiProcessors(client);
12+
13+
const mockSpan: SpanJSON = {
14+
description: 'ai.streamText',
15+
span_id: 'test-span-id',
16+
trace_id: 'test-trace-id',
17+
start_timestamp: 1000,
18+
timestamp: 2000,
19+
origin: 'auto.vercelai.otel',
20+
data: {
21+
'ai.usage.inputTokens': 100,
22+
'ai.usage.outputTokens': 50,
23+
},
24+
};
25+
26+
const event = {
27+
type: 'transaction' as const,
28+
spans: [mockSpan],
29+
};
30+
31+
const eventProcessor = client['_eventProcessors'].find(processor => processor.id === 'VercelAiEventProcessor');
32+
expect(eventProcessor).toBeDefined();
33+
34+
const processedEvent = eventProcessor!(event, {});
35+
36+
expect(processedEvent?.spans?.[0]?.data?.['gen_ai.usage.input_tokens']).toBe(100);
37+
expect(processedEvent?.spans?.[0]?.data?.['gen_ai.usage.output_tokens']).toBe(50);
38+
// Original attributes should be renamed to vercel.ai.* namespace
39+
expect(processedEvent?.spans?.[0]?.data?.['ai.usage.inputTokens']).toBeUndefined();
40+
expect(processedEvent?.spans?.[0]?.data?.['ai.usage.outputTokens']).toBeUndefined();
41+
});
42+
43+
it('should map ai.response.avgOutputTokensPerSecond to ai.response.avgCompletionTokensPerSecond', () => {
44+
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 });
45+
const client = new TestClient(options);
46+
client.init();
47+
addVercelAiProcessors(client);
48+
49+
const mockSpan: SpanJSON = {
50+
description: 'ai.streamText.doStream',
51+
span_id: 'test-span-id',
52+
trace_id: 'test-trace-id',
53+
start_timestamp: 1000,
54+
timestamp: 2000,
55+
origin: 'auto.vercelai.otel',
56+
data: {
57+
'ai.response.avgOutputTokensPerSecond': 25.5,
58+
},
59+
};
60+
61+
const event = {
62+
type: 'transaction' as const,
63+
spans: [mockSpan],
64+
};
65+
66+
const eventProcessor = client['_eventProcessors'].find(processor => processor.id === 'VercelAiEventProcessor');
67+
expect(eventProcessor).toBeDefined();
68+
69+
const processedEvent = eventProcessor!(event, {});
70+
71+
// Should be renamed to match the expected attribute name
72+
expect(processedEvent?.spans?.[0]?.data?.['vercel.ai.response.avgCompletionTokensPerSecond']).toBe(25.5);
73+
expect(processedEvent?.spans?.[0]?.data?.['ai.response.avgOutputTokensPerSecond']).toBeUndefined();
74+
});
75+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getSpanOpFromName } from '../../../src/tracing/vercel-ai/utils';
3+
4+
describe('vercel-ai rerank support', () => {
5+
describe('getSpanOpFromName', () => {
6+
it('should map ai.rerank to gen_ai.invoke_agent', () => {
7+
expect(getSpanOpFromName('ai.rerank')).toBe('gen_ai.invoke_agent');
8+
});
9+
10+
it('should map ai.rerank.doRerank to gen_ai.rerank', () => {
11+
expect(getSpanOpFromName('ai.rerank.doRerank')).toBe('gen_ai.rerank');
12+
});
13+
});
14+
});

packages/node/src/integrations/tracing/vercelai/instrumentation.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const INSTRUMENTED_METHODS = [
2626
'streamObject',
2727
'embed',
2828
'embedMany',
29+
'rerank',
2930
] as const;
3031

3132
interface MethodFirstArg extends Record<string, unknown> {
@@ -263,15 +264,21 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
263264
if (Object.prototype.toString.call(moduleExports) === '[object Module]') {
264265
// In ESM we take the usual route and just replace the exports we want to instrument
265266
for (const method of INSTRUMENTED_METHODS) {
266-
moduleExports[method] = generatePatch(moduleExports[method]);
267+
// Skip methods that don't exist in this version of the AI SDK (e.g., rerank was added in v6)
268+
if (moduleExports[method] != null) {
269+
moduleExports[method] = generatePatch(moduleExports[method]);
270+
}
267271
}
268272

269273
return moduleExports;
270274
} else {
271275
// In CJS we can't replace the exports in the original module because they
272276
// don't have setters, so we create a new object with the same properties
273277
const patchedModuleExports = INSTRUMENTED_METHODS.reduce((acc, curr) => {
274-
acc[curr] = generatePatch(moduleExports[curr]);
278+
// Skip methods that don't exist in this version of the AI SDK (e.g., rerank was added in v6)
279+
if (moduleExports[curr] != null) {
280+
acc[curr] = generatePatch(moduleExports[curr]);
281+
}
275282
return acc;
276283
}, {} as PatchedModuleExports);
277284

0 commit comments

Comments
 (0)