@posthog/ai
Version:
PostHog Node.js AI integrations
495 lines (453 loc) • 17.3 kB
text/typescript
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 }