diff --git a/.size-limit.js b/.size-limit.js index 38a83445d021..5bf0e534fbda 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -326,7 +326,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '175 KB', + limit: '176 KB', }, { name: '@sentry/node - without tracing', diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts index 9fd05f83c5f9..a53f8986512a 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts @@ -34,14 +34,14 @@ test('should create AI spans with correct attributes', async ({ page }) => { expect(firstPipelineSpan?.data?.['vercel.ai.model.id']).toBe('mock-model-id'); expect(firstPipelineSpan?.data?.['vercel.ai.model.provider']).toBe('mock-provider'); expect(firstPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the first span?'); - expect(firstPipelineSpan?.data?.['gen_ai.response.text']).toBe('First span here!'); + expect(firstPipelineSpan?.data?.['gen_ai.output.messages']).toContain('First span here!'); expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10); expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */ // Second AI call - explicitly enabled telemetry const secondPipelineSpan = aiPipelineSpans[0]; expect(secondPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the second span?'); - expect(secondPipelineSpan?.data?.['gen_ai.response.text']).toContain('Second span here!'); + expect(secondPipelineSpan?.data?.['gen_ai.output.messages']).toContain('Second span here!'); // Third AI call - with tool calls /* const thirdPipelineSpan = aiPipelineSpans[2]; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts index f7dc95e7d00d..5c519cb89a03 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/ai-test.test.ts @@ -34,14 +34,14 @@ test('should create AI spans with correct attributes', async ({ page }) => { expect(firstPipelineSpan?.data?.['vercel.ai.model.id']).toBe('mock-model-id'); expect(firstPipelineSpan?.data?.['vercel.ai.model.provider']).toBe('mock-provider'); expect(firstPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the first span?'); - expect(firstPipelineSpan?.data?.['gen_ai.response.text']).toBe('First span here!'); + expect(firstPipelineSpan?.data?.['gen_ai.output.messages']).toContain('First span here!'); expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10); expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */ // Second AI call - explicitly enabled telemetry const secondPipelineSpan = aiPipelineSpans[0]; expect(secondPipelineSpan?.data?.['vercel.ai.prompt']).toContain('Where is the second span?'); - expect(secondPipelineSpan?.data?.['gen_ai.response.text']).toContain('Second span here!'); + expect(secondPipelineSpan?.data?.['gen_ai.output.messages']).toContain('Second span here!'); // Third AI call - with tool calls /* const thirdPipelineSpan = aiPipelineSpans[2]; diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs index 9bfdd4a9793a..b6abe6fdf673 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/scenario.mjs @@ -51,6 +51,7 @@ async function run() { }), tools: { getWeather: { + description: 'Get the current weather for a location', parameters: z.object({ location: z.string() }), execute: async args => { return `Weather in ${args.location}: Sunny, 72°F`; diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts index 2919815b8f0d..809ba2308622 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/test.ts @@ -5,16 +5,16 @@ import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, - GEN_AI_RESPONSE_TEXT_ATTRIBUTE, - GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_SYSTEM_INSTRUCTIONS_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE, + GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE, GEN_AI_TOOL_INPUT_ATTRIBUTE, GEN_AI_TOOL_NAME_ATTRIBUTE, GEN_AI_TOOL_OUTPUT_ATTRIBUTE, @@ -91,9 +91,10 @@ describe('Vercel AI integration', () => { data: { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, @@ -119,11 +120,12 @@ describe('Vercel AI integration', () => { data: { [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, @@ -201,6 +203,7 @@ describe('Vercel AI integration', () => { status: 'ok', }), // Seventh span - tool call execution span + // Note: gen_ai.tool.description is NOT present when sendDefaultPii: false because ai.prompt.tools is not recorded expect.objectContaining({ data: { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', @@ -220,7 +223,7 @@ describe('Vercel AI integration', () => { }; const EXPECTED_AVAILABLE_TOOLS_JSON = - '[{"type":"function","name":"getWeather","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]'; + '[{"type":"function","name":"getWeather","description":"Get the current weather for a location","parameters":{"type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"}}]'; const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { transaction: 'main', @@ -230,9 +233,10 @@ describe('Vercel AI integration', () => { data: { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the first span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'First span here!', [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, @@ -264,11 +268,12 @@ describe('Vercel AI integration', () => { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'First span here!', [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, @@ -302,9 +307,10 @@ describe('Vercel AI integration', () => { data: { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 30, @@ -335,11 +341,12 @@ describe('Vercel AI integration', () => { data: { [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, @@ -373,10 +380,10 @@ describe('Vercel AI integration', () => { data: { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in San Francisco?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Tool call completed!"},{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{ \\"location\\": \\"San Francisco\\" }"}],"finish_reason":"tool_call"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Tool call completed!', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, [GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE]: 40, @@ -408,12 +415,12 @@ describe('Vercel AI integration', () => { [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Tool call completed!"},{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{ \\"location\\": \\"San Francisco\\" }"}],"finish_reason":"tool_call"}]', [GEN_AI_REQUEST_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], [GEN_AI_RESPONSE_ID_ATTRIBUTE]: expect.any(String), [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Tool call completed!', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), [GEN_AI_SYSTEM_ATTRIBUTE]: 'mock-provider', [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 15, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 25, @@ -447,6 +454,7 @@ describe('Vercel AI integration', () => { expect.objectContaining({ data: { [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', + [GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE]: 'Get the current weather for a location', [GEN_AI_TOOL_INPUT_ATTRIBUTE]: expect.any(String), [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', [GEN_AI_TOOL_OUTPUT_ATTRIBUTE]: expect.any(String), @@ -809,7 +817,6 @@ describe('Vercel AI integration', () => { data: expect.objectContaining({ [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 3, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringMatching(/^\[.*"(?:text|content)":"C+".*\]$/), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Response to truncated messages', }), }), // Second call: Last message is small and kept intact @@ -819,7 +826,6 @@ describe('Vercel AI integration', () => { [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.stringContaining( 'This is a small message that fits within the limit', ), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'Response to small message', }), }), ]), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs index 9ef1b8000741..2c83234064ae 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/scenario.mjs @@ -47,6 +47,7 @@ async function run() { }), tools: { getWeather: tool({ + description: 'Get the current weather for a location', inputSchema: z.object({ location: z.string() }), execute: async ({ location }) => `Weather in ${location}: Sunny, 72°F`, }), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts index 7d981a878363..a84b80e9abc5 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v5/test.ts @@ -5,15 +5,15 @@ import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, - GEN_AI_RESPONSE_TEXT_ATTRIBUTE, - GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE, + GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE, GEN_AI_TOOL_INPUT_ATTRIBUTE, GEN_AI_TOOL_NAME_ATTRIBUTE, GEN_AI_TOOL_OUTPUT_ATTRIBUTE, @@ -93,7 +93,8 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.pipeline.name': 'generateText', 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, @@ -127,7 +128,8 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.response.finishReason': 'stop', 'vercel.ai.response.model': 'mock-model-id', 'vercel.ai.response.id': expect.any(String), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.timestamp': expect.any(String), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), @@ -196,6 +198,7 @@ describe('Vercel AI integration (V5)', () => { status: 'ok', }), // Seventh span - tool call execution span + // Note: gen_ai.tool.description is NOT present when sendDefaultPii: false because ai.prompt.tools is not recorded expect.objectContaining({ data: { 'vercel.ai.operationId': 'ai.toolCall', @@ -215,7 +218,7 @@ describe('Vercel AI integration (V5)', () => { }; const EXPECTED_AVAILABLE_TOOLS_JSON = - '[{"type":"function","name":"getWeather","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false}}]'; + '[{"type":"function","name":"getWeather","description":"Get the current weather for a location","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false}}]'; const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { transaction: 'main', @@ -230,8 +233,9 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"Where is the first span?"}]', [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the first span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'First span here!', 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', @@ -257,10 +261,11 @@ describe('Vercel AI integration (V5)', () => { [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.finishReason': 'stop', 'vercel.ai.response.id': expect.any(String), 'vercel.ai.response.model': 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'First span here!', 'vercel.ai.response.timestamp': expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, @@ -290,8 +295,9 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', @@ -323,7 +329,8 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.response.finishReason': 'stop', 'vercel.ai.response.model': 'mock-model-id', 'vercel.ai.response.id': expect.any(String), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.timestamp': expect.any(String), [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), @@ -349,8 +356,9 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"What is the weather in San Francisco?"}]', [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in San Francisco?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', 'vercel.ai.response.finishReason': 'tool-calls', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', @@ -375,14 +383,14 @@ describe('Vercel AI integration (V5)', () => { 'vercel.ai.pipeline.name': 'generateText.doGenerate', [GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE]: 1, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', 'vercel.ai.prompt.toolChoice': expect.any(String), [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, 'vercel.ai.response.finishReason': 'tool-calls', 'vercel.ai.response.id': expect.any(String), 'vercel.ai.response.model': 'mock-model-id', - // 'gen_ai.response.text': 'Tool call completed!', // TODO: look into why this is not being set 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], @@ -406,6 +414,7 @@ describe('Vercel AI integration (V5)', () => { data: expect.objectContaining({ 'vercel.ai.operationId': 'ai.toolCall', [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', + [GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE]: 'Get the current weather for a location', [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', [GEN_AI_TOOL_INPUT_ATTRIBUTE]: expect.any(String), [GEN_AI_TOOL_OUTPUT_ATTRIBUTE]: expect.any(String), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/scenario.mjs index 66233d1dabe5..ee2dc802cd9c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/scenario.mjs @@ -62,6 +62,7 @@ async function run() { }), tools: { getWeather: tool({ + description: 'Get the current weather for a location', inputSchema: z.object({ location: z.string() }), execute: async ({ location }) => `Weather in ${location}: Sunny, 72°F`, }), diff --git a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts index 2a213f39410d..39ee00254373 100644 --- a/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/vercelai/v6/test.ts @@ -4,15 +4,15 @@ import { afterAll, describe, expect } from 'vitest'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE, GEN_AI_RESPONSE_ID_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, - GEN_AI_RESPONSE_TEXT_ATTRIBUTE, - GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, GEN_AI_SYSTEM_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE, + GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE, GEN_AI_TOOL_INPUT_ATTRIBUTE, GEN_AI_TOOL_NAME_ATTRIBUTE, GEN_AI_TOOL_OUTPUT_ATTRIBUTE, @@ -95,10 +95,11 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', 'vercel.ai.request.headers.user-agent': expect.any(String), 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, @@ -129,9 +130,10 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.response.finishReason': 'stop', 'vercel.ai.response.model': 'mock-model-id', 'vercel.ai.response.id': expect.any(String), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), 'vercel.ai.response.timestamp': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, @@ -199,6 +201,7 @@ describe('Vercel AI integration (V6)', () => { status: 'ok', }), // Seventh span - tool call execution span + // Note: gen_ai.tool.description is NOT present when sendDefaultPii: false because ai.prompt.tools is not recorded expect.objectContaining({ data: expect.objectContaining({ 'vercel.ai.operationId': 'ai.toolCall', @@ -218,7 +221,7 @@ describe('Vercel AI integration (V6)', () => { }; const EXPECTED_AVAILABLE_TOOLS_JSON = - '[{"type":"function","name":"getWeather","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false}}]'; + '[{"type":"function","name":"getWeather","description":"Get the current weather for a location","inputSchema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"location":{"type":"string"}},"required":["location"],"additionalProperties":false}}]'; const EXPECTED_TRANSACTION_DEFAULT_PII_TRUE = { transaction: 'main', @@ -233,8 +236,9 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"Where is the first span?"}]', 'vercel.ai.request.headers.user-agent': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the first span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'First span here!', 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', @@ -260,10 +264,11 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.request.headers.user-agent': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":[{"type":"text","text":"Where is the first span?"}]}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"First span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.finishReason': 'stop', 'vercel.ai.response.id': expect.any(String), 'vercel.ai.response.model': 'mock-model-id', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: 'First span here!', 'vercel.ai.response.timestamp': expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, @@ -293,8 +298,9 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"Where is the second span?"}]', 'vercel.ai.request.headers.user-agent': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"Where is the second span?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', 'vercel.ai.response.finishReason': 'stop', - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', @@ -327,9 +333,10 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.response.finishReason': 'stop', 'vercel.ai.response.model': 'mock-model-id', 'vercel.ai.response.id': expect.any(String), - [GEN_AI_RESPONSE_TEXT_ATTRIBUTE]: expect.any(String), 'vercel.ai.response.timestamp': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"text","content":"Second span here!"}],"finish_reason":"stop"}]', [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['stop'], [GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE]: 10, [GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE]: 20, @@ -352,8 +359,9 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.prompt': '[{"role":"user","content":"What is the weather in San Francisco?"}]', 'vercel.ai.request.headers.user-agent': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: '[{"role":"user","content":"What is the weather in San Francisco?"}]', + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', 'vercel.ai.response.finishReason': 'tool-calls', - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_MODEL_ATTRIBUTE]: 'mock-model-id', @@ -378,14 +386,14 @@ describe('Vercel AI integration (V6)', () => { 'vercel.ai.pipeline.name': 'generateText.doGenerate', 'vercel.ai.request.headers.user-agent': expect.any(String), [GEN_AI_INPUT_MESSAGES_ATTRIBUTE]: expect.any(String), + [GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE]: + '[{"role":"assistant","parts":[{"type":"tool_call","id":"call-1","name":"getWeather","arguments":"{\\"location\\":\\"San Francisco\\"}"}],"finish_reason":"tool_call"}]', 'vercel.ai.prompt.toolChoice': expect.any(String), [GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]: EXPECTED_AVAILABLE_TOOLS_JSON, 'vercel.ai.response.finishReason': 'tool-calls', 'vercel.ai.response.id': expect.any(String), 'vercel.ai.response.model': 'mock-model-id', - // 'gen_ai.response.text': 'Tool call completed!', // TODO: look into why this is not being set 'vercel.ai.response.timestamp': expect.any(String), - [GEN_AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]: expect.any(String), 'vercel.ai.settings.maxRetries': 2, 'vercel.ai.streaming': false, [GEN_AI_RESPONSE_FINISH_REASONS_ATTRIBUTE]: ['tool-calls'], @@ -409,6 +417,7 @@ describe('Vercel AI integration (V6)', () => { data: expect.objectContaining({ 'vercel.ai.operationId': 'ai.toolCall', [GEN_AI_TOOL_CALL_ID_ATTRIBUTE]: 'call-1', + [GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE]: 'Get the current weather for a location', [GEN_AI_TOOL_NAME_ATTRIBUTE]: 'getWeather', [GEN_AI_TOOL_INPUT_ATTRIBUTE]: expect.any(String), [GEN_AI_TOOL_OUTPUT_ATTRIBUTE]: expect.any(String), diff --git a/packages/core/src/tracing/ai/gen-ai-attributes.ts b/packages/core/src/tracing/ai/gen-ai-attributes.ts index dc88e6315852..4f8d4b0161c2 100644 --- a/packages/core/src/tracing/ai/gen-ai-attributes.ts +++ b/packages/core/src/tracing/ai/gen-ai-attributes.ts @@ -126,6 +126,14 @@ export const GEN_AI_INPUT_MESSAGES_ORIGINAL_LENGTH_ATTRIBUTE = 'sentry.sdk_meta. */ export const GEN_AI_INPUT_MESSAGES_ATTRIBUTE = 'gen_ai.input.messages'; +/** + * The model's response messages including text and tool calls + * Only recorded when recordOutputs is enabled + * Format: stringified array of message objects with role, parts, and finish_reason + * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-output-messages + */ +export const GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE = 'gen_ai.output.messages'; + /** * The system instructions extracted from system messages * Only recorded when recordInputs is enabled @@ -269,6 +277,12 @@ export const GEN_AI_TOOL_INPUT_ATTRIBUTE = 'gen_ai.tool.input'; */ export const GEN_AI_TOOL_OUTPUT_ATTRIBUTE = 'gen_ai.tool.output'; +/** + * The description of the tool being used + * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/#gen-ai-tool-description + */ +export const GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE = 'gen_ai.tool.description'; + // ============================================================================= // OPENAI-SPECIFIC ATTRIBUTES // ============================================================================= diff --git a/packages/core/src/tracing/ai/mediaStripping.ts b/packages/core/src/tracing/ai/mediaStripping.ts index f4870cd5a9de..cb8e5d7b959e 100644 --- a/packages/core/src/tracing/ai/mediaStripping.ts +++ b/packages/core/src/tracing/ai/mediaStripping.ts @@ -47,6 +47,8 @@ export function isContentMedia(part: unknown): part is ContentMedia { hasInputAudio(part) || hasFileData(part) || hasMediaTypeData(part) || + hasVercelFileData(part) || + hasVercelImageData(part) || hasBlobOrBase64Type(part) || hasB64Json(part) || hasImageGenerationResult(part) || @@ -113,6 +115,41 @@ function hasMediaTypeData(part: NonNullable): part is { media_type: str return 'media_type' in part && typeof part.media_type === 'string' && 'data' in part; } +/** + * Check for Vercel AI SDK file format: { type: "file", mediaType: "...", data: "..." } + * Only matches base64/binary data, not HTTP/HTTPS URLs (which should be preserved). + */ +function hasVercelFileData(part: NonNullable): part is { type: 'file'; mediaType: string; data: string } { + return ( + 'type' in part && + part.type === 'file' && + 'mediaType' in part && + typeof part.mediaType === 'string' && + 'data' in part && + typeof part.data === 'string' && + // Only strip base64/binary data, not HTTP/HTTPS URLs which should be preserved as references + !part.data.startsWith('http://') && + !part.data.startsWith('https://') + ); +} + +/** + * Check for Vercel AI SDK image format: { type: "image", image: "base64...", mimeType?: "..." } + * Only matches base64/data URIs, not HTTP/HTTPS URLs (which should be preserved). + * Note: mimeType is optional in Vercel AI SDK image parts. + */ +function hasVercelImageData(part: NonNullable): part is { type: 'image'; image: string; mimeType?: string } { + return ( + 'type' in part && + part.type === 'image' && + 'image' in part && + typeof part.image === 'string' && + // Only strip base64/data URIs, not HTTP/HTTPS URLs which should be preserved as references + !part.image.startsWith('http://') && + !part.image.startsWith('https://') + ); +} + function hasBlobOrBase64Type(part: NonNullable): part is { type: 'blob' | 'base64'; content: string } { return 'type' in part && (part.type === 'blob' || part.type === 'base64'); } @@ -131,7 +168,7 @@ function hasDataUri(part: NonNullable): part is { uri: string } { const REMOVED_STRING = '[Blob substitute]'; -const MEDIA_FIELDS = ['image_url', 'data', 'content', 'b64_json', 'result', 'uri'] as const; +const MEDIA_FIELDS = ['image_url', 'data', 'content', 'b64_json', 'result', 'uri', 'image'] as const; /** * Replace inline binary data in a single media content part with a placeholder. diff --git a/packages/core/src/tracing/ai/messageTruncation.ts b/packages/core/src/tracing/ai/messageTruncation.ts index 499d25ee6e47..aade37b84474 100644 --- a/packages/core/src/tracing/ai/messageTruncation.ts +++ b/packages/core/src/tracing/ai/messageTruncation.ts @@ -229,11 +229,53 @@ function truncatePartsMessage(message: PartsMessage, maxBytes: number): unknown[ } } +/** + * Truncate a message with `content: [...]` array format (Vercel AI SDK, OpenAI multimodal). + * Content arrays contain parts like `{ type: "text", text: "..." }`. + * + * @param message - Message with content array property + * @param maxBytes - Maximum byte limit + * @returns Array with truncated message, or empty array if it doesn't fit + */ +function truncateContentArrayMessage(message: ContentArrayMessage, maxBytes: number): unknown[] { + const { content } = message; + + // Find the first text part to truncate + const textPartIndex = content.findIndex( + part => part && typeof part === 'object' && 'type' in part && part.type === 'text' && 'text' in part, + ); + + if (textPartIndex === -1) { + // No text part found, cannot truncate safely + return []; + } + + const textPart = content[textPartIndex] as { type: string; text: string }; + + // Calculate overhead (message structure with empty text) + const emptyContent = content.map((part, i) => (i === textPartIndex ? { ...textPart, text: '' } : part)); + const emptyMessage = { ...message, content: emptyContent }; + const overhead = jsonBytes(emptyMessage); + const availableForText = maxBytes - overhead; + + if (availableForText <= 0) { + return []; + } + + const truncatedText = truncateTextByBytes(textPart.text, availableForText); + const truncatedContent = content.map((part, i) => + i === textPartIndex ? { ...textPart, text: truncatedText } : part, + ); + + return [{ ...message, content: truncatedContent }]; +} + /** * Truncate a single message to fit within maxBytes. * - * Supports two message formats: + * Supports three message formats: * - OpenAI/Anthropic: `{ ..., content: string }` + * - Vercel AI/OpenAI multimodal: `{ ..., content: Array<{type, text?, ...}> }` * - Google GenAI: `{ ..., parts: Array }` * * @param message - The message to truncate @@ -257,6 +299,10 @@ function truncateSingleMessage(message: unknown, maxBytes: number): unknown[] { return truncateContentMessage(message, maxBytes); } + if (isContentArrayMessage(message)) { + return truncateContentArrayMessage(message, maxBytes); + } + if (isPartsMessage(message)) { return truncatePartsMessage(message, maxBytes); } diff --git a/packages/core/src/tracing/vercel-ai/index.ts b/packages/core/src/tracing/vercel-ai/index.ts index 919c06eb12d6..8b62079295af 100644 --- a/packages/core/src/tracing/vercel-ai/index.ts +++ b/packages/core/src/tracing/vercel-ai/index.ts @@ -7,9 +7,12 @@ import { spanToJSON } from '../../utils/spanUtils'; import { GEN_AI_INPUT_MESSAGES_ATTRIBUTE, GEN_AI_OPERATION_NAME_ATTRIBUTE, + GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE, + GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE, GEN_AI_REQUEST_MODEL_ATTRIBUTE, GEN_AI_RESPONSE_MODEL_ATTRIBUTE, GEN_AI_TOOL_CALL_ID_ATTRIBUTE, + GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE, GEN_AI_TOOL_INPUT_ATTRIBUTE, GEN_AI_TOOL_NAME_ATTRIBUTE, GEN_AI_TOOL_OUTPUT_ATTRIBUTE, @@ -42,6 +45,7 @@ import { AI_OPERATION_ID_ATTRIBUTE, AI_PROMPT_MESSAGES_ATTRIBUTE, AI_PROMPT_TOOLS_ATTRIBUTE, + AI_RESPONSE_FINISH_REASON_ATTRIBUTE, AI_RESPONSE_OBJECT_ATTRIBUTE, AI_RESPONSE_PROVIDER_METADATA_ATTRIBUTE, AI_RESPONSE_TEXT_ATTRIBUTE, @@ -124,13 +128,21 @@ function vercelAiEventProcessor(event: Event): Event { accumulateTokensForParent(span, tokenAccumulator); } - // Second pass: apply accumulated token data to parent spans + // Second pass: apply tool descriptions and accumulated tokens for (const span of event.spans) { - if (span.op !== 'gen_ai.invoke_agent') { - continue; + if (span.op === 'gen_ai.execute_tool') { + const toolName = span.data[GEN_AI_TOOL_NAME_ATTRIBUTE]; + if (typeof toolName === 'string') { + const description = findToolDescription(event.spans, toolName); + if (description) { + span.data[GEN_AI_TOOL_DESCRIPTION_ATTRIBUTE] = description; + } + } } - applyAccumulatedTokens(span, tokenAccumulator); + if (span.op === 'gen_ai.invoke_agent') { + applyAccumulatedTokens(span, tokenAccumulator); + } } // Also apply to root when it is the invoke_agent pipeline @@ -142,6 +154,140 @@ function vercelAiEventProcessor(event: Event): Event { return event; } + +/** + * Finds a tool description by scanning spans for gen_ai.request.available_tools + * (already processed from ai.prompt.tools in the first pass). + */ +function findToolDescription(spans: SpanJSON[], toolName: string): string | undefined { + for (const span of spans) { + const availableTools = span.data[GEN_AI_REQUEST_AVAILABLE_TOOLS_ATTRIBUTE]; + if (typeof availableTools !== 'string') { + continue; + } + try { + const tools = JSON.parse(availableTools) as Array<{ name?: string; description?: string }>; + const tool = tools.find(t => t.name === toolName); + if (tool?.description) { + return tool.description; + } + } catch { + // ignore + } + } + return undefined; +} + +/** + * Tool call structure from Vercel AI SDK + * Note: V5/V6 use 'input' for arguments, V4 and earlier use 'args' + */ +interface VercelToolCall { + toolCallId: string; + toolName: string; + input?: Record | string; // V5/V6 + args?: string; // V4 and earlier +} + +/** + * Normalize finish reason to match OpenTelemetry semantic conventions. + * Valid values: "stop", "length", "content_filter", "tool_call", "error" + * + * Vercel AI SDK uses "tool-calls" (plural, with hyphen) which we map to "tool_call". + */ +function normalizeFinishReason(finishReason: unknown): string { + if (typeof finishReason !== 'string') { + return 'stop'; + } + + // Map Vercel AI SDK finish reasons to OpenTelemetry semantic convention values + switch (finishReason) { + case 'tool-calls': + return 'tool_call'; + case 'stop': + case 'length': + case 'content_filter': + case 'error': + return finishReason; + default: + // For unknown values, return as-is (schema allows arbitrary strings) + return finishReason; + } +} + +/** + * Build gen_ai.output.messages from ai.response.text and/or ai.response.toolCalls + * + * Format follows OpenTelemetry semantic conventions: + * [{"role": "assistant", "parts": [...], "finish_reason": "stop"}] + * + * Parts can be: + * - {"type": "text", "content": "..."} + * - {"type": "tool_call", "id": "...", "name": "...", "arguments": "..."} + */ +function buildOutputMessages(attributes: Record): void { + const responseText = attributes[AI_RESPONSE_TEXT_ATTRIBUTE]; + const responseToolCalls = attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]; + const finishReason = attributes[AI_RESPONSE_FINISH_REASON_ATTRIBUTE]; + + // Skip if neither text nor tool calls are present + if (responseText == null && responseToolCalls == null) { + return; + } + + const parts: Array> = []; + + // Add text part if present + if (typeof responseText === 'string' && responseText.length > 0) { + parts.push({ + type: 'text', + content: responseText, + }); + } + + // Add tool call parts if present + if (responseToolCalls != null) { + try { + // Tool calls can be a string (JSON) or already parsed array + const toolCalls: VercelToolCall[] = + typeof responseToolCalls === 'string' ? JSON.parse(responseToolCalls) : responseToolCalls; + + if (Array.isArray(toolCalls)) { + for (const toolCall of toolCalls) { + // V5/V6 use 'input', V4 and earlier use 'args' + const args = toolCall.input ?? toolCall.args; + parts.push({ + type: 'tool_call', + id: toolCall.toolCallId, + name: toolCall.toolName, + arguments: typeof args === 'string' ? args : JSON.stringify(args), + }); + } + } + } catch { + // Ignore parsing errors + } + } + + // Only set output messages and delete source attributes if we have parts + // This ensures we don't lose telemetry data if parsing fails + if (parts.length > 0) { + const outputMessage = { + role: 'assistant', + parts, + finish_reason: normalizeFinishReason(finishReason), + }; + + attributes[GEN_AI_OUTPUT_MESSAGES_ATTRIBUTE] = JSON.stringify([outputMessage]); + + // Remove the source attributes since they're now captured in gen_ai.output.messages + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete attributes[AI_RESPONSE_TEXT_ATTRIBUTE]; + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete attributes[AI_RESPONSE_TOOL_CALLS_ATTRIBUTE]; + } +} + /** * Post-process spans emitted by the Vercel AI SDK. */ @@ -196,8 +342,11 @@ function processEndedVercelAiSpan(span: SpanJSON): void { delete attributes[OPERATION_NAME_ATTRIBUTE]; } renameAttributeKey(attributes, AI_PROMPT_MESSAGES_ATTRIBUTE, GEN_AI_INPUT_MESSAGES_ATTRIBUTE); - renameAttributeKey(attributes, AI_RESPONSE_TEXT_ATTRIBUTE, 'gen_ai.response.text'); - renameAttributeKey(attributes, AI_RESPONSE_TOOL_CALLS_ATTRIBUTE, 'gen_ai.response.tool_calls'); + + // Build gen_ai.output.messages from response text and/or tool calls + // Note: buildOutputMessages also removes the source attributes when output is successfully generated + buildOutputMessages(attributes); + renameAttributeKey(attributes, AI_RESPONSE_OBJECT_ATTRIBUTE, 'gen_ai.response.object'); renameAttributeKey(attributes, AI_PROMPT_TOOLS_ATTRIBUTE, 'gen_ai.request.available_tools');