langsmith
Version:
Client library to connect to the LangSmith Observability and Evaluation Platform.
283 lines (282 loc) • 10.1 kB
JavaScript
import { isRecord } from "../../utils/types.js";
import { extractInputTokenDetails, extractOutputTokenDetails, } from "../../utils/vercel.js";
const guessMimetypeFromBase64 = (data) => {
// Check magic bytes from base64 data
const bytes = atob(data.substring(0, 20)); // Decode first few bytes
// PNG: 89 50 4E 47
if (bytes.charCodeAt(0) === 0x89 &&
bytes.charCodeAt(1) === 0x50 &&
bytes.charCodeAt(2) === 0x4e &&
bytes.charCodeAt(3) === 0x47) {
return "image/png";
}
// JPEG: FF D8 FF
if (bytes.charCodeAt(0) === 0xff &&
bytes.charCodeAt(1) === 0xd8 &&
bytes.charCodeAt(2) === 0xff) {
return "image/jpeg";
}
// GIF: 47 49 46 38
if (bytes.charCodeAt(0) === 0x47 &&
bytes.charCodeAt(1) === 0x49 &&
bytes.charCodeAt(2) === 0x46 &&
bytes.charCodeAt(3) === 0x38) {
return "image/gif";
}
// WebP: 52 49 46 46 (RIFF) ... 57 45 42 50 (WEBP)
if (bytes.charCodeAt(0) === 0x52 &&
bytes.charCodeAt(1) === 0x49 &&
bytes.charCodeAt(2) === 0x46 &&
bytes.charCodeAt(3) === 0x46) {
if (bytes.indexOf("WEBP") !== -1) {
return "image/webp";
}
}
// PDF: 25 50 44 46 (%PDF)
if (bytes.charCodeAt(0) === 0x25 &&
bytes.charCodeAt(1) === 0x50 &&
bytes.charCodeAt(2) === 0x44 &&
bytes.charCodeAt(3) === 0x46) {
return "application/pdf";
}
return undefined;
};
function _isAISDKFileData(input) {
if (!isRecord(input))
return false;
if (input.type === "data" && "data" in input)
return true;
if (input.type === "url" && "url" in input)
return true;
if (input.type === "reference" && "reference" in input)
return true;
if (input.type === "text" && "text" in input)
return true;
return false;
}
function _toUint8Array(fileData) {
// Covers `fileData: ArrayBuffer | Buffer | Uint8Array`
if (fileData instanceof Uint8Array) {
return fileData;
}
if (fileData != null &&
typeof fileData === "object" &&
"type" in fileData &&
"data" in fileData &&
typeof fileData.data === "object" &&
// eslint-disable-next-line no-instanceof/no-instanceof
fileData.data instanceof Uint8Array) {
return fileData.data;
// eslint-disable-next-line no-instanceof/no-instanceof
}
if (fileData instanceof ArrayBuffer) {
return new Uint8Array(fileData);
}
return undefined;
}
export const normalizeFileDataAsDataURL = (fileData, mimeType) => {
if (_isAISDKFileData(fileData)) {
if (fileData.type === "data") {
return normalizeFileDataAsDataURL(fileData.data, mimeType);
}
if (fileData.type === "url") {
return fileData.url.toString();
}
if (fileData.type === "reference") {
// TODO: figure out if we can store the reference in a more reasonable format
return `data:application/octet-stream;base64,${btoa(JSON.stringify(fileData.reference))}`;
}
if (fileData.type === "text") {
return `data:text/plain;base64,${btoa(fileData.text)}`;
}
throw new Error("AISDKFileData is not supported");
}
if (fileData instanceof URL) {
return fileData.toString();
}
if (typeof fileData !== "string") {
const uint8Array = _toUint8Array(fileData);
if (uint8Array) {
let binary = "";
for (let i = 0; i < uint8Array.length; i++) {
binary += String.fromCharCode(uint8Array[i]);
}
const base64 = btoa(binary);
const dataType = mimeType ??
guessMimetypeFromBase64(base64) ??
"application/octet-stream";
return `data:${dataType};base64,${base64}`;
}
}
if (typeof fileData === "string") {
if (fileData.startsWith("http://") || fileData.startsWith("https://")) {
return fileData;
}
if (!fileData.startsWith("data:")) {
return `data:${mimeType ??
guessMimetypeFromBase64(fileData) ??
"application/octet-stream"};base64,${fileData}`;
}
return fileData;
}
return "";
};
export const convertMessageToTracedFormat = (rawMessage, responseMetadata) => {
const message = rawMessage;
const formattedMessage = {
...message,
};
if (Array.isArray(message.content)) {
if (message.role === "assistant") {
const toolCallBlocks = message.content.filter((block) => {
return (block != null &&
typeof block === "object" &&
block.type === "tool-call");
});
const toolCalls = toolCallBlocks.map((block) => {
// AI SDK 4 shim
let toolArgs = block.input ?? (("args" in block && block.args) || undefined);
if (typeof toolArgs !== "string") {
toolArgs = JSON.stringify(toolArgs);
}
return {
id: block.toolCallId,
type: "function",
function: {
name: block.toolName,
arguments: toolArgs,
},
};
});
if (toolCalls.length > 0) {
formattedMessage.tool_calls = toolCalls;
}
}
const newContent = message.content.map((part) => {
if (part.type === "file") {
const { data, mediaType, filename, ...rest } = part;
return {
...rest,
file: {
filename,
file_data: normalizeFileDataAsDataURL(data, mediaType),
},
};
}
else if (part.type === "image") {
const { image, mediaType, ...rest } = part;
return {
...rest,
type: "image_url",
image_url: normalizeFileDataAsDataURL(image, mediaType),
};
}
else if (part.type === "reasoning" &&
"text" in part &&
typeof part.text === "string") {
return {
type: "reasoning",
reasoning: part.text,
};
}
return part;
});
formattedMessage.content = newContent;
}
else if (message.content == null && "text" in message) {
// AI SDK 4 shim
formattedMessage.content = message.text ?? "";
if ("toolCalls" in message &&
Array.isArray(message.toolCalls) &&
!("tool_calls" in formattedMessage)) {
formattedMessage.tool_calls = message.toolCalls.map((toolCall) => {
return {
id: toolCall.toolCallId,
type: "function",
function: { name: toolCall.toolName, arguments: toolCall.args },
};
});
}
}
if (responseMetadata != null) {
formattedMessage.response_metadata = responseMetadata;
}
return formattedMessage;
};
export const setUsageMetadataOnRunTree = (result, runTree) => {
if (result.usage == null || typeof result.usage !== "object") {
return;
}
const usage = result.usage;
let inputTokens;
let outputTokens;
let totalTokens;
// AI SDK 6: Check for object-based token structures first
if (isRecord(usage.inputTokens) &&
usage.inputTokens?.total != null &&
typeof usage.inputTokens.total === "number") {
// AI SDK 6 detected
inputTokens = usage.inputTokens.total;
if (isRecord(usage.outputTokens) &&
usage.outputTokens?.total != null &&
typeof usage.outputTokens.total === "number") {
outputTokens = usage.outputTokens.total;
}
totalTokens = result.usage?.totalTokens;
if (typeof totalTokens !== "number" &&
typeof inputTokens === "number" &&
typeof outputTokens === "number") {
totalTokens = inputTokens + outputTokens;
}
}
else if (typeof usage.inputTokens === "number") {
// AI SDK 5 detected
inputTokens = usage.inputTokens;
if (typeof usage.outputTokens === "number") {
outputTokens = usage.outputTokens;
}
totalTokens = result.usage?.totalTokens;
if (typeof totalTokens !== "number" &&
typeof inputTokens === "number" &&
typeof outputTokens === "number") {
totalTokens = inputTokens + outputTokens;
}
}
else {
// AI SDK 4 fallback
if (typeof usage.promptTokens === "number") {
inputTokens = usage.promptTokens;
}
if (typeof usage.completionTokens === "number") {
outputTokens = usage.completionTokens;
}
totalTokens = result.usage?.totalTokens;
if (typeof totalTokens !== "number" &&
typeof inputTokens === "number" &&
typeof outputTokens === "number") {
totalTokens = inputTokens + outputTokens;
}
}
const langsmithUsage = {
input_tokens: inputTokens,
output_tokens: outputTokens,
total_tokens: totalTokens,
};
const inputTokenDetails = extractInputTokenDetails(result.usage, result.providerMetadata);
const outputTokenDetails = extractOutputTokenDetails(result.usage, result.providerMetadata);
runTree.extra = {
...runTree.extra,
metadata: {
...runTree.extra?.metadata,
usage_metadata: {
...langsmithUsage,
input_token_details: {
...inputTokenDetails,
},
output_token_details: {
...outputTokenDetails,
},
},
},
};
};