@posthog/ai
Version:
PostHog Node.js AI integrations
1,567 lines (1,558 loc) • 103 kB
JavaScript
'use strict';
var openai = require('openai');
var uuid = require('uuid');
var buffer = require('buffer');
var ai = require('ai');
var AnthropicOriginal = require('@anthropic-ai/sdk');
var genai = require('@google/genai');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var uuid__namespace = /*#__PURE__*/_interopNamespaceDefault(uuid);
// 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'];
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 formatResponseOpenAI = response => {
const output = [];
if (response.choices) {
for (const choice of response.choices) {
const content = [];
let role = 'assistant';
if (choice.message) {
if (choice.message.role) {
role = choice.message.role;
}
if (choice.message.content) {
content.push({
type: 'text',
text: choice.message.content
});
}
if (choice.message.tool_calls) {
for (const toolCall of choice.message.tool_calls) {
content.push({
type: 'function',
id: toolCall.id,
function: {
name: toolCall.function.name,
arguments: toolCall.function.arguments
}
});
}
}
}
if (content.length > 0) {
output.push({
role,
content
});
}
}
}
// Handle Responses API format
if (response.output) {
const content = [];
let role = 'assistant';
for (const item of response.output) {
if (item.type === 'message') {
role = item.role;
if (item.content && Array.isArray(item.content)) {
for (const contentItem of item.content) {
if (contentItem.type === 'output_text' && contentItem.text) {
content.push({
type: 'text',
text: contentItem.text
});
} else if (contentItem.text) {
content.push({
type: 'text',
text: contentItem.text
});
} else if (contentItem.type === 'input_image' && contentItem.image_url) {
content.push({
type: 'image',
image: contentItem.image_url
});
}
}
} else if (item.content) {
content.push({
type: 'text',
text: String(item.content)
});
}
} else if (item.type === 'function_call') {
content.push({
type: 'function',
id: item.call_id || item.id || '',
function: {
name: item.name,
arguments: item.arguments || {}
}
});
}
}
if (content.length > 0) {
output.push({
role,
content
});
}
}
return output;
};
const formatResponseGemini = response => {
const output = [];
if (response.candidates && Array.isArray(response.candidates)) {
for (const candidate of response.candidates) {
if (candidate.content && candidate.content.parts) {
const content = [];
for (const part of candidate.content.parts) {
if (part.text) {
content.push({
type: 'text',
text: part.text
});
} else if (part.functionCall) {
content.push({
type: 'function',
function: {
name: part.functionCall.name,
arguments: part.functionCall.args
}
});
}
}
if (content.length > 0) {
output.push({
role: 'assistant',
content
});
}
} else if (candidate.text) {
output.push({
role: 'assistant',
content: [{
type: 'text',
text: candidate.text
}]
});
}
}
} else if (response.text) {
output.push({
role: 'assistant',
content: [{
type: 'text',
text: response.text
}]
});
}
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;
};
const truncate = str => {
try {
const buffer$1 = buffer.Buffer.from(str, STRING_FORMAT);
if (buffer$1.length <= MAX_OUTPUT_SIZE) {
return str;
}
const truncatedBuffer = buffer$1.slice(0, MAX_OUTPUT_SIZE);
return `${truncatedBuffer.toString(STRING_FORMAT)}... [truncated]`;
} catch (error) {
console.error('Error truncating, likely not a string');
return str;
}
};
/**
* 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 (provider === 'anthropic') {
if (params.tools) {
return params.tools;
}
return null;
} else if (provider === 'gemini') {
if (params.config && params.config.tools) {
return params.config.tools;
}
return null;
} else if (provider === 'openai') {
if (params.tools) {
return params.tools;
}
return null;
} else if (provider === 'vercel') {
// Vercel AI SDK stores tools in params.mode.tools when mode type is 'regular'
if (params.mode?.type === 'regular' && params.mode.tools) {
return params.mode.tools;
}
return null;
}
return null;
};
function sanitizeValues(obj) {
if (obj === undefined || obj === null) {
return obj;
}
const jsonSafe = JSON.parse(JSON.stringify(obj));
if (typeof jsonSafe === 'string') {
return buffer.Buffer.from(jsonSafe, STRING_FORMAT).toString(STRING_FORMAT);
} 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 sendEventToPosthog = async ({
client,
distinctId,
traceId,
model,
provider,
input,
output,
latency,
baseURL,
params,
httpStatus = 200,
usage = {},
isError = false,
error,
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 (isError) {
errorData = {
$ai_is_error: true,
$ai_error: safeError
};
}
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
} : {})
};
const properties = {
$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,
$ai_output_tokens: usage.outputTokens ?? 0,
...additionalTokenValues,
$ai_latency: latency,
$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: '$ai_generation',
properties,
groups: params.posthogGroups
};
if (captureImmediate) {
// await capture promise to send single event in serverless environments
await client.captureImmediate(event);
} else {
client.capture(event);
}
};
// Type guards for safer type checking
const isString = value => {
return typeof value === 'string';
};
const isObject = value => {
return value !== null && typeof value === 'object' && !Array.isArray(value);
};
const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
// ============================================
// 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 (!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;
}
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);
};
// ============================================
// Provider-Specific Image Sanitizers
// ============================================
const sanitizeOpenAIImage = item => {
if (!isObject(item)) return item;
// Handle image_url format
if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
return {
...item,
image_url: {
...item.image_url,
url: redactBase64DataUrl(item.image_url.url)
}
};
}
return item;
};
const sanitizeOpenAIResponseImage = item => {
if (!isObject(item)) return item;
// Handle input_image format
if (item.type === 'input_image' && 'image_url' in item) {
return {
...item,
image_url: redactBase64DataUrl(item.image_url)
};
}
return item;
};
const sanitizeAnthropicImage = item => {
if (!isObject(item)) return item;
// Handle Anthropic's image format
if (item.type === 'image' && '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 sanitizeGeminiPart = part => {
if (!isObject(part)) return part;
// Handle Gemini's inline data format
if ('inlineData' in part && isObject(part.inlineData) && 'data' in part.inlineData) {
return {
...part,
inlineData: {
...part.inlineData,
data: REDACTED_IMAGE_PLACEHOLDER
}
};
}
return part;
};
const processGeminiItem = item => {
if (!isObject(item)) return item;
// If it has parts, process them
if ('parts' in item && item.parts) {
const parts = Array.isArray(item.parts) ? item.parts.map(sanitizeGeminiPart) : sanitizeGeminiPart(item.parts);
return {
...item,
parts
};
}
return item;
};
const sanitizeLangChainImage = item => {
if (!isObject(item)) return item;
// OpenAI style
if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
return {
...item,
image_url: {
...item.image_url,
url: redactBase64DataUrl(item.image_url.url)
}
};
}
// Direct image with data field
if (item.type === 'image' && 'data' in item) {
return {
...item,
data: redactBase64DataUrl(item.data)
};
}
// Anthropic style
if (item.type === 'image' && 'source' in item && isObject(item.source) && 'data' in item.source) {
return {
...item,
source: {
...item.source,
data: redactBase64DataUrl(item.source.data)
}
};
}
// Google style
if (item.type === 'media' && 'data' in item) {
return {
...item,
data: redactBase64DataUrl(item.data)
};
}
return item;
};
// Export individual sanitizers for tree-shaking
const sanitizeOpenAI = data => {
return processMessages(data, sanitizeOpenAIImage);
};
const sanitizeOpenAIResponse = data => {
return processMessages(data, sanitizeOpenAIResponseImage);
};
const sanitizeAnthropic = data => {
return processMessages(data, sanitizeAnthropicImage);
};
const sanitizeGemini = data => {
// Gemini has a different structure with 'parts' directly on items instead of 'content'
// So we need custom processing instead of using processMessages
if (!data) return data;
if (Array.isArray(data)) {
return data.map(processGeminiItem);
}
return processGeminiItem(data);
};
const sanitizeLangChain = data => {
return processMessages(data, sanitizeLangChainImage);
};
const Chat = openai.OpenAI.Chat;
const Completions = Chat.Completions;
const Responses = openai.OpenAI.Responses;
class PostHogOpenAI extends openai.OpenAI {
constructor(config) {
const {
posthog,
...openAIConfig
} = config;
super(openAIConfig);
this.phClient = posthog;
this.chat = new WrappedChat$1(this, this.phClient);
this.responses = new WrappedResponses$1(this, this.phClient);
}
}
let WrappedChat$1 = class WrappedChat extends Chat {
constructor(parentClient, phClient) {
super(parentClient);
this.completions = new WrappedCompletions$1(parentClient, phClient);
}
};
let WrappedCompletions$1 = class WrappedCompletions extends Completions {
constructor(client, phClient) {
super(client);
this.phClient = phClient;
}
// --- Implementation Signature
create(body, options) {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
posthogCaptureImmediate,
...openAIParams
} = body;
const traceId = posthogTraceId ?? uuid.v4();
const startTime = Date.now();
const parentPromise = super.create(openAIParams, options);
if (openAIParams.stream) {
return parentPromise.then(value => {
if ('tee' in value) {
const [stream1, stream2] = value.tee();
(async () => {
try {
let accumulatedContent = '';
let usage = {
inputTokens: 0,
outputTokens: 0
};
for await (const chunk of stream1) {
const delta = chunk?.choices?.[0]?.delta?.content ?? '';
accumulatedContent += delta;
if (chunk.usage) {
usage = {
inputTokens: chunk.usage.prompt_tokens ?? 0,
outputTokens: chunk.usage.completion_tokens ?? 0,
reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0
};
}
}
const latency = (Date.now() - startTime) / 1000;
const availableTools = extractAvailableToolCalls('openai', openAIParams);
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAI(openAIParams.messages),
output: [{
content: accumulatedContent,
role: 'assistant'
}],
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage,
tools: availableTools,
captureImmediate: posthogCaptureImmediate
});
} catch (error) {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAI(openAIParams.messages),
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
}
})();
// Return the other stream to the user
return stream2;
}
return value;
});
} else {
const wrappedPromise = parentPromise.then(async result => {
if ('choices' in result) {
const latency = (Date.now() - startTime) / 1000;
const availableTools = extractAvailableToolCalls('openai', openAIParams);
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAI(openAIParams.messages),
output: formatResponseOpenAI(result),
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage?.prompt_tokens ?? 0,
outputTokens: result.usage?.completion_tokens ?? 0,
reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
},
tools: availableTools,
captureImmediate: posthogCaptureImmediate
});
}
return result;
}, async error => {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAI(openAIParams.messages),
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
throw error;
});
return wrappedPromise;
}
}
};
let WrappedResponses$1 = class WrappedResponses extends Responses {
constructor(client, phClient) {
super(client);
this.phClient = phClient;
}
// --- Implementation Signature
create(body, options) {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
posthogCaptureImmediate,
...openAIParams
} = body;
const traceId = posthogTraceId ?? uuid.v4();
const startTime = Date.now();
const parentPromise = super.create(openAIParams, options);
if (openAIParams.stream) {
return parentPromise.then(value => {
if ('tee' in value && typeof value.tee === 'function') {
const [stream1, stream2] = value.tee();
(async () => {
try {
let finalContent = [];
let usage = {
inputTokens: 0,
outputTokens: 0
};
for await (const chunk of stream1) {
if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
finalContent = chunk.response.output;
}
if ('response' in chunk && chunk.response?.usage) {
usage = {
inputTokens: chunk.response.usage.input_tokens ?? 0,
outputTokens: chunk.response.usage.output_tokens ?? 0,
reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: chunk.response.usage.input_tokens_details?.cached_tokens ?? 0
};
}
}
const latency = (Date.now() - startTime) / 1000;
const availableTools = extractAvailableToolCalls('openai', openAIParams);
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAIResponse(openAIParams.input),
output: finalContent,
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage,
tools: availableTools,
captureImmediate: posthogCaptureImmediate
});
} catch (error) {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAIResponse(openAIParams.input),
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
}
})();
return stream2;
}
return value;
});
} else {
const wrappedPromise = parentPromise.then(async result => {
if ('output' in result) {
const latency = (Date.now() - startTime) / 1000;
const availableTools = extractAvailableToolCalls('openai', openAIParams);
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAIResponse(openAIParams.input),
output: formatResponseOpenAI({
output: result.output
}),
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage?.input_tokens ?? 0,
outputTokens: result.usage?.output_tokens ?? 0,
reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
},
tools: availableTools,
captureImmediate: posthogCaptureImmediate
});
}
return result;
}, async error => {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAIResponse(openAIParams.input),
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
throw error;
});
return wrappedPromise;
}
}
parse(body, options) {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
posthogCaptureImmediate,
...openAIParams
} = body;
const traceId = posthogTraceId ?? uuid.v4();
const startTime = Date.now();
// Create a temporary instance that bypasses our wrapped create method
const originalCreate = super.create.bind(this);
const originalSelf = this;
const tempCreate = originalSelf.create;
originalSelf.create = originalCreate;
try {
const parentPromise = super.parse(openAIParams, options);
const wrappedPromise = parentPromise.then(async result => {
const latency = (Date.now() - startTime) / 1000;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAIResponse(openAIParams.input),
output: result.output,
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage?.input_tokens ?? 0,
outputTokens: result.usage?.output_tokens ?? 0,
reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
},
captureImmediate: posthogCaptureImmediate
});
return result;
}, async error => {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'openai',
input: sanitizeOpenAIResponse(openAIParams.input),
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
throw error;
});
return wrappedPromise;
} finally {
// Restore our wrapped create method
originalSelf.create = tempCreate;
}
}
};
class PostHogAzureOpenAI extends openai.AzureOpenAI {
constructor(config) {
const {
posthog,
...openAIConfig
} = config;
super(openAIConfig);
this.phClient = posthog;
this.chat = new WrappedChat(this, this.phClient);
}
}
class WrappedChat extends openai.AzureOpenAI.Chat {
constructor(parentClient, phClient) {
super(parentClient);
this.completions = new WrappedCompletions(parentClient, phClient);
}
}
class WrappedCompletions extends openai.AzureOpenAI.Chat.Completions {
constructor(client, phClient) {
super(client);
this.phClient = phClient;
}
// --- Implementation Signature
create(body, options) {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
posthogCaptureImmediate,
...openAIParams
} = body;
const traceId = posthogTraceId ?? uuid.v4();
const startTime = Date.now();
const parentPromise = super.create(openAIParams, options);
if (openAIParams.stream) {
return parentPromise.then(value => {
if ('tee' in value) {
const [stream1, stream2] = value.tee();
(async () => {
try {
let accumulatedContent = '';
let usage = {
inputTokens: 0,
outputTokens: 0
};
for await (const chunk of stream1) {
const delta = chunk?.choices?.[0]?.delta?.content ?? '';
accumulatedContent += delta;
if (chunk.usage) {
usage = {
inputTokens: chunk.usage.prompt_tokens ?? 0,
outputTokens: chunk.usage.completion_tokens ?? 0,
reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: chunk.usage.prompt_tokens_details?.cached_tokens ?? 0
};
}
}
const latency = (Date.now() - startTime) / 1000;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'azure',
input: openAIParams.messages,
output: [{
content: accumulatedContent,
role: 'assistant'
}],
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage,
captureImmediate: posthogCaptureImmediate
});
} catch (error) {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'azure',
input: openAIParams.messages,
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
}
})();
// Return the other stream to the user
return stream2;
}
return value;
});
} else {
const wrappedPromise = parentPromise.then(async result => {
if ('choices' in result) {
const latency = (Date.now() - startTime) / 1000;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'azure',
input: openAIParams.messages,
output: formatResponseOpenAI(result),
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage?.prompt_tokens ?? 0,
outputTokens: result.usage?.completion_tokens ?? 0,
reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
},
captureImmediate: posthogCaptureImmediate
});
}
return result;
}, async error => {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
model: openAIParams.model,
provider: 'azure',
input: openAIParams.messages,
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
throw error;
});
return wrappedPromise;
}
}
}
class WrappedResponses extends openai.AzureOpenAI.Responses {
constructor(client, phClient) {
super(client);
this.phClient = phClient;
}
// --- Implementation Signature
create(body, options) {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
posthogCaptureImmediate,
...openAIParams
} = body;
const traceId = posthogTraceId ?? uuid.v4();
const startTime = Date.now();
const parentPromise = super.create(openAIParams, options);
if (openAIParams.stream) {
return parentPromise.then(value => {
if ('tee' in value && typeof value.tee === 'function') {
const [stream1, stream2] = value.tee();
(async () => {
try {
let finalContent = [];
let usage = {
inputTokens: 0,
outputTokens: 0
};
for await (const chunk of stream1) {
if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
finalContent = chunk.response.output;
}
if ('usage' in chunk && chunk.usage) {
usage = {
inputTokens: chunk.usage.input_tokens ?? 0,
outputTokens: chunk.usage.output_tokens ?? 0,
reasoningTokens: chunk.usage.output_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: chunk.usage.input_tokens_details?.cached_tokens ?? 0
};
}
}
const latency = (Date.now() - startTime) / 1000;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'azure',
input: openAIParams.input,
output: finalContent,
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage,
captureImmediate: posthogCaptureImmediate
});
} catch (error) {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'azure',
input: openAIParams.input,
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
}
})();
return stream2;
}
return value;
});
} else {
const wrappedPromise = parentPromise.then(async result => {
if ('output' in result) {
const latency = (Date.now() - startTime) / 1000;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'azure',
input: openAIParams.input,
output: result.output,
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage?.input_tokens ?? 0,
outputTokens: result.usage?.output_tokens ?? 0,
reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
},
captureImmediate: posthogCaptureImmediate
});
}
return result;
}, async error => {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'azure',
input: openAIParams.input,
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
throw error;
});
return wrappedPromise;
}
}
parse(body, options) {
const {
posthogDistinctId,
posthogTraceId,
posthogProperties,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
posthogPrivacyMode = false,
posthogGroups,
posthogCaptureImmediate,
...openAIParams
} = body;
const traceId = posthogTraceId ?? uuid.v4();
const startTime = Date.now();
const parentPromise = super.parse(openAIParams, options);
const wrappedPromise = parentPromise.then(async result => {
const latency = (Date.now() - startTime) / 1000;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'azure',
input: openAIParams.input,
output: result.output,
latency,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: 200,
usage: {
inputTokens: result.usage?.input_tokens ?? 0,
outputTokens: result.usage?.output_tokens ?? 0,
reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
},
captureImmediate: posthogCaptureImmediate
});
return result;
}, async error => {
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId,
traceId,
//@ts-expect-error
model: openAIParams.model,
provider: 'azure',
input: openAIParams.input,
output: [],
latency: 0,
baseURL: this.baseURL ?? '',
params: body,
httpStatus: error?.status ? error.status : 500,
usage: {
inputTokens: 0,
outputTokens: 0
},
isError: true,
error: JSON.stringify(error),
captureImmediate: posthogCaptureImmediate
});
throw error;
});
return wrappedPromise;
}
}
const mapVercelParams = params => {
return {
temperature: params.temperature,
max_output_tokens: params.maxOutputTokens,
top_p: params.topP,
frequency_penalty: params.frequencyPenalty,
presence_penalty: params.presencePenalty,
stop: params.stopSequences,
stream: params.stream
};
};
const mapVercelPrompt = messages => {
// Map and truncate individual content
const inputs = messages.map(message => {
let content;
// Handle system role which has string content
if (message.role === 'system') {
content = [{
type: 'text',
text: truncate(String(message.content))
}];
} else {
// Handle other roles which have array content
if (Array.isArray(message.content)) {
content = message.content.map(c => {
if (c.type === 'text') {
return {
type: 'text',
text: truncate(c.text)
};
} else if (c.type === 'file') {
// For file type, check if it's a data URL and redact if needed
let fileData;
const contentData = c.data;
if (contentData instanceof URL) {
fileData = contentData.toString();
} else if (isString(contentData)) {
// Redact base64 data URLs and raw base64 to prevent oversized events
fileData = redactBase64DataUrl(contentData);
} else {
fileData = 'raw files not supported';
}
return {
type: 'file',
file: fileData,
mediaType: c.mediaType
};
} else if (c.type === 'reasoning') {
return {
type: 'reasoning',
text: truncate(c.reasoning)
};
} else if (c.type === 'tool-call') {
return {
type: 'tool-call',
toolCallId: c.toolCallId,
toolName: c.toolName,
input: c.input
};
} else if (c.type === 'tool-result') {
return {
type: 'tool-result',
toolCallId: c.toolCallId,
toolName: c.toolName,
output: c.output,
isError: c.isError
};
}
return {
type: 'text',
text: ''
};
});
} else {
// Fallback for non-array content
content = [{
type: 'text',
text: truncate(String(message.content))
}];
}
}
return {
role: message.role,
content
};
});
try {
// Trim the inputs array until its JSON size fits within MAX_OUTPUT_SIZE
let serialized = JSON.stringify(inputs);
let removedCount = 0;
// We need to keep track of the initial size of the inputs array because we're going to be mutating it
const initialSize = inputs.length;
for (let i = 0; i < initialSize && buffer.Buffer.byteLength(serialized, 'utf8') > MAX_OUTPUT_SIZE; i++) {
inputs.shift();
removedCount++;
serialized = JSON.stringify(inputs);
}
if (removedCount > 0) {
// Add one placeholder to indicate how many were removed
inputs.unshift({
role: 'posthog',
content: `[${removedCount} message${removedCount === 1 ? '' : 's'} removed due to size limit]`
});
}
} catch (error) {
console.error('Error stringifying inputs', error);
return [{
role: 'posthog',
content: 'An error occurred while processing your request. Please try again.'
}];
}
return inputs;
};
const mapVercelOutput = result => {
const content = result.map(item => {
if (item.type === 'text') {
return {
type: 'text',
text: truncate(item.text)
};
}
if (item.type === 'tool-call') {
return {
type: 'tool-call',
id: item.toolCallId,
function: {
name: item.toolName,
arguments: item.args || JSON.stringify(item.arguments || {})
}
};
}
if (item.type === 'reasoning') {
return {
type: 'reasoning',
text: truncate(item.text)
};
}
if (item.type === 'file') {
// Handle files similar to input mapping - avoid large base64 data
let fileData;
if (item.data instanceof URL) {
fileData = item.data.toString();
} else if (typeof item.data === 'string') {
fileData = redactBase64DataUrl(item.data);
// If not redacted and still large, replace with size indicator
if (fileData === item.data && item.data.length > 1000) {
fileData = `[${item.mediaType} file - ${item.data.length} bytes]`;
}
} else {
fileData = `[binary ${item.mediaType} file]`;
}
return {
type: 'file',
name: 'generated_file',
mediaType: item.mediaType,
data: fileData
};
}
if (item.type === 'source') {
return {
type: 'source',
sourceType: item.sourceType,
id: item.id,
url: item.url || '',
title: item.title || ''
};
}
// Fallback for unknown types - try to extract text if possible
return {
type: 'text',
text: truncate(JSON.stringify(item))
};
});
if (content.length > 0) {
return [{
role: 'assistant',
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
}];
}
// otherwise stringify and truncate
try {
const jsonOutput = JSON.stringify(result);
return [{
content: truncate(jsonOutput),
role: 'assistant'
}];
} catch (error) {
console.error('Error stringifying output');
return [];
}
};
const extractProvider = model => {
const provider = model.provider.toLowerCase();
const providerName = provider.split('.')[0];
return providerName;
};
const createInstrumentationMiddleware = (phClient, model, options) => {
const middleware = {
wrapGenerate: async ({
doGenerate,
params
}) => {
const startTime = Date.now();
const mergedParams = {
...options,
...mapVercelParams(params)
};
const availableTools = extractAvailableToolCalls('vercel', params);
try {
const result = await doGenerate();
const modelId = options.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
const provider = options.posthogProviderOverride ?? extractProvider(model);
const baseURL = ''; // cannot currently get baseURL from vercel
const content = mapVercelOutput(result.content);
const latency = (Date.now() - startTime) / 1000;
const providerMetadata = result.providerMetadata;
const additionalTokenValues = {
...(providerMetadata?.anthropic ? {
cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
} : {})
};
const usage = {
inputTokens: result.usage.inputTokens,
outputTokens: result.usage.outputTokens,
reasoningTokens: result.usage.reasoningTokens,
cacheReadInputTokens: result.usage.cachedInputTokens,
...additionalTokenValues
};
await sendEventToPosthog({
client: phClient,
distinctId: options.posthogDistinctId,
traceId: options.posthogTraceId ?? uuid.v4(),
model: modelId,
provider: provider,
input: options.posthogPrivacyMode ? ''