@posthog/ai 7.16.15 → 7.17.1

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,8 +2,6 @@ import { GoogleGenAI } from '@google/genai';
2
2
  import { v4 } from 'uuid';
3
3
  import { uuidv7 } from '@posthog/core';
4
4
 
5
- var version = "7.16.15";
6
-
7
5
  // Type guards for safer type checking
8
6
 
9
7
  const isString = value => {
@@ -111,6 +109,14 @@ function getTokensSource(posthogProperties) {
111
109
  }
112
110
  return 'sdk';
113
111
  }
112
+ const STRING_FORMAT = 'utf8';
113
+
114
+ // Reused across calls to avoid per-invocation allocation; truncate() runs
115
+ // hundreds of times for prompts with many parts.
116
+ new TextEncoder();
117
+ new TextDecoder(STRING_FORMAT, {
118
+ fatal: false
119
+ });
114
120
 
115
121
  /**
116
122
  * Safely converts content to a string, preserving structure for objects/arrays.
@@ -295,73 +301,69 @@ function addDefaults(params) {
295
301
  traceId: params.traceId ?? v4()
296
302
  };
297
303
  }
298
- const sendEventWithErrorToPosthog = async ({
299
- client,
300
- traceId,
301
- error,
302
- ...args
303
- }) => {
304
- const httpStatus = error && typeof error === 'object' && 'status' in error ? error.status ?? 500 : 500;
305
- const properties = {
306
- client,
307
- traceId,
308
- httpStatus,
309
- error: JSON.stringify(error),
310
- ...args
311
- };
312
- const enrichedError = error;
313
- if (client.options?.enableExceptionAutocapture) {
314
- // assign a uuid that can be used to link the trace and exception events
315
- const exceptionId = uuidv7();
316
- client.captureException(error, undefined, {
317
- $ai_trace_id: traceId
318
- }, exceptionId);
319
- enrichedError.__posthog_previously_captured_error = true;
320
- properties.exceptionId = exceptionId;
321
- }
322
- await sendEventToPosthog(properties);
323
- return enrichedError;
324
- };
325
- const sendEventToPosthog = async ({
326
- client,
327
- eventType = AIEvent.Generation,
328
- distinctId,
329
- traceId,
330
- model,
331
- provider,
332
- input,
333
- output,
334
- latency,
335
- timeToFirstToken,
336
- baseURL,
337
- params,
338
- httpStatus = 200,
339
- usage = {},
340
- error,
341
- exceptionId,
342
- stopReason,
343
- tools,
344
- captureImmediate = false
345
- }) => {
304
+
305
+ var version = "7.17.1";
306
+
307
+ /**
308
+ * Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape
309
+ * directly so that any caller — first-party SDK wrappers and external code
310
+ * alike produces an identical event.
311
+ */
312
+
313
+ /**
314
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
315
+ *
316
+ * This is the canonical primitive that every `@posthog/ai` wrapper
317
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
318
+ * external code can use it directly to instrument LLM calls made through
319
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
320
+ * same events the SDK wrappers produce.
321
+ *
322
+ * When `error` is set, the event is captured as an error. If the error is an
323
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
324
+ * so callers can re-throw the original error reference safely.
325
+ */
326
+ const captureAiGeneration = async (client, options) => {
346
327
  if (!client.capture) {
347
- return Promise.resolve();
348
- }
349
- // sanitize input and output for UTF-8 validity
350
- const safeInput = sanitizeValues(input);
351
- const safeOutput = sanitizeValues(output);
352
- const safeError = sanitizeValues(error);
328
+ return;
329
+ }
330
+ const traceId = options.traceId ?? v4();
331
+ const eventType = options.eventType ?? AIEvent.Generation;
332
+ const privacyMode = options.privacyMode ?? false;
333
+ const usage = options.usage ?? {};
334
+ const safeInput = sanitizeValues(options.input);
335
+ const safeOutput = sanitizeValues(options.output);
336
+ let httpStatus = options.httpStatus;
353
337
  let errorData = {};
354
- if (error) {
338
+ if (options.error) {
339
+ if (httpStatus === undefined) {
340
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
341
+ httpStatus = options.error.status;
342
+ } else {
343
+ httpStatus = 500;
344
+ }
345
+ }
346
+ let exceptionId;
347
+ if (client.options?.enableExceptionAutocapture) {
348
+ exceptionId = uuidv7();
349
+ client.captureException(options.error, undefined, {
350
+ $ai_trace_id: traceId
351
+ }, exceptionId);
352
+ if (typeof options.error === 'object') {
353
+ options.error.__posthog_previously_captured_error = true;
354
+ }
355
+ }
355
356
  errorData = {
356
357
  $ai_is_error: true,
357
- $ai_error: safeError,
358
+ $ai_error: sanitizeValues(JSON.stringify(options.error)),
358
359
  $exception_event_id: exceptionId
359
360
  };
360
361
  }
362
+ httpStatus = httpStatus ?? 200;
361
363
  let costOverrideData = {};
362
- if (params.posthogCostOverride) {
363
- const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
364
- const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
364
+ if (options.costOverride) {
365
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
366
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
365
367
  costOverrideData = {
366
368
  $ai_input_cost_usd: inputCostUSD,
367
369
  $ai_output_cost_usd: outputCostUSD,
@@ -388,50 +390,48 @@ const sendEventToPosthog = async ({
388
390
  const properties = {
389
391
  $ai_lib: 'posthog-ai',
390
392
  $ai_lib_version: version,
391
- $ai_provider: params.posthogProviderOverride ?? provider,
392
- $ai_model: params.posthogModelOverride ?? model,
393
- $ai_model_parameters: getModelParams(params),
394
- $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
395
- $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
393
+ $ai_provider: options.providerOverride ?? options.provider,
394
+ $ai_model: options.modelOverride ?? options.model,
395
+ $ai_model_parameters: options.modelParameters ?? {},
396
+ $ai_input: withPrivacyMode(client, privacyMode, safeInput),
397
+ $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
396
398
  $ai_http_status: httpStatus,
397
399
  $ai_input_tokens: usage.inputTokens ?? 0,
398
400
  ...(usage.outputTokens !== undefined ? {
399
401
  $ai_output_tokens: usage.outputTokens
400
402
  } : {}),
401
403
  ...additionalTokenValues,
402
- $ai_latency: latency,
403
- ...(timeToFirstToken !== undefined ? {
404
- $ai_time_to_first_token: timeToFirstToken
404
+ $ai_latency: options.latency ?? 0,
405
+ ...(options.timeToFirstToken !== undefined ? {
406
+ $ai_time_to_first_token: options.timeToFirstToken
405
407
  } : {}),
406
408
  $ai_trace_id: traceId,
407
- $ai_base_url: baseURL,
408
- ...params.posthogProperties,
409
- $ai_tokens_source: getTokensSource(params.posthogProperties),
410
- ...(distinctId ? {} : {
409
+ $ai_base_url: options.baseURL ?? '',
410
+ ...options.properties,
411
+ $ai_tokens_source: getTokensSource(options.properties),
412
+ ...(options.distinctId ? {} : {
411
413
  $process_person_profile: false
412
414
  }),
413
- ...(stopReason ? {
414
- $ai_stop_reason: stopReason
415
+ ...(options.stopReason ? {
416
+ $ai_stop_reason: options.stopReason
415
417
  } : {}),
416
- ...(tools ? {
417
- $ai_tools: tools
418
+ ...(options.tools ? {
419
+ $ai_tools: options.tools
418
420
  } : {}),
419
421
  ...errorData,
420
422
  ...costOverrideData
421
423
  };
422
424
  const event = {
423
- distinctId: distinctId ?? traceId,
425
+ distinctId: options.distinctId ?? traceId,
424
426
  event: eventType,
425
427
  properties,
426
- groups: params.posthogGroups
428
+ groups: options.groups
427
429
  };
428
- if (captureImmediate) {
429
- // await capture promise to send single event in serverless environments
430
+ if (options.captureImmediate) {
430
431
  await client.captureImmediate(event);
431
432
  } else {
432
433
  client.capture(event);
433
434
  }
434
- return Promise.resolve();
435
435
  };
436
436
 
437
437
  class PostHogGoogleGenAI {
@@ -462,8 +462,7 @@ class WrappedModels {
462
462
  const availableTools = extractAvailableToolCalls('gemini', geminiParams);
463
463
  const metadata = response.usageMetadata;
464
464
  const finishReason = response.candidates?.[0]?.finishReason;
465
- await sendEventToPosthog({
466
- client: this.phClient,
465
+ await captureAiGeneration(this.phClient, {
467
466
  ...posthogParams,
468
467
  model: geminiParams.model,
469
468
  provider: 'gemini',
@@ -471,7 +470,7 @@ class WrappedModels {
471
470
  output: formatResponseGemini(response),
472
471
  latency,
473
472
  baseURL: 'https://generativelanguage.googleapis.com',
474
- params: params,
473
+ modelParameters: getModelParams(params),
475
474
  httpStatus: 200,
476
475
  usage: {
477
476
  inputTokens: metadata?.promptTokenCount ?? 0,
@@ -487,8 +486,7 @@ class WrappedModels {
487
486
  return response;
488
487
  } catch (error) {
489
488
  const latency = (Date.now() - startTime) / 1000;
490
- const enrichedError = await sendEventWithErrorToPosthog({
491
- client: this.phClient,
489
+ await captureAiGeneration(this.phClient, {
492
490
  ...posthogParams,
493
491
  model: geminiParams.model,
494
492
  provider: 'gemini',
@@ -496,14 +494,14 @@ class WrappedModels {
496
494
  output: [],
497
495
  latency,
498
496
  baseURL: 'https://generativelanguage.googleapis.com',
499
- params: params,
497
+ modelParameters: getModelParams(params),
500
498
  usage: {
501
499
  inputTokens: 0,
502
500
  outputTokens: 0
503
501
  },
504
- error: error
502
+ error
505
503
  });
506
- throw enrichedError;
504
+ throw error;
507
505
  }
508
506
  }
509
507
  async *generateContentStream(params) {
@@ -607,8 +605,7 @@ class WrappedModels {
607
605
  role: 'assistant',
608
606
  content: accumulatedContent
609
607
  }] : [];
610
- await sendEventToPosthog({
611
- client: this.phClient,
608
+ await captureAiGeneration(this.phClient, {
612
609
  ...posthogParams,
613
610
  model: geminiParams.model,
614
611
  provider: 'gemini',
@@ -617,7 +614,7 @@ class WrappedModels {
617
614
  latency,
618
615
  timeToFirstToken,
619
616
  baseURL: 'https://generativelanguage.googleapis.com',
620
- params: params,
617
+ modelParameters: getModelParams(params),
621
618
  httpStatus: 200,
622
619
  usage: {
623
620
  ...usage,
@@ -629,8 +626,7 @@ class WrappedModels {
629
626
  });
630
627
  } catch (error) {
631
628
  const latency = (Date.now() - startTime) / 1000;
632
- const enrichedError = await sendEventWithErrorToPosthog({
633
- client: this.phClient,
629
+ await captureAiGeneration(this.phClient, {
634
630
  ...posthogParams,
635
631
  model: geminiParams.model,
636
632
  provider: 'gemini',
@@ -638,14 +634,14 @@ class WrappedModels {
638
634
  output: [],
639
635
  latency,
640
636
  baseURL: 'https://generativelanguage.googleapis.com',
641
- params: params,
637
+ modelParameters: getModelParams(params),
642
638
  usage: {
643
639
  inputTokens: 0,
644
640
  outputTokens: 0
645
641
  },
646
- error: error
642
+ error
647
643
  });
648
- throw enrichedError;
644
+ throw error;
649
645
  }
650
646
  }
651
647
  async embedContent(params) {
@@ -658,8 +654,7 @@ class WrappedModels {
658
654
  const response = await this.client.models.embedContent(geminiParams);
659
655
  const latency = (Date.now() - startTime) / 1000;
660
656
  const inputTokens = extractEmbeddingTokenCount(response);
661
- await sendEventToPosthog({
662
- client: this.phClient,
657
+ await captureAiGeneration(this.phClient, {
663
658
  ...posthogParams,
664
659
  eventType: AIEvent.Embedding,
665
660
  model: geminiParams.model,
@@ -668,7 +663,7 @@ class WrappedModels {
668
663
  output: null,
669
664
  latency,
670
665
  baseURL: 'https://generativelanguage.googleapis.com',
671
- params: params,
666
+ modelParameters: getModelParams(params),
672
667
  httpStatus: 200,
673
668
  usage: {
674
669
  inputTokens
@@ -677,8 +672,7 @@ class WrappedModels {
677
672
  return response;
678
673
  } catch (error) {
679
674
  const latency = (Date.now() - startTime) / 1000;
680
- const enrichedError = await sendEventWithErrorToPosthog({
681
- client: this.phClient,
675
+ await captureAiGeneration(this.phClient, {
682
676
  ...posthogParams,
683
677
  eventType: AIEvent.Embedding,
684
678
  model: geminiParams.model,
@@ -687,13 +681,13 @@ class WrappedModels {
687
681
  output: null,
688
682
  latency,
689
683
  baseURL: 'https://generativelanguage.googleapis.com',
690
- params: params,
684
+ modelParameters: getModelParams(params),
691
685
  usage: {
692
686
  inputTokens: 0
693
687
  },
694
- error: error
688
+ error
695
689
  });
696
- throw enrichedError;
690
+ throw error;
697
691
  }
698
692
  }
699
693
  formatPartsAsContentBlocks(parts) {