Skip to content

Commit 4e3ffd5

Browse files
authored
feat(ai-sdk): migrate ai-sdk to opentelemtry 1.40 semantic conventions (#924)
1 parent 4c8becd commit 4e3ffd5

File tree

77 files changed

+3963
-5581
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+3963
-5581
lines changed

packages/instrumentation-utils/src/content-block-mappers.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,3 +321,206 @@ export const mapBedrockContentBlock = (block: any): object => {
321321
return { type: block.type, ...block };
322322
}
323323
};
324+
325+
// =============================================================================
326+
// Vercel AI SDK
327+
// =============================================================================
328+
// Maps a single Vercel AI SDK content part (from ai.prompt.messages span attr)
329+
// to its OTel-compliant part object. Verified against:
330+
// - AI SDK v6 ToolCallPart / ToolResultPart / ImagePart / FilePart / ReasoningPart types
331+
// - OTel gen_ai input/output messages JSON schemas (v1.40)
332+
//
333+
// text → TextPart { type:"text", content }
334+
// reasoning → ReasoningPart { type:"reasoning", content }
335+
// tool-call → ToolCallRequestPart { type:"tool_call", id?, name, arguments? }
336+
// tool-result→ ToolCallResponsePart { type:"tool_call_response", id?, response }
337+
// image (string data URI) → BlobPart { modality:"image", mime_type?, content }
338+
// image (string URL) → UriPart { modality:"image", uri }
339+
// image (URL object) → UriPart { modality:"image", uri: url.href }
340+
// image (binary data) → BlobPart { modality:"image", content (base64) }
341+
// file (inline data) → BlobPart { modality inferred from mediaType, content }
342+
// file (URL) → UriPart { modality inferred from mediaType, uri }
343+
// <unknown> → GenericPart
344+
345+
/**
346+
* Content part type values as emitted by the Vercel AI SDK v6 in span attributes.
347+
* Source: @ai-sdk/provider-utils ToolCallPart / ToolResultPart / ImagePart / FilePart / ReasoningPart
348+
*/
349+
const enum AiSdkPartType {
350+
Text = "text",
351+
Reasoning = "reasoning",
352+
ToolCall = "tool-call",
353+
ToolResult = "tool-result",
354+
Image = "image",
355+
File = "file",
356+
}
357+
358+
/**
359+
* Maps a single Vercel AI SDK content part to its OTel-compliant part object.
360+
*
361+
* Field names follow AI SDK v6:
362+
* ToolCallPart: toolCallId, toolName, input
363+
* ToolResultPart: toolCallId, toolName, output (ToolResultOutput union)
364+
* ImagePart: image (DataContent | URL), optional mediaType
365+
* FilePart: data (DataContent | URL), mediaType (required)
366+
* ReasoningPart: text
367+
*/
368+
export const mapAiSdkContentPart = (part: any): any => {
369+
if (!part || typeof part !== "object") {
370+
return { type: AiSdkPartType.Text, content: String(part ?? "") };
371+
}
372+
373+
switch (part.type) {
374+
case AiSdkPartType.Text:
375+
return { type: AiSdkPartType.Text, content: part.text ?? "" };
376+
377+
// OTel type is "reasoning", AI SDK v6 field is `text`
378+
case AiSdkPartType.Reasoning:
379+
return { type: AiSdkPartType.Reasoning, content: part.text ?? "" };
380+
381+
case AiSdkPartType.ToolCall:
382+
// AI SDK v6: toolCallId, toolName, input
383+
return {
384+
type: "tool_call",
385+
id: part.toolCallId ?? null,
386+
name: part.toolName,
387+
arguments: part.input,
388+
};
389+
390+
case AiSdkPartType.ToolResult: {
391+
// AI SDK v6: output is ToolResultOutput — { type: 'text'|'json', value } union
392+
// Unwrap to the actual value for tracing; fall back to the full object if unknown shape
393+
const output = part.output;
394+
const response =
395+
output && typeof output === "object" && "value" in output
396+
? output.value
397+
: output;
398+
return {
399+
type: "tool_call_response",
400+
id: part.toolCallId ?? null,
401+
response,
402+
};
403+
}
404+
405+
case AiSdkPartType.Image: {
406+
// AI SDK v6: image is DataContent | URL; optional mediaType
407+
const imgSrc = part.image ?? null;
408+
const mimeType = part.mediaType ?? null;
409+
410+
if (imgSrc instanceof URL) {
411+
return {
412+
type: "uri",
413+
modality: "image",
414+
uri: imgSrc.href,
415+
mime_type: mimeType,
416+
};
417+
}
418+
if (typeof imgSrc === "string") {
419+
if (imgSrc.startsWith("data:")) {
420+
const [header, data] = imgSrc.split(",");
421+
const detectedMime = header.match(/data:([^;]+)/)?.[1] ?? mimeType;
422+
return {
423+
type: "blob",
424+
modality: "image",
425+
...(detectedMime ? { mime_type: detectedMime } : {}),
426+
content: data || "",
427+
};
428+
}
429+
return {
430+
type: "uri",
431+
modality: "image",
432+
uri: imgSrc,
433+
mime_type: mimeType,
434+
};
435+
}
436+
if (imgSrc != null) {
437+
// Binary data (Uint8Array / ArrayBuffer / Buffer) — base64 encode
438+
const bytes =
439+
imgSrc instanceof ArrayBuffer ? new Uint8Array(imgSrc) : imgSrc;
440+
const b64 = Buffer.from(bytes).toString("base64");
441+
return {
442+
type: "blob",
443+
modality: "image",
444+
mime_type: mimeType,
445+
content: b64,
446+
};
447+
}
448+
return { type: "blob", modality: "image", content: "" };
449+
}
450+
451+
case AiSdkPartType.File: {
452+
// AI SDK v6: data is DataContent | URL; mediaType is required
453+
const fileSrc = part.data ?? null;
454+
const mimeType = part.mediaType ?? null;
455+
// Infer modality from MIME type (best-effort)
456+
const modality = mimeType?.startsWith("image/")
457+
? "image"
458+
: mimeType?.startsWith("audio/")
459+
? "audio"
460+
: mimeType?.startsWith("video/")
461+
? "video"
462+
: "document";
463+
464+
if (fileSrc instanceof URL) {
465+
return {
466+
type: "uri",
467+
modality,
468+
uri: fileSrc.href,
469+
mime_type: mimeType,
470+
};
471+
}
472+
if (typeof fileSrc === "string") {
473+
if (fileSrc.startsWith("data:")) {
474+
const [, data] = fileSrc.split(",");
475+
return {
476+
type: "blob",
477+
modality,
478+
mime_type: mimeType,
479+
content: data || "",
480+
};
481+
}
482+
return { type: "uri", modality, uri: fileSrc, mime_type: mimeType };
483+
}
484+
if (fileSrc != null) {
485+
const bytes =
486+
fileSrc instanceof ArrayBuffer ? new Uint8Array(fileSrc) : fileSrc;
487+
const b64 = Buffer.from(bytes).toString("base64");
488+
return { type: "blob", modality, mime_type: mimeType, content: b64 };
489+
}
490+
return { type: "blob", modality, content: "" };
491+
}
492+
493+
default:
494+
// GenericPart — preserve unknown types as-is
495+
return { type: part.type, ...part };
496+
}
497+
};
498+
499+
/**
500+
* Converts Vercel AI SDK message content to an array of OTel parts.
501+
* Accepts: plain string, array of SDK content parts, or a JSON-serialized array
502+
* (the AI SDK serializes content arrays as JSON strings in span attributes).
503+
*/
504+
export const mapAiSdkMessageContent = (content: any): any[] => {
505+
if (typeof content === "string") {
506+
try {
507+
const parsed = JSON.parse(content);
508+
if (Array.isArray(parsed)) {
509+
return parsed.map(mapAiSdkContentPart);
510+
}
511+
} catch {
512+
// plain string
513+
}
514+
return [{ type: "text", content }];
515+
}
516+
517+
if (Array.isArray(content)) {
518+
return content.map(mapAiSdkContentPart);
519+
}
520+
521+
if (content && typeof content === "object") {
522+
return [mapAiSdkContentPart(content)];
523+
}
524+
525+
return [{ type: "text", content: String(content ?? "") }];
526+
};

packages/instrumentation-utils/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,6 @@ export {
99
mapAnthropicContentBlock,
1010
mapOpenAIContentBlock,
1111
mapBedrockContentBlock,
12+
mapAiSdkContentPart,
13+
mapAiSdkMessageContent,
1214
} from "./content-block-mappers";

packages/sample-app/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@
5959
"node": ">=14"
6060
},
6161
"dependencies": {
62-
"@ai-sdk/openai": "^2.0.19",
62+
"@ai-sdk/anthropic": "^3.0.68",
63+
"@ai-sdk/openai": "^3.0.52",
6364
"@anthropic-ai/sdk": "^0.80.0",
6465
"@aws-sdk/client-bedrock-runtime": "^3.969.0",
6566
"@azure/identity": "^4.4.1",
@@ -80,7 +81,7 @@
8081
"@traceloop/instrumentation-langchain": "workspace:*",
8182
"@traceloop/node-server-sdk": "workspace:*",
8283
"@types/jimp": "^0.2.28",
83-
"ai": "^5.0.52",
84+
"ai": "6.0.132",
8485
"cheerio": "^1.1.2",
8586
"chromadb": "^3.0.9",
8687
"cohere-ai": "^7.17.1",

packages/sample-app/src/conversations/sample_chatbot_interactive.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as traceloop from "@traceloop/node-server-sdk";
22
import { openai } from "@ai-sdk/openai";
3-
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
3+
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
44
import * as readline from "readline";
55
import { z } from "zod";
66

@@ -23,7 +23,7 @@ const colors = {
2323
};
2424

2525
class InteractiveChatbot {
26-
private conversationHistory: CoreMessage[] = [];
26+
private conversationHistory: ModelMessage[] = [];
2727
private rl: readline.Interface;
2828
private conversationId: string;
2929
private userId: string;

packages/sample-app/src/conversations/sample_conversation_id_config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as traceloop from "@traceloop/node-server-sdk";
22
import { openai } from "@ai-sdk/openai";
3-
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
3+
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
44
import * as readline from "readline";
55
import { z } from "zod";
66

@@ -26,7 +26,7 @@ const colors = {
2626
};
2727

2828
class InteractiveChatbot {
29-
private conversationHistory: CoreMessage[] = [];
29+
private conversationHistory: ModelMessage[] = [];
3030
private rl: readline.Interface;
3131
private conversationId: string;
3232
private userId: string;

packages/sample-app/src/conversations/sample_with_conversation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as traceloop from "@traceloop/node-server-sdk";
22
import { openai } from "@ai-sdk/openai";
3-
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
3+
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
44
import * as readline from "readline";
55
import { z } from "zod";
66

@@ -25,7 +25,7 @@ const colors = {
2525
};
2626

2727
class InteractiveChatbot {
28-
private conversationHistory: CoreMessage[] = [];
28+
private conversationHistory: ModelMessage[] = [];
2929
private rl: readline.Interface;
3030
private conversationId: string;
3131
private userId: string;

0 commit comments

Comments
 (0)