UNPKG

@posthog/ai

Version:
1,507 lines (1,501 loc) 87.9 kB
import OpenAIOrignal, { AzureOpenAI } from 'openai'; import * as uuid from 'uuid'; import { v4 } from 'uuid'; import { Buffer } from 'buffer'; import { experimental_wrapLanguageModel } from 'ai'; import AnthropicOriginal from '@anthropic-ai/sdk'; import { GoogleGenAI } from '@google/genai'; // 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 => { // Example approach if "response.content" holds array of text segments, etc. const output = []; for (const choice of response.content ?? []) { if (choice?.text) { output.push({ role: 'assistant', content: choice.text }); } } return output; }; 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 mergeSystemPrompt = (params, provider) => { if (provider == 'anthropic') { const messages = params.messages || []; if (!params.system) { return messages; } const systemMessage = params.system; return [{ role: 'system', content: systemMessage }, ...messages]; } return params.messages; }; const withPrivacyMode = (client, privacyMode, input) => { return client.privacy_mode || privacyMode ? null : input; }; const truncate = str => { try { const buffer = Buffer.from(str, STRING_FORMAT); if (buffer.length <= MAX_OUTPUT_SIZE) { return str; } const truncatedBuffer = buffer.slice(0, MAX_OUTPUT_SIZE); return `${truncatedBuffer.toString(STRING_FORMAT)}... [truncated]`; } catch (error) { console.error('Error truncating, likely not a string'); return str; } }; function sanitizeValues(obj) { if (obj === undefined || obj === null) { return obj; } const jsonSafe = JSON.parse(JSON.stringify(obj)); if (typeof jsonSafe === 'string') { return 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 { 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); } } class WrappedChat$1 extends OpenAIOrignal.Chat { constructor(parentClient, phClient) { super(parentClient); this.completions = new WrappedCompletions$1(parentClient, phClient); } } class WrappedCompletions$1 extends OpenAIOrignal.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 ?? 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$1 extends OpenAIOrignal.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 ?? 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 ?? 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; } } } class PostHogAzureOpenAI extends AzureOpenAI { constructor(config) { const { posthog, ...openAIConfig } = config; super(openAIConfig); this.phClient = posthog; this.chat = new WrappedChat(this, this.phClient); } } class WrappedChat extends AzureOpenAI.Chat { constructor(parentClient, phClient) { super(parentClient); this.completions = new WrappedCompletions(parentClient, phClient); } } class WrappedCompletions extends 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 ?? 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: '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, 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, 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, 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 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 ?? 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, traceId, 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, traceId, 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, traceId, 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, traceId, 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 ?? 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, traceId, 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, traceId, 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_tokens: params.maxTokens, top_p: params.topP, frequency_penalty: params.frequencyPenalty, presence_penalty: params.presencePenalty, stop: params.stopSequences, stream: params.stream }; }; const mapVercelPrompt = prompt => { // normalize single inputs into an array of messages let promptsArray; if (typeof prompt === 'string') { promptsArray = [{ role: 'user', content: prompt }]; } else if (!Array.isArray(prompt)) { promptsArray = [prompt]; } else { promptsArray = prompt; } // Map and truncate individual content const inputs = promptsArray.map(p => { let content = {}; if (Array.isArray(p.content)) { content = p.content.map(c => { if (c.type === 'text') { return { type: 'text', content: truncate(c.text) }; } else if (c.type === 'image') { return { type: 'image', content: { // if image is a url use it, or use "none supported" image: c.image instanceof URL ? c.image.toString() : 'raw images not supported', mimeType: c.mimeType } }; } else if (c.type === 'file') { return { type: 'file', content: { file: c.data instanceof URL ? c.data.toString() : 'raw files not supported', mimeType: c.mimeType } }; } else if (c.type === 'tool-call') { return { type: 'tool-call', content: { toolCallId: c.toolCallId, toolName: c.toolName, args: c.args } }; } else if (c.type === 'tool-result') { return { type: 'tool-result', content: { toolCallId: c.toolCallId, toolName: c.toolName, result: c.result, isError: c.isError } }; } return { content: '' }; }); } else { content = { type: 'text', text: truncate(p.content) }; } return { role: p.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.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 => { // normalize string results to object const normalizedResult = typeof result === 'string' ? { text: result } : result; const output = { ...(normalizedResult.text ? { text: normalizedResult.text } : {}), ...(normalizedResult.object ? { object: normalizedResult.object } : {}), ...(normalizedResult.reasoning ? { reasoning: normalizedResult.reasoning } : {}), ...(normalizedResult.response ? { response: normalizedResult.response } : {}), ...(normalizedResult.finishReason ? { finishReason: normalizedResult.finishReason } : {}), ...(normalizedResult.usage ? { usage: normalizedResult.usage } : {}), ...(normalizedResult.warnings ? { warnings: normalizedResult.warnings } : {}), ...(normalizedResult.providerMetadata ? { toolCalls: normalizedResult.providerMetadata } : {}), ...(normalizedResult.files ? { files: normalizedResult.files.map(file => ({ name: file.name, size: file.size, type: file.type })) } : {}) }; if (output.text && !output.object && !output.reasoning) { return [{ content: truncate(output.text), role: 'assistant' }]; } // otherwise stringify and truncate try { const jsonOutput = JSON.stringify(output); 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) }; try { const result = await doGenerate(); const latency = (Date.now() - startTime) / 1000; 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); // let tools = result.toolCalls const providerMetadata = result.providerMetadata; const additionalTokenValues = { ...(providerMetadata?.openai?.reasoningTokens ? { reasoningTokens: providerMetadata.openai.reasoningTokens } : {}), ...(providerMetadata?.openai?.cachedPromptTokens ? { cacheReadInputTokens: providerMetadata.openai.cachedPromptTokens } : {}), ...(providerMetadata?.anthropic ? { cacheReadInputTokens: providerMetadata.anthropic.cacheReadInputTokens, cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens } : {}) }; await sendEventToPosthog({ client: phClient, distinctId: options.posthogDistinctId, traceId: options.posthogTraceId, model: modelId, provider: provider, input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt), output: [{ content, role: 'assistant' }], latency, baseURL, params: mergedParams, httpStatus: 200, usage: { inputTokens: result.usage.promptTokens, outputTokens: result.usage.completionTokens, ...additionalTokenValues }, captureImmediate: options.posthogCaptureImmediate }); return result; } catch (error) { const modelId = model.modelId; await sendEventToPosthog({ client: phClient, distinctId: options.posthogDistinctId, traceId: options.posthogTraceId, model: modelId, provider: model.provider, input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt), output: [], latency: 0, baseURL: '', params: mergedParams, httpStatus: error?.status ? error.status : 500, usage: { inputTokens: 0, outputTokens: 0 }, isError: true, error: truncate(JSON.stringify(error)), captureImmediate: options.posthogCaptureImmediate }); throw error; } }, wrapStream: async ({ doStream, params }) => { const startTime = Date.now(); let generatedText = ''; let usage = {}; const mergedParams = { ...options, ...mapVercelParams(params) }; const modelId = options.posthogModelOverride ?? model.modelId; const provider = options.posthogProviderOverride ?? extractProvider(model); const baseURL = ''; // cannot currently get baseURL from vercel try { const { stream, ...rest } = await doStream(); const transformStream = new TransformStream({ transform(chunk, controller) { if (chunk.type === 'text-delta') { generatedText += chunk.textDelta; } if (chunk.type === 'finish') { usage = { inputTokens: chunk.usage?.promptTokens, outputTokens: chunk.usage?.completionTokens }; if (chunk.providerMetadata?.openai?.reasoningTokens) { usage.reasoningTokens = chunk.providerMetadata.openai.reasoningTokens; } if (chunk.providerMetadata?.openai?.cachedPromptTokens) { usage.cacheReadInputTokens = chunk.providerMetadata.openai.cachedPromptTokens; } if (chunk.providerMetadata?.anthropic?.cacheReadInputTokens) { usage.cacheReadInputTokens = chunk.providerMetadata.anthropic.cacheReadInputTokens; } if (chunk.providerMetadata?.anthropic?.cacheCreationInputTokens) { usage.cacheCreationInputTokens = chunk.providerMetadata.anthropic.cacheCreationInputTokens; } } controller.enqueue(chunk); }, flush: async () => { const latency = (Date.now() - startTime) / 1000; await sendEventToPosthog({ client: phClient, distinctId: options.posthogDistinctId, traceId: options.posthogTraceId, model: modelId, provider: provider, input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt), output: [{ content: generatedText, role: 'assistant' }], latency, baseURL, params: mergedParams, httpStatus: 200, usage, captureImmediate: options.posthogCaptureImmediate }); } }); return { stream: stream.pipeThrough(transformStream), ...rest }; } catch (error) { await sendEventToPosthog({ client: phClient, distinctId: options.posthogDistinctId, traceId: options.posthogTraceId, model: modelId, provider: provider, input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt), output: [], latency: 0, baseURL: '', params: mergedParams, httpStatus: error?.status ? error.status : 500, usage: { inputTokens: 0, outputTokens: 0 }, isError: true, error: truncate(JSON.stringify(error)), captureImmediate: options.posthogCaptureImmediate }); throw error; } } }; return middleware; }; const wrapVercelLanguageModel = (model, phClient, options) => { const traceId = options.posthogTraceId ?? v4(); const middleware = createInstrumentationMiddleware(phClient, model, { ...options, posthogTraceId: traceId, posthogDistinctId: options.posthogDistinctId ?? traceId }); const wrappedModel = experimental_wrapLanguageModel({ model, middleware }); return wrappedModel; }; class PostHogAnthropic extends AnthropicOriginal { constructor(config) { const { posthog, ...anthropicConfig } = config; super(anthropicConfig); this.phClient = posthog; this.messages = new WrappedMessages(this, this.phClient); } } class WrappedMessages extends AnthropicOriginal.Messages { constructor(parentClient, phClient) { super(parentClient); this.phClient = phClient; } create(body, options) { const { posthogDistinctId, posthogTraceId, posthogProperties, // eslint-disable-next-line @typescript-eslint/no-unused-vars posthogPrivacyMode = false, posthogGroups, posthogCaptureImmediate, ...anthropicParams } = body; const traceId = posthogTraceId ?? v4(); const startTime = Date.now(); const parentPromise = super.create(anthropicParams, options); if (anthropicParams.stream) { return parentPromise.then(value => { let accumulatedContent = ''; const usage = { inputTokens: 0, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0 }; if ('tee' in value) { const [stream1, stream2] = value.tee(); (async () => { try { for await (const chunk of stream1) { if ('delta' in chunk) { if ('text' in chunk.delta) { const delta = chunk?.delta?.text ?? ''; accumulatedContent += delta; } } if (chunk.type == 'message_start') { 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; } if ('usage' in chunk) { usage.outputTokens = chunk.usage.output_tokens ?? 0; } } const latency = (Date.now() - startTime) / 1000; await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: anthropicParams.model, provider: 'anthropic', input: mergeSystemPrompt(anthropicParams, 'anthropic'), output: [{ content: accumulatedContent, role: 'assistant' }], latency, baseURL: this.baseURL ?? '', params: body, httpStatus: 200, usage, captureImmediate: posthogCaptureImmediate }); } catch (error) { // error handling await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: anthropicParams.model, provider: 'anthropic', input: mergeSystemPrompt(anthropicParams, 'anthropic'), 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 ('content' in result) { const latency = (Date.now() - startTime) / 1000; await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: anthropicParams.model, provider: 'anthropic', input: mergeSystemPrompt(anthropicParams, 'anthropic'), 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 }, captureImmediate: posthogCaptureImmediate }); } return result; }, async error => { await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: anthropicParams.model, provider: 'anthropic', input: mergeSystemPrompt(anthropicParams, 'anthropic'), 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 PostHogGoogleGenAI { constructor(config) { const { posthog, ...geminiConfig } = config; this.phClient = posthog; this.client = new GoogleGenAI(geminiConfig); this.models = new WrappedModels(this.client, this.phClient); } } class WrappedModels { constructor(client, phClient) { this.client = client; this.phClient = phClient; } async generateContent(params) { const { posthogDistinctId, posthogTraceId, posthogProperties, posthogGroups, posthogCaptureImmediate, ...geminiParams } = params; const traceId = posthogTraceId ?? v4(); const startTime = Date.now(); try { const response = await this.client.models.generateContent(geminiParams); const latency = (Date.now() - startTime) / 1000; await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: geminiParams.model, provider: 'gemini', input: this.formatInput(geminiParams.contents), output: this.formatOutput(response), latency, baseURL: 'https://generativelanguage.googleapis.com', params: params, httpStatus: 200, usage: { inputTokens: response.usageMetadata?.promptTokenCount ?? 0, outputTokens: response.usageMetadata?.candidatesTokenCount ?? 0 }, captureImmediate: posthogCaptureImmediate }); return response; } catch (error) { const latency = (Date.now() - startTime) / 1000; await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: geminiParams.model, provider: 'gemini', input: this.formatInput(geminiParams.contents), output: [], latency, baseURL: