UNPKG

@posthog/ai

Version:
495 lines (453 loc) 17.3 kB
import OpenAIOrignal, { ClientOptions } from 'openai' import { PostHog } from 'posthog-node' import { v4 as uuidv4 } from 'uuid' import { formatResponseOpenAI, MonitoringParams, sendEventToPosthog } from '../utils' import type { APIPromise } from 'openai' import type { Stream } from 'openai/streaming' import type { ParsedResponse } from 'openai/resources/responses/responses' type ChatCompletion = OpenAIOrignal.ChatCompletion type ChatCompletionChunk = OpenAIOrignal.ChatCompletionChunk type ChatCompletionCreateParamsBase = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParams type ChatCompletionCreateParamsNonStreaming = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParamsNonStreaming type ChatCompletionCreateParamsStreaming = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParamsStreaming type ResponsesCreateParamsBase = OpenAIOrignal.Responses.ResponseCreateParams type ResponsesCreateParamsNonStreaming = OpenAIOrignal.Responses.ResponseCreateParamsNonStreaming type ResponsesCreateParamsStreaming = OpenAIOrignal.Responses.ResponseCreateParamsStreaming interface MonitoringOpenAIConfig extends ClientOptions { apiKey: string posthog: PostHog baseURL?: string } type RequestOptions = Record<string, any> export class PostHogOpenAI extends OpenAIOrignal { private readonly phClient: PostHog public chat: WrappedChat public responses: WrappedResponses constructor(config: MonitoringOpenAIConfig) { const { posthog, ...openAIConfig } = config super(openAIConfig) this.phClient = posthog this.chat = new WrappedChat(this, this.phClient) this.responses = new WrappedResponses(this, this.phClient) } } export class WrappedChat extends OpenAIOrignal.Chat { constructor(parentClient: PostHogOpenAI, phClient: PostHog) { super(parentClient) this.completions = new WrappedCompletions(parentClient, phClient) } public completions: WrappedCompletions } export class WrappedCompletions extends OpenAIOrignal.Chat.Completions { private readonly phClient: PostHog constructor(client: OpenAIOrignal, phClient: PostHog) { super(client) this.phClient = phClient } // --- Overload #1: Non-streaming public create( body: ChatCompletionCreateParamsNonStreaming & MonitoringParams, options?: RequestOptions ): APIPromise<ChatCompletion> // --- Overload #2: Streaming public create( body: ChatCompletionCreateParamsStreaming & MonitoringParams, options?: RequestOptions ): APIPromise<Stream<ChatCompletionChunk>> // --- Overload #3: Generic base public create( body: ChatCompletionCreateParamsBase & MonitoringParams, options?: RequestOptions ): APIPromise<ChatCompletion | Stream<ChatCompletionChunk>> // --- Implementation Signature public create( body: ChatCompletionCreateParamsBase & MonitoringParams, options?: RequestOptions ): APIPromise<ChatCompletion | Stream<ChatCompletionChunk>> { const { posthogDistinctId, posthogTraceId, posthogProperties, // eslint-disable-next-line @typescript-eslint/no-unused-vars posthogPrivacyMode = false, posthogGroups, posthogCaptureImmediate, ...openAIParams } = body const traceId = posthogTraceId ?? uuidv4() 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?: number outputTokens?: number reasoningTokens?: number cacheReadInputTokens?: number } = { 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 as any).baseURL ?? '', params: body, httpStatus: 200, usage, captureImmediate: posthogCaptureImmediate, }) } catch (error: any) { await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: openAIParams.model, provider: 'openai', input: openAIParams.messages, output: [], latency: 0, baseURL: (this as any).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 }) as APIPromise<Stream<ChatCompletionChunk>> } 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 as any).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: any) => { await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: openAIParams.model, provider: 'openai', input: openAIParams.messages, output: [], latency: 0, baseURL: (this as any).baseURL ?? '', params: body, httpStatus: error?.status ? error.status : 500, usage: { inputTokens: 0, outputTokens: 0, }, isError: true, error: JSON.stringify(error), captureImmediate: posthogCaptureImmediate, }) throw error } ) as APIPromise<ChatCompletion> return wrappedPromise } } } export class WrappedResponses extends OpenAIOrignal.Responses { private readonly phClient: PostHog constructor(client: OpenAIOrignal, phClient: PostHog) { super(client) this.phClient = phClient } // --- Overload #1: Non-streaming public create( body: ResponsesCreateParamsNonStreaming & MonitoringParams, options?: RequestOptions ): APIPromise<OpenAIOrignal.Responses.Response> // --- Overload #2: Streaming public create( body: ResponsesCreateParamsStreaming & MonitoringParams, options?: RequestOptions ): APIPromise<Stream<OpenAIOrignal.Responses.ResponseStreamEvent>> // --- Overload #3: Generic base public create( body: ResponsesCreateParamsBase & MonitoringParams, options?: RequestOptions ): APIPromise<OpenAIOrignal.Responses.Response | Stream<OpenAIOrignal.Responses.ResponseStreamEvent>> // --- Implementation Signature public create( body: ResponsesCreateParamsBase & MonitoringParams, options?: RequestOptions ): APIPromise<OpenAIOrignal.Responses.Response | Stream<OpenAIOrignal.Responses.ResponseStreamEvent>> { const { posthogDistinctId, posthogTraceId, posthogProperties, // eslint-disable-next-line @typescript-eslint/no-unused-vars posthogPrivacyMode = false, posthogGroups, posthogCaptureImmediate, ...openAIParams } = body const traceId = posthogTraceId ?? uuidv4() const startTime = Date.now() const parentPromise = super.create(openAIParams, options) if (openAIParams.stream) { return parentPromise.then((value) => { if ('tee' in value && typeof (value as any).tee === 'function') { const [stream1, stream2] = (value as any).tee() ;(async () => { try { let finalContent: any[] = [] let usage: { inputTokens?: number outputTokens?: number reasoningTokens?: number cacheReadInputTokens?: number } = { 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 as any).baseURL ?? '', params: body, httpStatus: 200, usage, captureImmediate: posthogCaptureImmediate, }) } catch (error: any) { await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: openAIParams.model, provider: 'openai', input: openAIParams.input, output: [], latency: 0, baseURL: (this as any).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 }) as APIPromise<Stream<OpenAIOrignal.Responses.ResponseStreamEvent>> } 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 as any).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: any) => { await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: openAIParams.model, provider: 'openai', input: openAIParams.input, output: [], latency: 0, baseURL: (this as any).baseURL ?? '', params: body, httpStatus: error?.status ? error.status : 500, usage: { inputTokens: 0, outputTokens: 0, }, isError: true, error: JSON.stringify(error), captureImmediate: posthogCaptureImmediate, }) throw error } ) as APIPromise<OpenAIOrignal.Responses.Response> return wrappedPromise } } public parse<Params extends ResponsesCreateParamsBase, ParsedT = any>( body: Params & MonitoringParams, options?: RequestOptions ): APIPromise<ParsedResponse<ParsedT>> { const { posthogDistinctId, posthogTraceId, posthogProperties, // eslint-disable-next-line @typescript-eslint/no-unused-vars posthogPrivacyMode = false, posthogGroups, posthogCaptureImmediate, ...openAIParams } = body const traceId = posthogTraceId ?? uuidv4() const startTime = Date.now() // Create a temporary instance that bypasses our wrapped create method const originalCreate = super.create.bind(this) const originalSelf = this as any 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 as any).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: any) => { await sendEventToPosthog({ client: this.phClient, distinctId: posthogDistinctId ?? traceId, traceId, model: openAIParams.model, provider: 'openai', input: openAIParams.input, output: [], latency: 0, baseURL: (this as any).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 as APIPromise<ParsedResponse<ParsedT>> } finally { // Restore our wrapped create method originalSelf.create = tempCreate } } } export default PostHogOpenAI export { PostHogOpenAI as OpenAI }