@posthog/ai 5.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,14 +2,18 @@ import OpenAIOrignal, { ClientOptions } from 'openai'
2
2
  import { PostHog } from 'posthog-node'
3
3
  import { v4 as uuidv4 } from 'uuid'
4
4
  import { formatResponseOpenAI, MonitoringParams, sendEventToPosthog } from '../utils'
5
+ import type { APIPromise } from 'openai'
6
+ import type { Stream } from 'openai/streaming'
7
+ import type { ParsedResponse } from 'openai/resources/responses/responses'
5
8
 
6
9
  type ChatCompletion = OpenAIOrignal.ChatCompletion
7
10
  type ChatCompletionChunk = OpenAIOrignal.ChatCompletionChunk
8
11
  type ChatCompletionCreateParamsBase = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParams
9
12
  type ChatCompletionCreateParamsNonStreaming = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParamsNonStreaming
10
13
  type ChatCompletionCreateParamsStreaming = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParamsStreaming
11
- import type { APIPromise, RequestOptions } from 'openai/core'
12
- import type { Stream } from 'openai/streaming'
14
+ type ResponsesCreateParamsBase = OpenAIOrignal.Responses.ResponseCreateParams
15
+ type ResponsesCreateParamsNonStreaming = OpenAIOrignal.Responses.ResponseCreateParamsNonStreaming
16
+ type ResponsesCreateParamsStreaming = OpenAIOrignal.Responses.ResponseCreateParamsStreaming
13
17
 
14
18
  interface MonitoringOpenAIConfig extends ClientOptions {
15
19
  apiKey: string
@@ -17,15 +21,19 @@ interface MonitoringOpenAIConfig extends ClientOptions {
17
21
  baseURL?: string
18
22
  }
19
23
 
24
+ type RequestOptions = Record<string, any>
25
+
20
26
  export class PostHogOpenAI extends OpenAIOrignal {
21
27
  private readonly phClient: PostHog
22
28
  public chat: WrappedChat
29
+ public responses: WrappedResponses
23
30
 
24
31
  constructor(config: MonitoringOpenAIConfig) {
25
32
  const { posthog, ...openAIConfig } = config
26
33
  super(openAIConfig)
27
34
  this.phClient = posthog
28
35
  this.chat = new WrappedChat(this, this.phClient)
36
+ this.responses = new WrappedResponses(this, this.phClient)
29
37
  }
30
38
  }
31
39
 
@@ -118,7 +126,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
118
126
  const latency = (Date.now() - startTime) / 1000
119
127
  await sendEventToPosthog({
120
128
  client: this.phClient,
121
- distinctId: posthogDistinctId ?? traceId,
129
+ distinctId: posthogDistinctId,
122
130
  traceId,
123
131
  model: openAIParams.model,
124
132
  provider: 'openai',
@@ -134,7 +142,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
134
142
  } catch (error: any) {
135
143
  await sendEventToPosthog({
136
144
  client: this.phClient,
137
- distinctId: posthogDistinctId ?? traceId,
145
+ distinctId: posthogDistinctId,
138
146
  traceId,
139
147
  model: openAIParams.model,
140
148
  provider: 'openai',
@@ -164,7 +172,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
164
172
  const latency = (Date.now() - startTime) / 1000
165
173
  await sendEventToPosthog({
166
174
  client: this.phClient,
167
- distinctId: posthogDistinctId ?? traceId,
175
+ distinctId: posthogDistinctId,
168
176
  traceId,
169
177
  model: openAIParams.model,
170
178
  provider: 'openai',
@@ -188,7 +196,7 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
188
196
  async (error: any) => {
189
197
  await sendEventToPosthog({
190
198
  client: this.phClient,
191
- distinctId: posthogDistinctId ?? traceId,
199
+ distinctId: posthogDistinctId,
192
200
  traceId,
193
201
  model: openAIParams.model,
194
202
  provider: 'openai',
@@ -215,6 +223,272 @@ export class WrappedCompletions extends OpenAIOrignal.Chat.Completions {
215
223
  }
216
224
  }
217
225
 
226
+ export class WrappedResponses extends OpenAIOrignal.Responses {
227
+ private readonly phClient: PostHog
228
+
229
+ constructor(client: OpenAIOrignal, phClient: PostHog) {
230
+ super(client)
231
+ this.phClient = phClient
232
+ }
233
+
234
+ // --- Overload #1: Non-streaming
235
+ public create(
236
+ body: ResponsesCreateParamsNonStreaming & MonitoringParams,
237
+ options?: RequestOptions
238
+ ): APIPromise<OpenAIOrignal.Responses.Response>
239
+
240
+ // --- Overload #2: Streaming
241
+ public create(
242
+ body: ResponsesCreateParamsStreaming & MonitoringParams,
243
+ options?: RequestOptions
244
+ ): APIPromise<Stream<OpenAIOrignal.Responses.ResponseStreamEvent>>
245
+
246
+ // --- Overload #3: Generic base
247
+ public create(
248
+ body: ResponsesCreateParamsBase & MonitoringParams,
249
+ options?: RequestOptions
250
+ ): APIPromise<OpenAIOrignal.Responses.Response | Stream<OpenAIOrignal.Responses.ResponseStreamEvent>>
251
+
252
+ // --- Implementation Signature
253
+ public create(
254
+ body: ResponsesCreateParamsBase & MonitoringParams,
255
+ options?: RequestOptions
256
+ ): APIPromise<OpenAIOrignal.Responses.Response | Stream<OpenAIOrignal.Responses.ResponseStreamEvent>> {
257
+ const {
258
+ posthogDistinctId,
259
+ posthogTraceId,
260
+ posthogProperties,
261
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
262
+ posthogPrivacyMode = false,
263
+ posthogGroups,
264
+ posthogCaptureImmediate,
265
+ ...openAIParams
266
+ } = body
267
+
268
+ const traceId = posthogTraceId ?? uuidv4()
269
+ const startTime = Date.now()
270
+
271
+ const parentPromise = super.create(openAIParams, options)
272
+
273
+ if (openAIParams.stream) {
274
+ return parentPromise.then((value) => {
275
+ if ('tee' in value && typeof (value as any).tee === 'function') {
276
+ const [stream1, stream2] = (value as any).tee()
277
+ ;(async () => {
278
+ try {
279
+ let finalContent: any[] = []
280
+ let usage: {
281
+ inputTokens?: number
282
+ outputTokens?: number
283
+ reasoningTokens?: number
284
+ cacheReadInputTokens?: number
285
+ } = {
286
+ inputTokens: 0,
287
+ outputTokens: 0,
288
+ }
289
+
290
+ for await (const chunk of stream1) {
291
+ if (
292
+ chunk.type === 'response.completed' &&
293
+ 'response' in chunk &&
294
+ chunk.response?.output &&
295
+ chunk.response.output.length > 0
296
+ ) {
297
+ finalContent = chunk.response.output
298
+ }
299
+ if ('response' in chunk && chunk.response?.usage) {
300
+ usage = {
301
+ inputTokens: chunk.response.usage.input_tokens ?? 0,
302
+ outputTokens: chunk.response.usage.output_tokens ?? 0,
303
+ reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
304
+ cacheReadInputTokens: chunk.response.usage.input_tokens_details?.cached_tokens ?? 0,
305
+ }
306
+ }
307
+ }
308
+
309
+ const latency = (Date.now() - startTime) / 1000
310
+ await sendEventToPosthog({
311
+ client: this.phClient,
312
+ distinctId: posthogDistinctId,
313
+ traceId,
314
+ model: openAIParams.model,
315
+ provider: 'openai',
316
+ input: openAIParams.input,
317
+ output: finalContent,
318
+ latency,
319
+ baseURL: (this as any).baseURL ?? '',
320
+ params: body,
321
+ httpStatus: 200,
322
+ usage,
323
+ captureImmediate: posthogCaptureImmediate,
324
+ })
325
+ } catch (error: any) {
326
+ await sendEventToPosthog({
327
+ client: this.phClient,
328
+ distinctId: posthogDistinctId,
329
+ traceId,
330
+ model: openAIParams.model,
331
+ provider: 'openai',
332
+ input: openAIParams.input,
333
+ output: [],
334
+ latency: 0,
335
+ baseURL: (this as any).baseURL ?? '',
336
+ params: body,
337
+ httpStatus: error?.status ? error.status : 500,
338
+ usage: { inputTokens: 0, outputTokens: 0 },
339
+ isError: true,
340
+ error: JSON.stringify(error),
341
+ captureImmediate: posthogCaptureImmediate,
342
+ })
343
+ }
344
+ })()
345
+
346
+ return stream2
347
+ }
348
+ return value
349
+ }) as APIPromise<Stream<OpenAIOrignal.Responses.ResponseStreamEvent>>
350
+ } else {
351
+ const wrappedPromise = parentPromise.then(
352
+ async (result) => {
353
+ if ('output' in result) {
354
+ const latency = (Date.now() - startTime) / 1000
355
+ await sendEventToPosthog({
356
+ client: this.phClient,
357
+ distinctId: posthogDistinctId,
358
+ traceId,
359
+ model: openAIParams.model,
360
+ provider: 'openai',
361
+ input: openAIParams.input,
362
+ output: result.output,
363
+ latency,
364
+ baseURL: (this as any).baseURL ?? '',
365
+ params: body,
366
+ httpStatus: 200,
367
+ usage: {
368
+ inputTokens: result.usage?.input_tokens ?? 0,
369
+ outputTokens: result.usage?.output_tokens ?? 0,
370
+ reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
371
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
372
+ },
373
+ captureImmediate: posthogCaptureImmediate,
374
+ })
375
+ }
376
+ return result
377
+ },
378
+ async (error: any) => {
379
+ await sendEventToPosthog({
380
+ client: this.phClient,
381
+ distinctId: posthogDistinctId,
382
+ traceId,
383
+ model: openAIParams.model,
384
+ provider: 'openai',
385
+ input: openAIParams.input,
386
+ output: [],
387
+ latency: 0,
388
+ baseURL: (this as any).baseURL ?? '',
389
+ params: body,
390
+ httpStatus: error?.status ? error.status : 500,
391
+ usage: {
392
+ inputTokens: 0,
393
+ outputTokens: 0,
394
+ },
395
+ isError: true,
396
+ error: JSON.stringify(error),
397
+ captureImmediate: posthogCaptureImmediate,
398
+ })
399
+ throw error
400
+ }
401
+ ) as APIPromise<OpenAIOrignal.Responses.Response>
402
+
403
+ return wrappedPromise
404
+ }
405
+ }
406
+
407
+ public parse<Params extends ResponsesCreateParamsBase, ParsedT = any>(
408
+ body: Params & MonitoringParams,
409
+ options?: RequestOptions
410
+ ): APIPromise<ParsedResponse<ParsedT>> {
411
+ const {
412
+ posthogDistinctId,
413
+ posthogTraceId,
414
+ posthogProperties,
415
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
416
+ posthogPrivacyMode = false,
417
+ posthogGroups,
418
+ posthogCaptureImmediate,
419
+ ...openAIParams
420
+ } = body
421
+
422
+ const traceId = posthogTraceId ?? uuidv4()
423
+ const startTime = Date.now()
424
+
425
+ // Create a temporary instance that bypasses our wrapped create method
426
+ const originalCreate = super.create.bind(this)
427
+ const originalSelf = this as any
428
+ const tempCreate = originalSelf.create
429
+ originalSelf.create = originalCreate
430
+
431
+ try {
432
+ const parentPromise = super.parse(openAIParams, options)
433
+
434
+ const wrappedPromise = parentPromise.then(
435
+ async (result) => {
436
+ const latency = (Date.now() - startTime) / 1000
437
+ await sendEventToPosthog({
438
+ client: this.phClient,
439
+ distinctId: posthogDistinctId,
440
+ traceId,
441
+ model: openAIParams.model,
442
+ provider: 'openai',
443
+ input: openAIParams.input,
444
+ output: result.output,
445
+ latency,
446
+ baseURL: (this as any).baseURL ?? '',
447
+ params: body,
448
+ httpStatus: 200,
449
+ usage: {
450
+ inputTokens: result.usage?.input_tokens ?? 0,
451
+ outputTokens: result.usage?.output_tokens ?? 0,
452
+ reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
453
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
454
+ },
455
+ captureImmediate: posthogCaptureImmediate,
456
+ })
457
+ return result
458
+ },
459
+ async (error: any) => {
460
+ await sendEventToPosthog({
461
+ client: this.phClient,
462
+ distinctId: posthogDistinctId,
463
+ traceId,
464
+ model: openAIParams.model,
465
+ provider: 'openai',
466
+ input: openAIParams.input,
467
+ output: [],
468
+ latency: 0,
469
+ baseURL: (this as any).baseURL ?? '',
470
+ params: body,
471
+ httpStatus: error?.status ? error.status : 500,
472
+ usage: {
473
+ inputTokens: 0,
474
+ outputTokens: 0,
475
+ },
476
+ isError: true,
477
+ error: JSON.stringify(error),
478
+ captureImmediate: posthogCaptureImmediate,
479
+ })
480
+ throw error
481
+ }
482
+ )
483
+
484
+ return wrappedPromise as APIPromise<ParsedResponse<ParsedT>>
485
+ } finally {
486
+ // Restore our wrapped create method
487
+ originalSelf.create = tempCreate
488
+ }
489
+ }
490
+ }
491
+
218
492
  export default PostHogOpenAI
219
493
 
220
494
  export { PostHogOpenAI as OpenAI }
package/src/utils.ts CHANGED
@@ -5,6 +5,7 @@ import AnthropicOriginal from '@anthropic-ai/sdk'
5
5
 
6
6
  type ChatCompletionCreateParamsBase = OpenAIOrignal.Chat.Completions.ChatCompletionCreateParams
7
7
  type MessageCreateParams = AnthropicOriginal.Messages.MessageCreateParams
8
+ type ResponseCreateParams = OpenAIOrignal.Responses.ResponseCreateParams
8
9
 
9
10
  // limit large outputs by truncating to 200kb (approx 200k bytes)
10
11
  export const MAX_OUTPUT_SIZE = 200000
@@ -28,7 +29,7 @@ export interface CostOverride {
28
29
  }
29
30
 
30
31
  export const getModelParams = (
31
- params: ((ChatCompletionCreateParamsBase | MessageCreateParams) & MonitoringParams) | null
32
+ params: ((ChatCompletionCreateParamsBase | MessageCreateParams | ResponseCreateParams) & MonitoringParams) | null
32
33
  ): Record<string, any> => {
33
34
  if (!params) {
34
35
  return {}
@@ -178,7 +179,7 @@ export type SendEventToPosthogParams = {
178
179
  cacheReadInputTokens?: any
179
180
  cacheCreationInputTokens?: any
180
181
  }
181
- params: (ChatCompletionCreateParamsBase | MessageCreateParams) & MonitoringParams
182
+ params: (ChatCompletionCreateParamsBase | MessageCreateParams | ResponseCreateParams) & MonitoringParams
182
183
  isError?: boolean
183
184
  error?: string
184
185
  tools?: any
@@ -18,8 +18,8 @@ interface ClientOptions {
18
18
  }
19
19
 
20
20
  interface CreateInstrumentationMiddlewareOptions {
21
- posthogDistinctId: string
22
- posthogTraceId: string
21
+ posthogDistinctId?: string
22
+ posthogTraceId?: string
23
23
  posthogProperties?: Record<string, any>
24
24
  posthogPrivacyMode?: boolean
25
25
  posthogGroups?: Record<string, any>
@@ -229,7 +229,7 @@ export const createInstrumentationMiddleware = (
229
229
  await sendEventToPosthog({
230
230
  client: phClient,
231
231
  distinctId: options.posthogDistinctId,
232
- traceId: options.posthogTraceId,
232
+ traceId: options.posthogTraceId ?? uuidv4(),
233
233
  model: modelId,
234
234
  provider: provider,
235
235
  input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
@@ -252,7 +252,7 @@ export const createInstrumentationMiddleware = (
252
252
  await sendEventToPosthog({
253
253
  client: phClient,
254
254
  distinctId: options.posthogDistinctId,
255
- traceId: options.posthogTraceId,
255
+ traceId: options.posthogTraceId ?? uuidv4(),
256
256
  model: modelId,
257
257
  provider: model.provider,
258
258
  input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
@@ -324,7 +324,7 @@ export const createInstrumentationMiddleware = (
324
324
  await sendEventToPosthog({
325
325
  client: phClient,
326
326
  distinctId: options.posthogDistinctId,
327
- traceId: options.posthogTraceId,
327
+ traceId: options.posthogTraceId ?? uuidv4(),
328
328
  model: modelId,
329
329
  provider: provider,
330
330
  input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
@@ -347,7 +347,7 @@ export const createInstrumentationMiddleware = (
347
347
  await sendEventToPosthog({
348
348
  client: phClient,
349
349
  distinctId: options.posthogDistinctId,
350
- traceId: options.posthogTraceId,
350
+ traceId: options.posthogTraceId ?? uuidv4(),
351
351
  model: modelId,
352
352
  provider: provider,
353
353
  input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
@@ -381,7 +381,7 @@ export const wrapVercelLanguageModel = (
381
381
  const middleware = createInstrumentationMiddleware(phClient, model, {
382
382
  ...options,
383
383
  posthogTraceId: traceId,
384
- posthogDistinctId: options.posthogDistinctId ?? traceId,
384
+ posthogDistinctId: options.posthogDistinctId,
385
385
  })
386
386
 
387
387
  const wrappedModel = wrapLanguageModel({
@@ -310,4 +310,35 @@ describe('PostHogGemini - Jest test suite', () => {
310
310
  expect(vertexClient).toBeInstanceOf(PostHogGemini)
311
311
  expect(vertexClient.models).toBeDefined()
312
312
  })
313
+
314
+ conditionalTest('anonymous user - $process_person_profile set to false', async () => {
315
+ await client.models.generateContent({
316
+ model: 'gemini-2.0-flash-001',
317
+ contents: 'Hello',
318
+ posthogTraceId: 'trace-123',
319
+ })
320
+
321
+ expect(mockPostHogClient.capture).toHaveBeenCalledTimes(1)
322
+ const [captureArgs] = (mockPostHogClient.capture as jest.Mock).mock.calls
323
+ const { distinctId, properties } = captureArgs[0]
324
+
325
+ expect(distinctId).toBe('trace-123')
326
+ expect(properties['$process_person_profile']).toBe(false)
327
+ })
328
+
329
+ conditionalTest('identified user - $process_person_profile not set', async () => {
330
+ await client.models.generateContent({
331
+ model: 'gemini-2.0-flash-001',
332
+ contents: 'Hello',
333
+ posthogDistinctId: 'user-456',
334
+ posthogTraceId: 'trace-123',
335
+ })
336
+
337
+ expect(mockPostHogClient.capture).toHaveBeenCalledTimes(1)
338
+ const [captureArgs] = (mockPostHogClient.capture as jest.Mock).mock.calls
339
+ const { distinctId, properties } = captureArgs[0]
340
+
341
+ expect(distinctId).toBe('user-456')
342
+ expect(properties['$process_person_profile']).toBeUndefined()
343
+ })
313
344
  })