@posthog/ai
Version:
PostHog Node.js AI integrations
592 lines (566 loc) • 19.6 kB
JavaScript
'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