@posthog/ai
Version:
PostHog Node.js AI integrations
543 lines (525 loc) • 17.9 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var OpenAIOrignal = require('openai');
var uuid = require('uuid');
var buffer = require('buffer');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var OpenAIOrignal__default = /*#__PURE__*/_interopDefaultLegacy(OpenAIOrignal);
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 formatResponseOpenAI = response => {
const output = [];
for (const choice of response.choices ?? []) {
if (choice.message?.content) {
output.push({
role: choice.message.role,
content: choice.message.content
});
}
}
return output;
};
const withPrivacyMode = (client, privacyMode, input) => {
return client.privacy_mode || privacyMode ? null : input;
};
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);
}
};
class PostHogOpenAI extends OpenAIOrignal__default["default"] {
constructor(config) {
const {
posthog,
...openAIConfig
} = config;
super(openAIConfig);
this.phClient = posthog;
this.chat = new WrappedChat(this, this.phClient);
this.responses = new WrappedResponses(this, this.phClient);
}
}
class WrappedChat extends OpenAIOrignal__default["default"].Chat {
constructor(parentClient, phClient) {
super(parentClient);
this.completions = new WrappedCompletions(parentClient, phClient);
}
}
class WrappedCompletions extends OpenAIOrignal__default["default"].Chat.Completions {
constructor(client, phClient) {
super(client);
this.phClient = phClient;
}
// --- Overload #1: Non-streaming
// --- Overload #2: Streaming
// --- Overload #3: Generic base
// --- 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,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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 OpenAIOrignal__default["default"].Responses {
constructor(client, phClient) {
super(client);
this.phClient = phClient;
}
// --- Overload #1: Non-streaming
// --- Overload #2: Streaming
// --- Overload #3: Generic base
// --- 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;
await sendEventToPosthog({
client: this.phClient,
distinctId: posthogDistinctId ?? traceId,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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();
// 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,
traceId,
model: openAIParams.model,
provider: 'openai',
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,
traceId,
model: openAIParams.model,
provider: 'openai',
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;
} finally {
// Restore our wrapped create method
originalSelf.create = tempCreate;
}
}
}
exports.OpenAI = PostHogOpenAI;
exports.PostHogOpenAI = PostHogOpenAI;
exports.WrappedChat = WrappedChat;
exports.WrappedCompletions = WrappedCompletions;
exports.WrappedResponses = WrappedResponses;
exports["default"] = PostHogOpenAI;
//# sourceMappingURL=index.cjs.map