Skip to content

Commit 4d9f995

Browse files
authored
feat(vercel): add gen.ai.input.messages + gen.ai.output.messages (#734)
1 parent 768ad76 commit 4d9f995

File tree

5 files changed

+682
-7
lines changed

5 files changed

+682
-7
lines changed

packages/ai-semantic-conventions/src/SemanticAttributes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const SpanAttributes = {
2222
LLM_REQUEST_TOP_P: "gen_ai.request.top_p",
2323
LLM_PROMPTS: "gen_ai.prompt",
2424
LLM_COMPLETIONS: "gen_ai.completion",
25+
LLM_INPUT_MESSAGES: "gen_ai.input.messages",
26+
LLM_OUTPUT_MESSAGES: "gen_ai.output.messages",
2527
LLM_RESPONSE_MODEL: "gen_ai.response.model",
2628
LLM_USAGE_PROMPT_TOKENS: "gen_ai.usage.prompt_tokens",
2729
LLM_USAGE_COMPLETION_TOKENS: "gen_ai.usage.completion_tokens",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
{
2+
"log": {
3+
"_recordingName": "Test AI SDK Integration with Recording/should set LLM_INPUT_MESSAGES and LLM_OUTPUT_MESSAGES attributes for chat completions",
4+
"creator": {
5+
"comment": "persister:fs",
6+
"name": "Polly.JS",
7+
"version": "6.0.6"
8+
},
9+
"entries": [
10+
{
11+
"_id": "9e0d86c7cc2553331485a7095b7c79c7",
12+
"_order": 0,
13+
"cache": {},
14+
"request": {
15+
"bodySize": 128,
16+
"cookies": [],
17+
"headers": [
18+
{
19+
"name": "content-type",
20+
"value": "application/json"
21+
}
22+
],
23+
"headersSize": 273,
24+
"httpVersion": "HTTP/1.1",
25+
"method": "POST",
26+
"postData": {
27+
"mimeType": "application/json",
28+
"params": [],
29+
"text": "{\"model\":\"gpt-3.5-turbo\",\"input\":[{\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"What is 2+2? Give a brief answer.\"}]}]}"
30+
},
31+
"queryString": [],
32+
"url": "https://api.openai.com/v1/responses"
33+
},
34+
"response": {
35+
"bodySize": 1368,
36+
"content": {
37+
"mimeType": "application/json",
38+
"size": 1368,
39+
"text": "{\n \"id\": \"resp_68ab8bb3a6a48196b327616ac8519242061ee2084aba21f9\",\n \"object\": \"response\",\n \"created_at\": 1756072883,\n \"status\": \"completed\",\n \"background\": false,\n \"error\": null,\n \"incomplete_details\": null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\": null,\n \"model\": \"gpt-3.5-turbo-0125\",\n \"output\": [\n {\n \"id\": \"msg_68ab8bb44de48196bc6fd27015c46560061ee2084aba21f9\",\n \"type\": \"message\",\n \"status\": \"completed\",\n \"content\": [\n {\n \"type\": \"output_text\",\n \"annotations\": [],\n \"logprobs\": [],\n \"text\": \"2+2 equals 4.\"\n }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\": true,\n \"previous_response_id\": null,\n \"prompt_cache_key\": null,\n \"reasoning\": {\n \"effort\": null,\n \"summary\": null\n },\n \"safety_identifier\": null,\n \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\": \"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\": 0,\n \"top_p\": 1.0,\n \"truncation\": \"disabled\",\n \"usage\": {\n \"input_tokens\": 19,\n \"input_tokens_details\": {\n \"cached_tokens\": 0\n },\n \"output_tokens\": 8,\n \"output_tokens_details\": {\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 27\n },\n \"user\": null,\n \"metadata\": {}\n}"
40+
},
41+
"cookies": [
42+
{
43+
"domain": ".api.openai.com",
44+
"httpOnly": true,
45+
"name": "_cfuvid",
46+
"path": "/",
47+
"sameSite": "None",
48+
"secure": true,
49+
"value": "YvH22Dd0_.ZSsD0IiE4C_SZLye9RnIosHw8rvV.a5xI-1756072884584-0.0.1.1-604800000"
50+
}
51+
],
52+
"headers": [
53+
{
54+
"name": "alt-svc",
55+
"value": "h3=\":443\"; ma=86400"
56+
},
57+
{
58+
"name": "cf-cache-status",
59+
"value": "DYNAMIC"
60+
},
61+
{
62+
"name": "cf-ray",
63+
"value": "974620c03ebe7da4-TLV"
64+
},
65+
{
66+
"name": "connection",
67+
"value": "keep-alive"
68+
},
69+
{
70+
"name": "content-encoding",
71+
"value": "br"
72+
},
73+
{
74+
"name": "content-type",
75+
"value": "application/json"
76+
},
77+
{
78+
"name": "date",
79+
"value": "Sun, 24 Aug 2025 22:01:24 GMT"
80+
},
81+
{
82+
"name": "openai-organization",
83+
"value": "traceloop"
84+
},
85+
{
86+
"name": "openai-processing-ms",
87+
"value": "860"
88+
},
89+
{
90+
"name": "openai-project",
91+
"value": "proj_tzz1TbPPOXaf6j9tEkVUBIAa"
92+
},
93+
{
94+
"name": "openai-version",
95+
"value": "2020-10-01"
96+
},
97+
{
98+
"name": "server",
99+
"value": "cloudflare"
100+
},
101+
{
102+
"name": "set-cookie",
103+
"value": "_cfuvid=YvH22Dd0_.ZSsD0IiE4C_SZLye9RnIosHw8rvV.a5xI-1756072884584-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None"
104+
},
105+
{
106+
"name": "strict-transport-security",
107+
"value": "max-age=31536000; includeSubDomains; preload"
108+
},
109+
{
110+
"name": "transfer-encoding",
111+
"value": "chunked"
112+
},
113+
{
114+
"name": "x-content-type-options",
115+
"value": "nosniff"
116+
},
117+
{
118+
"name": "x-envoy-upstream-service-time",
119+
"value": "866"
120+
},
121+
{
122+
"name": "x-ratelimit-limit-requests",
123+
"value": "10000"
124+
},
125+
{
126+
"name": "x-ratelimit-limit-tokens",
127+
"value": "50000000"
128+
},
129+
{
130+
"name": "x-ratelimit-remaining-requests",
131+
"value": "9999"
132+
},
133+
{
134+
"name": "x-ratelimit-remaining-tokens",
135+
"value": "49999976"
136+
},
137+
{
138+
"name": "x-ratelimit-reset-requests",
139+
"value": "6ms"
140+
},
141+
{
142+
"name": "x-ratelimit-reset-tokens",
143+
"value": "0s"
144+
},
145+
{
146+
"name": "x-request-id",
147+
"value": "req_ca7b8860830628b84350a7485599e844"
148+
}
149+
],
150+
"headersSize": 953,
151+
"httpVersion": "HTTP/1.1",
152+
"redirectURL": "",
153+
"status": 200,
154+
"statusText": "OK"
155+
},
156+
"startedDateTime": "2025-08-24T22:01:23.016Z",
157+
"time": 1409,
158+
"timings": {
159+
"blocked": -1,
160+
"connect": -1,
161+
"dns": -1,
162+
"receive": 0,
163+
"send": 0,
164+
"ssl": -1,
165+
"wait": 1409
166+
}
167+
}
168+
],
169+
"pages": [],
170+
"version": "1.2"
171+
}
172+
}

packages/traceloop-sdk/src/lib/tracing/ai-sdk-transformations.ts

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const AI_USAGE_PROMPT_TOKENS = "ai.usage.promptTokens";
1919
const AI_USAGE_COMPLETION_TOKENS = "ai.usage.completionTokens";
2020
const AI_MODEL_PROVIDER = "ai.model.provider";
2121
const AI_PROMPT_TOOLS = "ai.prompt.tools";
22+
const TYPE_TEXT = "text";
23+
const TYPE_TOOL_CALL = "tool_call";
24+
const ROLE_ASSISTANT = "assistant";
25+
const ROLE_USER = "user";
2226

2327
// Vendor mapping from AI SDK provider prefixes to standardized LLM_SYSTEM values
2428
// Uses prefixes to match AI SDK patterns like "openai.chat", "anthropic.messages", etc.
@@ -55,7 +59,21 @@ const transformResponseText = (attributes: Record<string, any>): void => {
5559
if (AI_RESPONSE_TEXT in attributes) {
5660
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] =
5761
attributes[AI_RESPONSE_TEXT];
58-
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant";
62+
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = ROLE_ASSISTANT;
63+
64+
const outputMessage = {
65+
role: ROLE_ASSISTANT,
66+
parts: [
67+
{
68+
type: TYPE_TEXT,
69+
content: attributes[AI_RESPONSE_TEXT],
70+
},
71+
],
72+
};
73+
attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] = JSON.stringify([
74+
outputMessage,
75+
]);
76+
5977
delete attributes[AI_RESPONSE_TEXT];
6078
}
6179
};
@@ -64,7 +82,21 @@ const transformResponseObject = (attributes: Record<string, any>): void => {
6482
if (AI_RESPONSE_OBJECT in attributes) {
6583
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.content`] =
6684
attributes[AI_RESPONSE_OBJECT];
67-
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant";
85+
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = ROLE_ASSISTANT;
86+
87+
const outputMessage = {
88+
role: ROLE_ASSISTANT,
89+
parts: [
90+
{
91+
type: TYPE_TEXT,
92+
content: attributes[AI_RESPONSE_OBJECT],
93+
},
94+
],
95+
};
96+
attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] = JSON.stringify([
97+
outputMessage,
98+
]);
99+
68100
delete attributes[AI_RESPONSE_OBJECT];
69101
}
70102
};
@@ -76,8 +108,9 @@ const transformResponseToolCalls = (attributes: Record<string, any>): void => {
76108
attributes[AI_RESPONSE_TOOL_CALLS] as string,
77109
);
78110

79-
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = "assistant";
111+
attributes[`${SpanAttributes.LLM_COMPLETIONS}.0.role`] = ROLE_ASSISTANT;
80112

113+
const toolCallParts: any[] = [];
81114
toolCalls.forEach((toolCall: any, index: number) => {
82115
if (toolCall.toolCallType === "function") {
83116
attributes[
@@ -86,9 +119,27 @@ const transformResponseToolCalls = (attributes: Record<string, any>): void => {
86119
attributes[
87120
`${SpanAttributes.LLM_COMPLETIONS}.0.tool_calls.${index}.arguments`
88121
] = toolCall.args;
122+
123+
toolCallParts.push({
124+
type: TYPE_TOOL_CALL,
125+
tool_call: {
126+
name: toolCall.toolName,
127+
arguments: toolCall.args,
128+
},
129+
});
89130
}
90131
});
91132

133+
if (toolCallParts.length > 0) {
134+
const outputMessage = {
135+
role: ROLE_ASSISTANT,
136+
parts: toolCallParts,
137+
};
138+
attributes[SpanAttributes.LLM_OUTPUT_MESSAGES] = JSON.stringify([
139+
outputMessage,
140+
]);
141+
}
142+
92143
delete attributes[AI_RESPONSE_TOOL_CALLS];
93144
} catch {
94145
// Ignore parsing errors
@@ -100,7 +151,10 @@ const processMessageContent = (content: any): string => {
100151
if (Array.isArray(content)) {
101152
const textItems = content.filter(
102153
(item: any) =>
103-
item && typeof item === "object" && item.type === "text" && item.text,
154+
item &&
155+
typeof item === "object" &&
156+
item.type === TYPE_TEXT &&
157+
item.text,
104158
);
105159

106160
if (textItems.length > 0) {
@@ -112,7 +166,7 @@ const processMessageContent = (content: any): string => {
112166
}
113167

114168
if (content && typeof content === "object") {
115-
if (content.type === "text" && content.text) {
169+
if (content.type === TYPE_TEXT && content.text) {
116170
return content.text;
117171
}
118172
return JSON.stringify(content);
@@ -126,7 +180,7 @@ const processMessageContent = (content: any): string => {
126180
(item: any) =>
127181
item &&
128182
typeof item === "object" &&
129-
item.type === "text" &&
183+
item.type === TYPE_TEXT &&
130184
item.text,
131185
);
132186

@@ -205,12 +259,32 @@ const transformPrompts = (attributes: Record<string, any>): void => {
205259
}
206260

207261
const messages = JSON.parse(jsonString);
262+
const inputMessages: any[] = [];
263+
208264
messages.forEach((msg: { role: string; content: any }, index: number) => {
209265
const processedContent = processMessageContent(msg.content);
210266
const contentKey = `${SpanAttributes.LLM_PROMPTS}.${index}.content`;
211267
attributes[contentKey] = processedContent;
212268
attributes[`${SpanAttributes.LLM_PROMPTS}.${index}.role`] = msg.role;
269+
270+
// Add to OpenTelemetry standard gen_ai.input.messages format
271+
inputMessages.push({
272+
role: msg.role,
273+
parts: [
274+
{
275+
type: TYPE_TEXT,
276+
content: processedContent,
277+
},
278+
],
279+
});
213280
});
281+
282+
// Set the OpenTelemetry standard input messages attribute
283+
if (inputMessages.length > 0) {
284+
attributes[SpanAttributes.LLM_INPUT_MESSAGES] =
285+
JSON.stringify(inputMessages);
286+
}
287+
214288
delete attributes[AI_PROMPT_MESSAGES];
215289
} catch {
216290
// Ignore parsing errors
@@ -223,7 +297,21 @@ const transformPrompts = (attributes: Record<string, any>): void => {
223297
if (promptData.prompt && typeof promptData.prompt === "string") {
224298
attributes[`${SpanAttributes.LLM_PROMPTS}.0.content`] =
225299
promptData.prompt;
226-
attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = "user";
300+
attributes[`${SpanAttributes.LLM_PROMPTS}.0.role`] = ROLE_USER;
301+
302+
const inputMessage = {
303+
role: ROLE_USER,
304+
parts: [
305+
{
306+
type: TYPE_TEXT,
307+
content: promptData.prompt,
308+
},
309+
],
310+
};
311+
attributes[SpanAttributes.LLM_INPUT_MESSAGES] = JSON.stringify([
312+
inputMessage,
313+
]);
314+
227315
delete attributes[AI_PROMPT];
228316
}
229317
} catch {

0 commit comments

Comments
 (0)