Skip to content

Commit 830aa2e

Browse files
committed
fix(tracing): Add missing attributes in vercel-ai spans (#18333)
Fix JS-1216
1 parent f31b899 commit 830aa2e

File tree

3 files changed

+103
-1
lines changed

3 files changed

+103
-1
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ import {
1010
import { getTruncatedJsonString } from '../ai/utils';
1111
import { toolCallSpanMap } from './constants';
1212
import type { TokenSummary } from './types';
13-
import { accumulateTokensForParent, applyAccumulatedTokens, convertAvailableToolsToJsonString } from './utils';
13+
import {
14+
accumulateTokensForParent,
15+
applyAccumulatedTokens,
16+
convertAvailableToolsToJsonString,
17+
requestMessagesFromPrompt,
18+
} from './utils';
1419
import type { ProviderMetadata } from './vercel-ai-attributes';
1520
import {
1621
AI_MODEL_ID_ATTRIBUTE,
@@ -141,6 +146,7 @@ function processEndedVercelAiSpan(span: SpanJSON): void {
141146
renameAttributeKey(attributes, AI_TOOL_CALL_RESULT_ATTRIBUTE, 'gen_ai.tool.output');
142147

143148
renameAttributeKey(attributes, AI_SCHEMA_ATTRIBUTE, 'gen_ai.request.schema');
149+
renameAttributeKey(attributes, AI_MODEL_ID_ATTRIBUTE, 'gen_ai.request.model');
144150

145151
addProviderMetadataToAttributes(attributes);
146152

@@ -206,6 +212,10 @@ function processGenerateSpan(span: Span, name: string, attributes: SpanAttribute
206212
if (attributes[AI_PROMPT_ATTRIBUTE]) {
207213
const truncatedPrompt = getTruncatedJsonString(attributes[AI_PROMPT_ATTRIBUTE] as string | string[]);
208214
span.setAttribute('gen_ai.prompt', truncatedPrompt);
215+
216+
if (!attributes['gen_ai.request.messages']) {
217+
requestMessagesFromPrompt(span, attributes[AI_PROMPT_ATTRIBUTE]);
218+
}
209219
}
210220
if (attributes[AI_MODEL_ID_ATTRIBUTE] && !attributes[GEN_AI_RESPONSE_MODEL_ATTRIBUTE]) {
211221
span.setAttribute(GEN_AI_RESPONSE_MODEL_ATTRIBUTE, attributes[AI_MODEL_ID_ATTRIBUTE]);

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TraceContext } from '../../types-hoist/context';
22
import type { Span, SpanJSON } from '../../types-hoist/span';
33
import { GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE } from '../ai/gen-ai-attributes';
4+
import { getTruncatedJsonString } from '../ai/utils';
45
import { toolCallSpanMap } from './constants';
56
import type { TokenSummary } from './types';
67

@@ -87,3 +88,39 @@ export function convertAvailableToolsToJsonString(tools: unknown[]): string {
8788
});
8889
return JSON.stringify(toolObjects);
8990
}
91+
92+
/**
93+
* Convert the prompt string to messages array
94+
*/
95+
export function convertPromptToMessages(prompt: string): { role: string; content: string }[] | undefined {
96+
try {
97+
const p = JSON.parse(prompt);
98+
if (!!p && typeof p === 'object') {
99+
const { prompt, system } = p;
100+
if (typeof prompt === 'string' || typeof system === 'string') {
101+
const messages: { role: string; content: string }[] = [];
102+
if (typeof system === 'string') {
103+
messages.push({ role: 'system', content: system });
104+
}
105+
if (typeof prompt === 'string') {
106+
messages.push({ role: 'user', content: prompt });
107+
}
108+
return messages.length ? messages : [];
109+
}
110+
}
111+
// eslint-disable-next-line no-empty
112+
} catch {}
113+
return undefined;
114+
}
115+
116+
/**
117+
* Generate a request.messages JSON array from the prompt field in the
118+
* invoke_agent op
119+
*/
120+
export function requestMessagesFromPrompt(span: Span, prompt: unknown): void {
121+
if (typeof prompt !== 'string') return;
122+
const maybeMessages = convertPromptToMessages(prompt);
123+
if (maybeMessages !== undefined) {
124+
span.setAttribute('gen_ai.request.messages', getTruncatedJsonString(maybeMessages));
125+
}
126+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { convertPromptToMessages } from '../../../src/tracing/vercel-ai/utils';
3+
4+
describe('vercel-ai-utils', () => {
5+
describe('convertPromptToMessages', () => {
6+
it('should convert a prompt with system to a messages array', () => {
7+
expect(
8+
convertPromptToMessages(
9+
JSON.stringify({
10+
system: 'You are a friendly robot',
11+
prompt: 'Hello, robot',
12+
}),
13+
),
14+
).toStrictEqual([
15+
{ role: 'system', content: 'You are a friendly robot' },
16+
{ role: 'user', content: 'Hello, robot' },
17+
]);
18+
});
19+
20+
it('should convert a system prompt to a messages array', () => {
21+
expect(
22+
convertPromptToMessages(
23+
JSON.stringify({
24+
system: 'You are a friendly robot',
25+
}),
26+
),
27+
).toStrictEqual([{ role: 'system', content: 'You are a friendly robot' }]);
28+
});
29+
30+
it('should convert a user only prompt to a messages array', () => {
31+
expect(
32+
convertPromptToMessages(
33+
JSON.stringify({
34+
prompt: 'Hello, robot',
35+
}),
36+
),
37+
).toStrictEqual([{ role: 'user', content: 'Hello, robot' }]);
38+
});
39+
40+
it('should ignore unexpected data', () => {
41+
expect(
42+
convertPromptToMessages(
43+
JSON.stringify({
44+
randomField: 'Hello, robot',
45+
nothing: 'that we know how to handle',
46+
}),
47+
),
48+
).toBe(undefined);
49+
});
50+
51+
it('should not break on invalid json', () => {
52+
expect(convertPromptToMessages('this is not json')).toBe(undefined);
53+
});
54+
});
55+
});

0 commit comments

Comments
 (0)