UNPKG

@posthog/ai

Version:
1,567 lines (1,558 loc) 103 kB
'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 ? ''