Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
203 changes: 203 additions & 0 deletions packages/instrumentation-utils/src/content-block-mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,206 @@ export const mapBedrockContentBlock = (block: any): object => {
return { type: block.type, ...block };
}
};

// =============================================================================
// Vercel AI SDK
// =============================================================================
// Maps a single Vercel AI SDK content part (from ai.prompt.messages span attr)
// to its OTel-compliant part object. Verified against:
// - AI SDK v6 ToolCallPart / ToolResultPart / ImagePart / FilePart / ReasoningPart types
// - OTel gen_ai input/output messages JSON schemas (v1.40)
//
// text → TextPart { type:"text", content }
// reasoning → ReasoningPart { type:"reasoning", content }
// tool-call → ToolCallRequestPart { type:"tool_call", id?, name, arguments? }
// tool-result→ ToolCallResponsePart { type:"tool_call_response", id?, response }
// image (string data URI) → BlobPart { modality:"image", mime_type?, content }
// image (string URL) → UriPart { modality:"image", uri }
// image (URL object) → UriPart { modality:"image", uri: url.href }
// image (binary data) → BlobPart { modality:"image", content (base64) }
// file (inline data) → BlobPart { modality inferred from mediaType, content }
// file (URL) → UriPart { modality inferred from mediaType, uri }
// <unknown> → GenericPart

/**
* Content part type values as emitted by the Vercel AI SDK v6 in span attributes.
* Source: @ai-sdk/provider-utils ToolCallPart / ToolResultPart / ImagePart / FilePart / ReasoningPart
*/
const enum AiSdkPartType {
Text = "text",
Reasoning = "reasoning",
ToolCall = "tool-call",
ToolResult = "tool-result",
Image = "image",
File = "file",
}

/**
* Maps a single Vercel AI SDK content part to its OTel-compliant part object.
*
* Field names follow AI SDK v6:
* ToolCallPart: toolCallId, toolName, input
* ToolResultPart: toolCallId, toolName, output (ToolResultOutput union)
* ImagePart: image (DataContent | URL), optional mediaType
* FilePart: data (DataContent | URL), mediaType (required)
* ReasoningPart: text
*/
export const mapAiSdkContentPart = (part: any): any => {
if (!part || typeof part !== "object") {
return { type: AiSdkPartType.Text, content: String(part ?? "") };
}

switch (part.type) {
case AiSdkPartType.Text:
return { type: AiSdkPartType.Text, content: part.text ?? "" };

// OTel type is "reasoning", AI SDK v6 field is `text`
case AiSdkPartType.Reasoning:
return { type: AiSdkPartType.Reasoning, content: part.text ?? "" };

case AiSdkPartType.ToolCall:
// AI SDK v6: toolCallId, toolName, input
return {
type: "tool_call",
id: part.toolCallId ?? null,
name: part.toolName,
arguments: part.input,
};

case AiSdkPartType.ToolResult: {
// AI SDK v6: output is ToolResultOutput — { type: 'text'|'json', value } union
// Unwrap to the actual value for tracing; fall back to the full object if unknown shape
const output = part.output;
const response =
output && typeof output === "object" && "value" in output
? output.value
: output;
return {
type: "tool_call_response",
id: part.toolCallId ?? null,
response,
};
}

case AiSdkPartType.Image: {
// AI SDK v6: image is DataContent | URL; optional mediaType
const imgSrc = part.image ?? null;
const mimeType = part.mediaType ?? null;

if (imgSrc instanceof URL) {
return {
type: "uri",
modality: "image",
uri: imgSrc.href,
mime_type: mimeType,
};
}
if (typeof imgSrc === "string") {
if (imgSrc.startsWith("data:")) {
const [header, data] = imgSrc.split(",");
const detectedMime = header.match(/data:([^;]+)/)?.[1] ?? mimeType;
return {
type: "blob",
modality: "image",
...(detectedMime ? { mime_type: detectedMime } : {}),
content: data || "",
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return {
type: "uri",
modality: "image",
uri: imgSrc,
mime_type: mimeType,
};
}
if (imgSrc != null) {
// Binary data (Uint8Array / ArrayBuffer / Buffer) — base64 encode
const bytes =
imgSrc instanceof ArrayBuffer ? new Uint8Array(imgSrc) : imgSrc;
const b64 = Buffer.from(bytes).toString("base64");
return {
type: "blob",
modality: "image",
mime_type: mimeType,
content: b64,
};
}
return { type: "blob", modality: "image", content: "" };
}

case AiSdkPartType.File: {
// AI SDK v6: data is DataContent | URL; mediaType is required
const fileSrc = part.data ?? null;
const mimeType = part.mediaType ?? null;
// Infer modality from MIME type (best-effort)
const modality = mimeType?.startsWith("image/")
? "image"
: mimeType?.startsWith("audio/")
? "audio"
: mimeType?.startsWith("video/")
? "video"
: "document";

if (fileSrc instanceof URL) {
return {
type: "uri",
modality,
uri: fileSrc.href,
mime_type: mimeType,
};
}
if (typeof fileSrc === "string") {
if (fileSrc.startsWith("data:")) {
const [, data] = fileSrc.split(",");
return {
type: "blob",
modality,
mime_type: mimeType,
content: data || "",
};
}
return { type: "uri", modality, uri: fileSrc, mime_type: mimeType };
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if (fileSrc != null) {
const bytes =
fileSrc instanceof ArrayBuffer ? new Uint8Array(fileSrc) : fileSrc;
const b64 = Buffer.from(bytes).toString("base64");
return { type: "blob", modality, mime_type: mimeType, content: b64 };
}
return { type: "blob", modality, content: "" };
}

default:
// GenericPart — preserve unknown types as-is
return { type: part.type, ...part };
}
};

/**
* Converts Vercel AI SDK message content to an array of OTel parts.
* Accepts: plain string, array of SDK content parts, or a JSON-serialized array
* (the AI SDK serializes content arrays as JSON strings in span attributes).
*/
export const mapAiSdkMessageContent = (content: any): any[] => {
if (typeof content === "string") {
try {
const parsed = JSON.parse(content);
if (Array.isArray(parsed)) {
return parsed.map(mapAiSdkContentPart);
}
} catch {
// plain string
}
return [{ type: "text", content }];
}

if (Array.isArray(content)) {
return content.map(mapAiSdkContentPart);
}

if (content && typeof content === "object") {
return [mapAiSdkContentPart(content)];
}

return [{ type: "text", content: String(content ?? "") }];
};
2 changes: 2 additions & 0 deletions packages/instrumentation-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ export {
mapAnthropicContentBlock,
mapOpenAIContentBlock,
mapBedrockContentBlock,
mapAiSdkContentPart,
mapAiSdkMessageContent,
} from "./content-block-mappers";
5 changes: 3 additions & 2 deletions packages/sample-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"node": ">=14"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.19",
"@ai-sdk/anthropic": "^3.0.68",
"@ai-sdk/openai": "^3.0.52",
"@anthropic-ai/sdk": "^0.80.0",
"@aws-sdk/client-bedrock-runtime": "^3.969.0",
"@azure/identity": "^4.4.1",
Expand All @@ -80,7 +81,7 @@
"@traceloop/instrumentation-langchain": "workspace:*",
"@traceloop/node-server-sdk": "workspace:*",
"@types/jimp": "^0.2.28",
"ai": "^5.0.52",
"ai": "6.0.132",
"cheerio": "^1.1.2",
"chromadb": "^3.0.9",
"cohere-ai": "^7.17.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as traceloop from "@traceloop/node-server-sdk";
import { openai } from "@ai-sdk/openai";
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
import * as readline from "readline";
import { z } from "zod";

Expand All @@ -23,7 +23,7 @@ const colors = {
};

class InteractiveChatbot {
private conversationHistory: CoreMessage[] = [];
private conversationHistory: ModelMessage[] = [];
private rl: readline.Interface;
private conversationId: string;
private userId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as traceloop from "@traceloop/node-server-sdk";
import { openai } from "@ai-sdk/openai";
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
import * as readline from "readline";
import { z } from "zod";

Expand All @@ -26,7 +26,7 @@ const colors = {
};

class InteractiveChatbot {
private conversationHistory: CoreMessage[] = [];
private conversationHistory: ModelMessage[] = [];
private rl: readline.Interface;
private conversationId: string;
private userId: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as traceloop from "@traceloop/node-server-sdk";
import { openai } from "@ai-sdk/openai";
import { streamText, CoreMessage, tool, stepCountIs } from "ai";
import { streamText, ModelMessage, tool, stepCountIs } from "ai";
import * as readline from "readline";
import { z } from "zod";

Expand All @@ -25,7 +25,7 @@ const colors = {
};

class InteractiveChatbot {
private conversationHistory: CoreMessage[] = [];
private conversationHistory: ModelMessage[] = [];
private rl: readline.Interface;
private conversationId: string;
private userId: string;
Expand Down
Loading
Loading