UNPKG

@posthog/ai

Version:
1,128 lines (1,107 loc) 36 kB
'use strict'; var uuid = require('uuid'); var core = require('@posthog/core'); var version = "7.9.2"; // Type guards for safer type checking const isString = value => { return typeof value === 'string'; }; const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]'; // ============================================ // Multimodal Feature Toggle // ============================================ const isMultimodalEnabled = () => { const val = process.env._INTERNAL_LLMA_MULTIMODAL || ''; return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes'; }; // ============================================ // Base64 Detection Helpers // ============================================ const isBase64DataUrl = str => { return /^data:([^;]+);base64,/.test(str); }; const isValidUrl = str => { try { new URL(str); return true; } catch { // Not an absolute URL, check if it's a relative URL or path return str.startsWith('/') || str.startsWith('./') || str.startsWith('../'); } }; const isRawBase64 = str => { // Skip if it's a valid URL or path if (isValidUrl(str)) { return false; } // Check if it's a valid base64 string // Base64 images are typically at least a few hundred chars, but we'll be conservative return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str); }; function redactBase64DataUrl(str) { if (isMultimodalEnabled()) return str; if (!isString(str)) return str; // Check for data URL format if (isBase64DataUrl(str)) { return REDACTED_IMAGE_PLACEHOLDER; } // Check for raw base64 (Vercel sends raw base64 for inline images) if (isRawBase64(str)) { return REDACTED_IMAGE_PLACEHOLDER; } return str; } // limit large outputs by truncating to 200kb (approx 200k bytes) const MAX_OUTPUT_SIZE = 200000; const STRING_FORMAT = 'utf8'; const getModelParams = params => { if (!params) { return {}; } const modelParams = {}; const paramKeys = ['temperature', 'max_tokens', 'max_completion_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'n', 'stop', 'stream', 'streaming', 'language', 'response_format', 'timestamp_granularities']; for (const key of paramKeys) { if (key in params && params[key] !== undefined) { modelParams[key] = params[key]; } } return modelParams; }; const withPrivacyMode = (client, privacyMode, input) => { return client.privacy_mode || privacyMode ? null : input; }; function toSafeString(input) { if (input === undefined || input === null) { return ''; } if (typeof input === 'string') { return input; } try { return JSON.stringify(input); } catch { console.warn('Failed to stringify input', input); return ''; } } const truncate = input => { const str = toSafeString(input); if (str === '') { return ''; } // Check if we need to truncate and ensure STRING_FORMAT is respected const encoder = new TextEncoder(); const buffer = encoder.encode(str); if (buffer.length <= MAX_OUTPUT_SIZE) { // Ensure STRING_FORMAT is respected return new TextDecoder(STRING_FORMAT).decode(buffer); } // Truncate the buffer and ensure a valid string is returned const truncatedBuffer = buffer.slice(0, MAX_OUTPUT_SIZE); // fatal: false means we get U+FFFD at the end if truncation broke the encoding const decoder = new TextDecoder(STRING_FORMAT, { fatal: false }); let truncatedStr = decoder.decode(truncatedBuffer); if (truncatedStr.endsWith('\uFFFD')) { truncatedStr = truncatedStr.slice(0, -1); } return `${truncatedStr}... [truncated]`; }; let AIEvent = /*#__PURE__*/function (AIEvent) { AIEvent["Generation"] = "$ai_generation"; AIEvent["Embedding"] = "$ai_embedding"; return AIEvent; }({}); function sanitizeValues(obj) { if (obj === undefined || obj === null) { return obj; } const jsonSafe = JSON.parse(JSON.stringify(obj)); if (typeof jsonSafe === 'string') { // Sanitize lone surrogates by round-tripping through UTF-8 return new TextDecoder().decode(new TextEncoder().encode(jsonSafe)); } else if (Array.isArray(jsonSafe)) { return jsonSafe.map(sanitizeValues); } else if (jsonSafe && typeof jsonSafe === 'object') { return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)])); } return jsonSafe; } const sendEventWithErrorToPosthog = async ({ client, traceId, error, ...args }) => { const httpStatus = error && typeof error === 'object' && 'status' in error ? error.status ?? 500 : 500; const properties = { client, traceId, httpStatus, error: JSON.stringify(error), ...args }; const enrichedError = error; if (client.options?.enableExceptionAutocapture) { // assign a uuid that can be used to link the trace and exception events const exceptionId = core.uuidv7(); client.captureException(error, undefined, { $ai_trace_id: traceId }, exceptionId); enrichedError.__posthog_previously_captured_error = true; properties.exceptionId = exceptionId; } await sendEventToPosthog(properties); return enrichedError; }; const sendEventToPosthog = async ({ client, eventType = AIEvent.Generation, distinctId, traceId, model, provider, input, output, latency, timeToFirstToken, baseURL, params, httpStatus = 200, usage = {}, error, exceptionId, tools, captureImmediate = false }) => { if (!client.capture) { return Promise.resolve(); } // sanitize input and output for UTF-8 validity const safeInput = sanitizeValues(input); const safeOutput = sanitizeValues(output); const safeError = sanitizeValues(error); let errorData = {}; if (error) { errorData = { $ai_is_error: true, $ai_error: safeError, $exception_event_id: exceptionId }; } let costOverrideData = {}; if (params.posthogCostOverride) { const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0); const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0); costOverrideData = { $ai_input_cost_usd: inputCostUSD, $ai_output_cost_usd: outputCostUSD, $ai_total_cost_usd: inputCostUSD + outputCostUSD }; } const additionalTokenValues = { ...(usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {}), ...(usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {}), ...(usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {}), ...(usage.webSearchCount ? { $ai_web_search_count: usage.webSearchCount } : {}), ...(usage.rawUsage ? { $ai_usage: usage.rawUsage } : {}) }; const properties = { $ai_lib: 'posthog-ai', $ai_lib_version: version, $ai_provider: params.posthogProviderOverride ?? provider, $ai_model: params.posthogModelOverride ?? model, $ai_model_parameters: getModelParams(params), $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput), $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput), $ai_http_status: httpStatus, $ai_input_tokens: usage.inputTokens ?? 0, ...(usage.outputTokens !== undefined ? { $ai_output_tokens: usage.outputTokens } : {}), ...additionalTokenValues, $ai_latency: latency, ...(timeToFirstToken !== undefined ? { $ai_time_to_first_token: timeToFirstToken } : {}), $ai_trace_id: traceId, $ai_base_url: baseURL, ...params.posthogProperties, ...(distinctId ? {} : { $process_person_profile: false }), ...(tools ? { $ai_tools: tools } : {}), ...errorData, ...costOverrideData }; const event = { distinctId: distinctId ?? traceId, event: eventType, properties, groups: params.posthogGroups }; if (captureImmediate) { // await capture promise to send single event in serverless environments await client.captureImmediate(event); } else { client.capture(event); } return Promise.resolve(); }; const OTEL_STATUS_ERROR = 2; const AI_TELEMETRY_METADATA_PREFIX = 'ai.telemetry.metadata.'; function parseJsonValue(value) { if (value === undefined || value === null) { return null; } if (typeof value !== 'string') { return value; } try { return JSON.parse(value); } catch { return null; } } function toNumber(value) { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string') { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return undefined; } function toStringValue(value) { return typeof value === 'string' ? value : undefined; } function toStringArray(value) { if (!Array.isArray(value)) { return []; } return value.filter(item => typeof item === 'string'); } function toSafeBinaryData(value) { const asString = typeof value === 'string' ? value : JSON.stringify(value ?? ''); return truncate(redactBase64DataUrl(asString)); } function toMimeType(value) { return typeof value === 'string' && value.length > 0 ? value : 'application/octet-stream'; } function getSpanLatencySeconds(span) { const duration = span.duration; if (!duration || !Array.isArray(duration) || duration.length !== 2) { return 0; } const seconds = Number(duration[0]) || 0; const nanos = Number(duration[1]) || 0; return seconds + nanos / 1_000_000_000; } function getOperationId(span) { const attributes = span.attributes || {}; const operationId = toStringValue(attributes['ai.operationId']); if (operationId) { return operationId; } return span.name || ''; } function isDoGenerateSpan(operationId) { return operationId.endsWith('.doGenerate'); } function isDoStreamSpan(operationId) { return operationId.endsWith('.doStream'); } function isDoEmbedSpan(operationId) { return operationId.endsWith('.doEmbed'); } function shouldMapAiSdkSpan(span) { const operationId = getOperationId(span); return isDoGenerateSpan(operationId) || isDoStreamSpan(operationId) || isDoEmbedSpan(operationId); } function extractAiSdkTelemetryMetadata(attributes) { const metadata = {}; for (const [key, value] of Object.entries(attributes)) { if (key.startsWith(AI_TELEMETRY_METADATA_PREFIX)) { metadata[key.slice(AI_TELEMETRY_METADATA_PREFIX.length)] = value; } } if (metadata.traceId && typeof metadata.traceId === 'string') { metadata.trace_id = metadata.traceId; } return metadata; } function mapPromptMessagesInput(attributes) { const promptMessages = parseJsonValue(attributes['ai.prompt.messages']) || []; if (!Array.isArray(promptMessages)) { return []; } return promptMessages.map(message => { const role = typeof message?.role === 'string' ? message.role : 'user'; const content = message?.content; if (typeof content === 'string') { return { role, content: [{ type: 'text', text: truncate(content) }] }; } if (Array.isArray(content)) { return { role, content: content.map(part => { if (part && typeof part === 'object' && 'type' in part) { const typedPart = part; if (typedPart.type === 'text' && typeof typedPart.text === 'string') { return { type: 'text', text: truncate(typedPart.text) }; } return typedPart; } return { type: 'text', text: truncate(String(part)) }; }) }; } return { role, content: [{ type: 'text', text: truncate(content) }] }; }); } function mapPromptInput(attributes, operationId) { if (isDoEmbedSpan(operationId)) { if (attributes['ai.values'] !== undefined) { return attributes['ai.values']; } return attributes['ai.value'] ?? null; } const promptMessages = mapPromptMessagesInput(attributes); if (promptMessages.length > 0) { return promptMessages; } if (attributes['ai.prompt'] !== undefined) { return [{ role: 'user', content: [{ type: 'text', text: truncate(attributes['ai.prompt']) }] }]; } return []; } function mapOutputPart(part) { const partType = toStringValue(part.type); if (partType === 'text' && typeof part.text === 'string') { return { type: 'text', text: truncate(part.text) }; } if (partType === 'tool-call') { const toolName = toStringValue(part.toolName) || toStringValue(part.function?.name) || ''; const toolCallId = toStringValue(part.toolCallId) || toStringValue(part.id) || ''; const input = 'input' in part ? part.input : part.function?.arguments; if (toolName) { return { type: 'tool-call', id: toolCallId, function: { name: toolName, arguments: typeof input === 'string' ? input : JSON.stringify(input ?? {}) } }; } } if (partType === 'file') { const mediaType = toMimeType(part.mediaType ?? part.mimeType ?? part.contentType); const data = part.data ?? part.base64 ?? part.bytes ?? part.url ?? part.uri; if (data !== undefined) { return { type: 'file', name: 'generated_file', mediaType, data: toSafeBinaryData(data) }; } } if (partType === 'image') { const mediaType = toMimeType(part.mediaType ?? part.mimeType ?? part.contentType ?? 'image/unknown'); const data = part.data ?? part.base64 ?? part.bytes ?? part.url ?? part.uri ?? part.image ?? part.image_url; if (data !== undefined) { return { type: 'file', name: 'generated_file', mediaType, data: toSafeBinaryData(data) }; } } const inlineData = part.inlineData ?? part.inline_data; if (inlineData && typeof inlineData === 'object' && inlineData.data !== undefined) { const mediaType = toMimeType(inlineData.mimeType ?? inlineData.mime_type); return { type: 'file', name: 'generated_file', mediaType, data: toSafeBinaryData(inlineData.data) }; } if (partType === 'object' && part.object !== undefined) { return { type: 'object', object: part.object }; } return null; } function mapResponseMessagesOutput(attributes) { const messagesRaw = parseJsonValue(attributes['ai.response.messages']) ?? parseJsonValue(attributes['ai.response.message']); if (!messagesRaw) { return []; } const messages = Array.isArray(messagesRaw) ? messagesRaw : [messagesRaw]; const mappedMessages = []; for (const message of messages) { if (!message || typeof message !== 'object') { continue; } const role = toStringValue(message.role) || 'assistant'; const content = message.content; if (typeof content === 'string') { mappedMessages.push({ role, content: [{ type: 'text', text: truncate(content) }] }); continue; } if (Array.isArray(content)) { const parts = content.map(part => part && typeof part === 'object' ? mapOutputPart(part) : null).filter(part => part !== null); if (parts.length > 0) { mappedMessages.push({ role, content: parts }); } continue; } } return mappedMessages; } function mapTextToolObjectOutputParts(attributes) { const responseText = toStringValue(attributes['ai.response.text']) || ''; const toolCalls = parseJsonValue(attributes['ai.response.toolCalls']) || []; const responseObjectRaw = attributes['ai.response.object']; const responseObject = parseJsonValue(responseObjectRaw); const contentParts = []; if (responseText) { contentParts.push({ type: 'text', text: truncate(responseText) }); } if (responseObjectRaw !== undefined) { contentParts.push({ type: 'object', object: responseObject ?? responseObjectRaw }); } if (Array.isArray(toolCalls)) { for (const toolCall of toolCalls) { if (!toolCall || typeof toolCall !== 'object') { continue; } const toolName = typeof toolCall.toolName === 'string' ? toolCall.toolName : ''; const toolCallId = typeof toolCall.toolCallId === 'string' ? toolCall.toolCallId : ''; if (!toolName) { continue; } const input = 'input' in toolCall ? toolCall.input : {}; contentParts.push({ type: 'tool-call', id: toolCallId, function: { name: toolName, arguments: typeof input === 'string' ? input : JSON.stringify(input) } }); } } return contentParts; } function mapResponseFilesOutput(attributes) { const responseFiles = parseJsonValue(attributes['ai.response.files']) || []; if (!Array.isArray(responseFiles)) { return []; } const mapped = []; for (const file of responseFiles) { if (!file || typeof file !== 'object') { continue; } const mimeType = toMimeType(file.mimeType ?? file.mediaType ?? file.contentType); const data = file.data ?? file.base64 ?? file.bytes; const url = typeof file.url === 'string' ? file.url : typeof file.uri === 'string' ? file.uri : undefined; if (data !== undefined) { mapped.push({ type: 'file', name: 'generated_file', mediaType: mimeType, data: toSafeBinaryData(data) }); continue; } if (url) { mapped.push({ type: 'file', name: 'generated_file', mediaType: mimeType, data: truncate(url) }); } } return mapped; } function extractGeminiParts(providerMetadata) { const parts = []; const visit = node => { if (!node || typeof node !== 'object') { return; } if (Array.isArray(node)) { for (const item of node) { visit(item); } return; } const objectNode = node; const maybeParts = objectNode.parts; if (Array.isArray(maybeParts)) { for (const part of maybeParts) { if (part && typeof part === 'object') { parts.push(part); } } } for (const value of Object.values(objectNode)) { visit(value); } }; visit(providerMetadata); return parts; } function mapProviderMetadataInlineDataOutput(providerMetadata) { const parts = extractGeminiParts(providerMetadata); const mapped = []; for (const part of parts) { const inlineData = part.inlineData ?? part.inline_data; if (!inlineData || typeof inlineData !== 'object') { continue; } const mimeType = toMimeType(inlineData.mimeType ?? inlineData.mime_type); if (inlineData.data === undefined) { continue; } mapped.push({ type: 'file', name: 'generated_file', mediaType: mimeType, data: toSafeBinaryData(inlineData.data) }); } return mapped; } function mapProviderMetadataTextOutput(providerMetadata) { const parts = extractGeminiParts(providerMetadata); const mapped = []; for (const part of parts) { if (typeof part.text === 'string' && part.text.length > 0) { mapped.push({ type: 'text', text: truncate(part.text) }); } } return mapped; } function extractMediaBlocksFromUnknownNode(node) { const mapped = []; const visit = value => { if (!value || typeof value !== 'object') { return; } if (Array.isArray(value)) { for (const item of value) { visit(item); } return; } const objectValue = value; const inlineData = objectValue.inlineData ?? objectValue.inline_data; if (inlineData && typeof inlineData === 'object' && inlineData.data !== undefined) { const mediaType = toMimeType(inlineData.mimeType ?? inlineData.mime_type); mapped.push({ type: 'file', name: 'generated_file', mediaType, data: toSafeBinaryData(inlineData.data) }); } if ((objectValue.type === 'file' || 'mediaType' in objectValue || 'mimeType' in objectValue) && objectValue.data) { const mediaType = toMimeType(objectValue.mediaType ?? objectValue.mimeType); mapped.push({ type: 'file', name: 'generated_file', mediaType, data: toSafeBinaryData(objectValue.data) }); } for (const child of Object.values(objectValue)) { visit(child); } }; visit(node); return mapped; } function mapUnknownResponseAttributeMediaOutput(attributes) { const mapped = []; for (const [key, value] of Object.entries(attributes)) { if (!key.startsWith('ai.response.')) { continue; } if (key === 'ai.response.text' || key === 'ai.response.toolCalls' || key === 'ai.response.object' || key === 'ai.response.files' || key === 'ai.response.message' || key === 'ai.response.messages' || key === 'ai.response.providerMetadata') { continue; } const parsed = typeof value === 'string' ? parseJsonValue(value) ?? value : value; mapped.push(...extractMediaBlocksFromUnknownNode(parsed)); } return mapped; } function mapGenericResponseAttributeMediaOutput(attributes) { const mapped = []; for (const [key, value] of Object.entries(attributes)) { if (!key.includes('response') || key.startsWith('ai.response.') || key === 'ai.response.providerMetadata' || key.startsWith('ai.prompt.') || key.startsWith('gen_ai.request.')) { continue; } const parsed = typeof value === 'string' ? parseJsonValue(value) : value; if (parsed === null || parsed === undefined) { continue; } mapped.push(...extractMediaBlocksFromUnknownNode(parsed)); } return mapped; } function dedupeContentParts(parts) { const seen = new Set(); const deduped = []; for (const part of parts) { const key = JSON.stringify(part); if (seen.has(key)) { continue; } seen.add(key); deduped.push(part); } return deduped; } function mapOutput(attributes, operationId, providerMetadata) { if (isDoEmbedSpan(operationId)) { // Keep embedding behavior aligned with existing provider wrappers. return null; } const responseMessages = mapResponseMessagesOutput(attributes); if (responseMessages.length > 0) { return responseMessages; } const textToolObjectParts = mapTextToolObjectOutputParts(attributes); const responseFileParts = mapResponseFilesOutput(attributes); const unknownMediaParts = mapUnknownResponseAttributeMediaOutput(attributes); const genericResponseMediaParts = mapGenericResponseAttributeMediaOutput(attributes); const providerMetadataTextParts = mapProviderMetadataTextOutput(providerMetadata); const providerMetadataInlineParts = mapProviderMetadataInlineDataOutput(providerMetadata); const mergedContentParts = dedupeContentParts([...textToolObjectParts, ...responseFileParts, ...unknownMediaParts, ...genericResponseMediaParts, ...providerMetadataTextParts, ...providerMetadataInlineParts]); const contentParts = mergedContentParts; if (contentParts.length === 0) { return []; } return [{ role: 'assistant', content: contentParts }]; } function mapModelSettings(attributes, operationId) { const temperature = toNumber(attributes['ai.settings.temperature']) ?? toNumber(attributes['gen_ai.request.temperature']); const maxTokens = toNumber(attributes['ai.settings.maxTokens']) ?? toNumber(attributes['gen_ai.request.max_tokens']); const maxOutputTokens = toNumber(attributes['ai.settings.maxOutputTokens']); const topP = toNumber(attributes['ai.settings.topP']) ?? toNumber(attributes['gen_ai.request.top_p']); const frequencyPenalty = toNumber(attributes['ai.settings.frequencyPenalty']) ?? toNumber(attributes['gen_ai.request.frequency_penalty']); const presencePenalty = toNumber(attributes['ai.settings.presencePenalty']) ?? toNumber(attributes['gen_ai.request.presence_penalty']); const stopSequences = parseJsonValue(attributes['ai.settings.stopSequences']) ?? parseJsonValue(attributes['gen_ai.request.stop_sequences']); const stream = isDoStreamSpan(operationId); return { ...(temperature !== undefined ? { temperature } : {}), ...(maxTokens !== undefined ? { max_tokens: maxTokens } : {}), ...(maxOutputTokens !== undefined ? { max_completion_tokens: maxOutputTokens } : {}), ...(topP !== undefined ? { top_p: topP } : {}), ...(frequencyPenalty !== undefined ? { frequency_penalty: frequencyPenalty } : {}), ...(presencePenalty !== undefined ? { presence_penalty: presencePenalty } : {}), ...(stopSequences !== null ? { stop: stopSequences } : {}), ...(stream ? { stream: true } : {}) }; } function mapUsage(attributes, providerMetadata, operationId) { if (isDoEmbedSpan(operationId)) { const tokens = toNumber(attributes['ai.usage.tokens']) ?? toNumber(attributes['gen_ai.usage.input_tokens']) ?? 0; return { inputTokens: tokens, rawUsage: { usage: { tokens }, providerMetadata } }; } const inputTokens = toNumber(attributes['ai.usage.promptTokens']) ?? toNumber(attributes['gen_ai.usage.input_tokens']) ?? 0; const outputTokens = toNumber(attributes['ai.usage.completionTokens']) ?? toNumber(attributes['gen_ai.usage.output_tokens']) ?? 0; const totalTokens = toNumber(attributes['ai.usage.totalTokens']); const reasoningTokens = toNumber(attributes['ai.usage.reasoningTokens']); const cachedInputTokens = toNumber(attributes['ai.usage.cachedInputTokens']); return { inputTokens, outputTokens, ...(reasoningTokens !== undefined ? { reasoningTokens } : {}), ...(cachedInputTokens !== undefined ? { cacheReadInputTokens: cachedInputTokens } : {}), rawUsage: { usage: { promptTokens: inputTokens, completionTokens: outputTokens, ...(totalTokens !== undefined ? { totalTokens } : {}) }, providerMetadata } }; } function parsePromptTools(attributes) { const rawTools = attributes['ai.prompt.tools']; if (!Array.isArray(rawTools)) { return null; } const parsedTools = []; for (const rawTool of rawTools) { if (typeof rawTool === 'string') { const parsed = parseJsonValue(rawTool); if (parsed !== null) { parsedTools.push(parsed); } continue; } if (rawTool && typeof rawTool === 'object') { parsedTools.push(rawTool); } } return parsedTools.length > 0 ? parsedTools : null; } function extractProviderMetadata(attributes) { const rawProviderMetadata = attributes['ai.response.providerMetadata']; return parseJsonValue(rawProviderMetadata) || {}; } function getAiSdkFrameworkVersion(span) { const instrumentedSpan = span; const attributes = span.attributes || {}; const instrumentationScopeVersion = toStringValue(instrumentedSpan.instrumentationScope?.version) || toStringValue(instrumentedSpan.instrumentationLibrary?.version); const aiUserAgent = toStringValue(attributes['ai.request.headers.user-agent']); const userAgentVersionMatch = aiUserAgent?.match(/\bai\/(\d+(?:\.\d+)*)\b/i); const userAgentVersion = userAgentVersionMatch?.[1]; const rawVersion = instrumentationScopeVersion || userAgentVersion; if (!rawVersion) { return undefined; } const majorVersionMatch = rawVersion.match(/^v?(\d+)/i); return majorVersionMatch ? majorVersionMatch[1] : rawVersion; } function buildPosthogProperties(attributes, operationId) { const telemetryMetadata = extractAiSdkTelemetryMetadata(attributes); const finishReasons = toStringArray(parseJsonValue(attributes['gen_ai.response.finish_reasons'])); const finishReason = toStringValue(attributes['ai.response.finishReason']) || finishReasons[0]; const toolChoice = parseJsonValue(attributes['ai.prompt.toolChoice']) ?? attributes['ai.prompt.toolChoice']; return { ...telemetryMetadata, $ai_framework: 'vercel', ai_operation_id: operationId, ...(finishReason ? { ai_finish_reason: finishReason } : {}), ...(toStringValue(attributes['ai.response.model']) ? { ai_response_model: attributes['ai.response.model'] } : {}), ...(toStringValue(attributes['gen_ai.response.model']) ? { ai_response_model: attributes['gen_ai.response.model'] } : {}), ...(toStringValue(attributes['ai.response.id']) ? { ai_response_id: attributes['ai.response.id'] } : {}), ...(toStringValue(attributes['gen_ai.response.id']) ? { ai_response_id: attributes['gen_ai.response.id'] } : {}), ...(toStringValue(attributes['ai.response.timestamp']) ? { ai_response_timestamp: attributes['ai.response.timestamp'] } : {}), ...(toNumber(attributes['ai.response.msToFinish']) !== undefined ? { ai_response_ms_to_finish: toNumber(attributes['ai.response.msToFinish']) } : {}), ...(toNumber(attributes['ai.response.avgCompletionTokensPerSecond']) !== undefined ? { ai_response_avg_completion_tokens_per_second: toNumber(attributes['ai.response.avgCompletionTokensPerSecond']) } : {}), ...(toStringValue(attributes['ai.telemetry.functionId']) ? { ai_telemetry_function_id: attributes['ai.telemetry.functionId'] } : {}), ...(toNumber(attributes['ai.settings.maxRetries']) !== undefined ? { ai_settings_max_retries: toNumber(attributes['ai.settings.maxRetries']) } : {}), ...(toNumber(attributes['gen_ai.request.top_k']) !== undefined ? { ai_request_top_k: toNumber(attributes['gen_ai.request.top_k']) } : {}), ...(attributes['ai.schema.name'] !== undefined ? { ai_schema_name: attributes['ai.schema.name'] } : {}), ...(attributes['ai.schema.description'] !== undefined ? { ai_schema_description: attributes['ai.schema.description'] } : {}), ...(attributes['ai.settings.output'] !== undefined ? { ai_settings_output: attributes['ai.settings.output'] } : {}), ...(toolChoice ? { ai_prompt_tool_choice: toolChoice } : {}) }; } function buildAiSdkMapperResult(span) { const attributes = span.attributes || {}; const operationId = getOperationId(span); const providerMetadata = extractProviderMetadata(attributes); const model = toStringValue(attributes['ai.model.id']) || toStringValue(attributes['gen_ai.request.model']) || 'unknown'; const provider = (toStringValue(attributes['ai.model.provider']) || toStringValue(attributes['gen_ai.system']) || 'unknown').toLowerCase(); const latency = getSpanLatencySeconds(span); const timeToFirstTokenMs = toNumber(attributes['ai.response.msToFirstChunk']); const timeToFirstToken = timeToFirstTokenMs !== undefined ? timeToFirstTokenMs / 1000 : undefined; const input = mapPromptInput(attributes, operationId); const output = mapOutput(attributes, operationId, providerMetadata); const usage = mapUsage(attributes, providerMetadata, operationId); const modelParams = mapModelSettings(attributes, operationId); const tools = parsePromptTools(attributes); const httpStatus = toNumber(attributes['http.response.status_code']) || 200; const eventType = isDoEmbedSpan(operationId) ? AIEvent.Embedding : AIEvent.Generation; const frameworkVersion = getAiSdkFrameworkVersion(span); const error = span.status?.code === OTEL_STATUS_ERROR ? span.status.message || 'AI SDK span recorded error status' : undefined; return { model, provider, input, output, latency, timeToFirstToken, httpStatus, eventType, usage, tools, modelParams, posthogProperties: { ...buildPosthogProperties(attributes, operationId), ...(frameworkVersion ? { $ai_framework_version: frameworkVersion } : {}) }, error }; } const aiSdkSpanMapper = { name: 'ai-sdk', canMap: shouldMapAiSdkSpan, map: span => { return buildAiSdkMapperResult(span); } }; const defaultSpanMappers = [aiSdkSpanMapper]; function pickMapper(span, mappers) { return mappers.find(mapper => { try { return mapper.canMap(span); } catch { return false; } }); } function getTraceId(span, options, mapperTraceId) { if (mapperTraceId) { return mapperTraceId; } if (options.posthogTraceId) { return options.posthogTraceId; } const spanTraceId = span.spanContext?.().traceId; return spanTraceId || uuid.v4(); } function buildPosthogParams(options, traceId, distinctId, modelParams, posthogProperties) { return { ...modelParams, posthogDistinctId: distinctId, posthogTraceId: traceId, posthogProperties, posthogPrivacyMode: options.posthogPrivacyMode, posthogGroups: options.posthogGroups, posthogModelOverride: options.posthogModelOverride, posthogProviderOverride: options.posthogProviderOverride, posthogCostOverride: options.posthogCostOverride, posthogCaptureImmediate: options.posthogCaptureImmediate }; } async function captureSpan(span, phClient, options = {}) { if (options.shouldExportSpan && options.shouldExportSpan({ otelSpan: span }) === false) { return; } const mappers = options.mappers ?? defaultSpanMappers; const mapper = pickMapper(span, mappers); if (!mapper) { return; } const mapped = mapper.map(span, { options }); if (!mapped) { return; } const traceId = getTraceId(span, options, mapped.traceId); const distinctId = mapped.distinctId ?? options.posthogDistinctId; const posthogProperties = { ...options.posthogProperties, ...mapped.posthogProperties }; const params = buildPosthogParams(options, traceId, distinctId, mapped.modelParams ?? {}, posthogProperties); const baseURL = mapped.baseURL ?? ''; const usage = mapped.usage ?? {}; if (mapped.error !== undefined) { await sendEventWithErrorToPosthog({ eventType: mapped.eventType, client: phClient, distinctId, traceId, model: mapped.model, provider: mapped.provider, input: mapped.input, output: mapped.output, latency: mapped.latency, baseURL, params: params, usage, tools: mapped.tools, error: mapped.error, captureImmediate: options.posthogCaptureImmediate }); return; } await sendEventToPosthog({ eventType: mapped.eventType, client: phClient, distinctId, traceId, model: mapped.model, provider: mapped.provider, input: mapped.input, output: mapped.output, latency: mapped.latency, timeToFirstToken: mapped.timeToFirstToken, baseURL, params: params, httpStatus: mapped.httpStatus ?? 200, usage, tools: mapped.tools, captureImmediate: options.posthogCaptureImmediate }); } class PostHogSpanProcessor { pendingCaptures = new Set(); constructor(phClient, options = {}) { this.phClient = phClient; this.options = options; } onStart(_span, _parentContext) { // no-op } onEnd(span) { const capturePromise = captureSpan(span, this.phClient, this.options).catch(error => { console.error('Failed to capture telemetry span', error); }).finally(() => { this.pendingCaptures.delete(capturePromise); }); this.pendingCaptures.add(capturePromise); } async shutdown() { await this.forceFlush(); } async forceFlush() { while (this.pendingCaptures.size > 0) { await Promise.allSettled([...this.pendingCaptures]); } } } function createPostHogSpanProcessor(phClient, options = {}) { return new PostHogSpanProcessor(phClient, options); } exports.PostHogSpanProcessor = PostHogSpanProcessor; exports.aiSdkSpanMapper = aiSdkSpanMapper; exports.captureSpan = captureSpan; exports.createPostHogSpanProcessor = createPostHogSpanProcessor; //# sourceMappingURL=index.cjs.map