UNPKG

@posthog/ai

Version:
592 lines (566 loc) 19.6 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var AnthropicOriginal = require('@anthropic-ai/sdk'); var uuid = require('uuid'); var core = require('@posthog/core'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var AnthropicOriginal__default = /*#__PURE__*/_interopDefault(AnthropicOriginal); var version = "7.9.2"; // Type guards for safer type checking const isObject = value => { return value !== null && typeof value === 'object' && !Array.isArray(value); }; 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'; }; // ============================================ // Common Message Processing // ============================================ const processMessages = (messages, transformContent) => { if (!messages) return messages; const processContent = content => { if (typeof content === 'string') return content; if (!content) return content; if (Array.isArray(content)) { return content.map(transformContent); } // Handle single object content return transformContent(content); }; const processMessage = msg => { if (!isObject(msg) || !('content' in msg)) return msg; return { ...msg, content: processContent(msg.content) }; }; // Handle both arrays and single messages if (Array.isArray(messages)) { return messages.map(processMessage); } return processMessage(messages); }; const sanitizeAnthropicImage = item => { if (isMultimodalEnabled()) return item; if (!isObject(item)) return item; // Handle Anthropic's image and document formats (same structure, different type field) if ((item.type === 'image' || item.type === 'document') && 'source' in item && isObject(item.source) && item.source.type === 'base64' && 'data' in item.source) { return { ...item, source: { ...item.source, data: REDACTED_IMAGE_PLACEHOLDER } }; } return item; }; const sanitizeAnthropic = data => { return processMessages(data, sanitizeAnthropicImage); }; 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 formatResponseAnthropic = response => { const output = []; const content = []; for (const choice of response.content ?? []) { if (choice?.type === 'text' && choice?.text) { content.push({ type: 'text', text: choice.text }); } else if (choice?.type === 'tool_use' && choice?.name && choice?.id) { content.push({ type: 'function', id: choice.id, function: { name: choice.name, arguments: choice.input || {} } }); } } if (content.length > 0) { output.push({ role: 'assistant', content }); } return output; }; const mergeSystemPrompt = (params, provider) => { { const messages = params.messages || []; if (!params.system) { return messages; } const systemMessage = params.system; return [{ role: 'system', content: systemMessage }, ...messages]; } }; const withPrivacyMode = (client, privacyMode, input) => { return client.privacy_mode || privacyMode ? null : input; }; /** * Extract available tool calls from the request parameters. * These are the tools provided to the LLM, not the tool calls in the response. */ const extractAvailableToolCalls = (provider, params) => { { if (params.tools) { return params.tools; } return null; } }; 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 POSTHOG_PARAMS_MAP = { posthogDistinctId: 'distinctId', posthogTraceId: 'traceId', posthogProperties: 'properties', posthogPrivacyMode: 'privacyMode', posthogGroups: 'groups', posthogModelOverride: 'modelOverride', posthogProviderOverride: 'providerOverride', posthogCostOverride: 'costOverride', posthogCaptureImmediate: 'captureImmediate' }; function extractPosthogParams(body) { const providerParams = {}; const posthogParams = {}; for (const [key, value] of Object.entries(body)) { if (POSTHOG_PARAMS_MAP[key]) { posthogParams[POSTHOG_PARAMS_MAP[key]] = value; } else if (key.startsWith('posthog')) { console.warn(`Unknown Posthog parameter ${key}`); } else { providerParams[key] = value; } } return { providerParams: providerParams, posthogParams: addDefaults(posthogParams) }; } function addDefaults(params) { return { ...params, privacyMode: params.privacyMode ?? false, traceId: params.traceId ?? uuid.v4() }; } 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(); }; class PostHogAnthropic extends AnthropicOriginal__default.default { constructor(config) { const { posthog, ...anthropicConfig } = config; super(anthropicConfig); this.phClient = posthog; this.messages = new WrappedMessages(this, this.phClient); } } class WrappedMessages extends AnthropicOriginal__default.default.Messages { constructor(parentClient, phClient) { super(parentClient); this.phClient = phClient; this.baseURL = parentClient.baseURL; } create(body, options) { const { providerParams: anthropicParams, posthogParams } = extractPosthogParams(body); const startTime = Date.now(); const parentPromise = super.create(anthropicParams, options); if (anthropicParams.stream) { return parentPromise.then(value => { let accumulatedContent = ''; const contentBlocks = []; const toolsInProgress = new Map(); let currentTextBlock = null; let firstTokenTime; const usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, webSearchCount: 0 }; let lastRawUsage; if ('tee' in value) { const [stream1, stream2] = value.tee(); (async () => { try { for await (const chunk of stream1) { // Handle content block start events if (chunk.type === 'content_block_start') { if (chunk.content_block?.type === 'text') { currentTextBlock = { type: 'text', text: '' }; contentBlocks.push(currentTextBlock); } else if (chunk.content_block?.type === 'tool_use') { if (firstTokenTime === undefined) { firstTokenTime = Date.now(); } const toolBlock = { type: 'function', id: chunk.content_block.id, function: { name: chunk.content_block.name, arguments: {} } }; contentBlocks.push(toolBlock); toolsInProgress.set(chunk.content_block.id, { block: toolBlock, inputString: '' }); currentTextBlock = null; } } // Handle text delta events if ('delta' in chunk) { if ('text' in chunk.delta) { const delta = chunk.delta.text; if (firstTokenTime === undefined) { firstTokenTime = Date.now(); } accumulatedContent += delta; if (currentTextBlock) { currentTextBlock.text += delta; } } } // Handle tool input delta events if (chunk.type === 'content_block_delta' && chunk.delta?.type === 'input_json_delta') { const block = chunk.index !== undefined ? contentBlocks[chunk.index] : undefined; const toolId = block?.type === 'function' ? block.id : undefined; if (toolId && toolsInProgress.has(toolId)) { const tool = toolsInProgress.get(toolId); if (tool) { tool.inputString += chunk.delta.partial_json || ''; } } } // Handle content block stop events if (chunk.type === 'content_block_stop') { currentTextBlock = null; // Parse accumulated tool input if (chunk.index !== undefined) { const block = contentBlocks[chunk.index]; if (block?.type === 'function' && block.id && toolsInProgress.has(block.id)) { const tool = toolsInProgress.get(block.id); if (tool) { try { block.function.arguments = JSON.parse(tool.inputString); } catch (e) { // Keep empty object if parsing fails console.error('Error parsing tool input:', e); } } toolsInProgress.delete(block.id); } } } if (chunk.type == 'message_start') { lastRawUsage = chunk.message.usage; usage.inputTokens = chunk.message.usage.input_tokens ?? 0; usage.cacheCreationInputTokens = chunk.message.usage.cache_creation_input_tokens ?? 0; usage.cacheReadInputTokens = chunk.message.usage.cache_read_input_tokens ?? 0; usage.webSearchCount = chunk.message.usage.server_tool_use?.web_search_requests ?? 0; } if ('usage' in chunk) { lastRawUsage = chunk.usage; usage.outputTokens = chunk.usage.output_tokens ?? 0; // Update web search count if present in delta if (chunk.usage.server_tool_use?.web_search_requests !== undefined) { usage.webSearchCount = chunk.usage.server_tool_use.web_search_requests; } } } usage.rawUsage = lastRawUsage; const latency = (Date.now() - startTime) / 1000; const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined; const availableTools = extractAvailableToolCalls('anthropic', anthropicParams); // Format output to match non-streaming version const formattedOutput = contentBlocks.length > 0 ? [{ role: 'assistant', content: contentBlocks }] : [{ role: 'assistant', content: [{ type: 'text', text: accumulatedContent }] }]; await sendEventToPosthog({ client: this.phClient, ...posthogParams, model: anthropicParams.model, provider: 'anthropic', input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams, 'anthropic')), output: formattedOutput, latency, timeToFirstToken, baseURL: this.baseURL, params: body, httpStatus: 200, usage, tools: availableTools }); } catch (error) { const enrichedError = await sendEventWithErrorToPosthog({ client: this.phClient, ...posthogParams, model: anthropicParams.model, provider: 'anthropic', input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams)), output: [], latency: 0, baseURL: this.baseURL, params: body, usage: { inputTokens: 0, outputTokens: 0 }, error: error }); throw enrichedError; } })(); // Return the other stream to the user return stream2; } return value; }); } else { const wrappedPromise = parentPromise.then(async result => { if ('content' in result) { const latency = (Date.now() - startTime) / 1000; const availableTools = extractAvailableToolCalls('anthropic', anthropicParams); await sendEventToPosthog({ client: this.phClient, ...posthogParams, model: anthropicParams.model, provider: 'anthropic', input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams)), output: formatResponseAnthropic(result), latency, baseURL: this.baseURL, params: body, httpStatus: 200, usage: { inputTokens: result.usage.input_tokens ?? 0, outputTokens: result.usage.output_tokens ?? 0, cacheCreationInputTokens: result.usage.cache_creation_input_tokens ?? 0, cacheReadInputTokens: result.usage.cache_read_input_tokens ?? 0, webSearchCount: result.usage.server_tool_use?.web_search_requests ?? 0, rawUsage: result.usage }, tools: availableTools }); } return result; }, async error => { await sendEventToPosthog({ client: this.phClient, ...posthogParams, model: anthropicParams.model, provider: 'anthropic', input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams)), output: [], latency: 0, baseURL: this.baseURL, params: body, httpStatus: error?.status ? error.status : 500, usage: { inputTokens: 0, outputTokens: 0 }, error: JSON.stringify(error) }); throw error; }); return wrappedPromise; } } } exports.Anthropic = PostHogAnthropic; exports.PostHogAnthropic = PostHogAnthropic; exports.WrappedMessages = WrappedMessages; exports.default = PostHogAnthropic; //# sourceMappingURL=index.cjs.map