@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.
@@ -3,8 +3,6 @@
3
3
  var uuid = require('uuid');
4
4
  var core = require('@posthog/core');
5
5
 
6
- var version = "7.16.15";
7
-
8
6
  // Type guards for safer type checking
9
7
 
10
8
  const isString = value => {
@@ -76,6 +74,14 @@ function getTokensSource(posthogProperties) {
76
74
  const MAX_OUTPUT_SIZE = 200000;
77
75
  const STRING_FORMAT = 'utf8';
78
76
 
77
+ // Reused across calls to avoid per-invocation allocation; truncate() runs
78
+ // hundreds of times for prompts with many parts.
79
+ const sharedTextEncoder = new TextEncoder();
80
+ const sharedTextDecoder = new TextDecoder(STRING_FORMAT, {
81
+ fatal: false
82
+ });
83
+ const utf8ByteLength = str => sharedTextEncoder.encode(str).byteLength;
84
+
79
85
  /**
80
86
  * Safely converts content to a string, preserving structure for objects/arrays.
81
87
  * - If content is already a string, returns it as-is
@@ -138,20 +144,16 @@ const truncate = input => {
138
144
  }
139
145
 
140
146
  // Check if we need to truncate and ensure STRING_FORMAT is respected
141
- const encoder = new TextEncoder();
142
- const buffer = encoder.encode(str);
147
+ const buffer = sharedTextEncoder.encode(str);
143
148
  if (buffer.length <= MAX_OUTPUT_SIZE) {
144
149
  // Ensure STRING_FORMAT is respected
145
- return new TextDecoder(STRING_FORMAT).decode(buffer);
150
+ return sharedTextDecoder.decode(buffer);
146
151
  }
147
152
 
148
- // Truncate the buffer and ensure a valid string is returned
153
+ // Truncate the buffer and ensure a valid string is returned.
154
+ // fatal: false means we get U+FFFD at the end if truncation broke the encoding.
149
155
  const truncatedBuffer = buffer.slice(0, MAX_OUTPUT_SIZE);
150
- // fatal: false means we get U+FFFD at the end if truncation broke the encoding
151
- const decoder = new TextDecoder(STRING_FORMAT, {
152
- fatal: false
153
- });
154
- let truncatedStr = decoder.decode(truncatedBuffer);
156
+ let truncatedStr = sharedTextDecoder.decode(truncatedBuffer);
155
157
  if (truncatedStr.endsWith('\uFFFD')) {
156
158
  truncatedStr = truncatedStr.slice(0, -1);
157
159
  }
@@ -294,73 +296,69 @@ function sanitizeValues(obj) {
294
296
  }
295
297
  return jsonSafe;
296
298
  }
297
- const sendEventWithErrorToPosthog = async ({
298
- client,
299
- traceId,
300
- error,
301
- ...args
302
- }) => {
303
- const httpStatus = error && typeof error === 'object' && 'status' in error ? error.status ?? 500 : 500;
304
- const properties = {
305
- client,
306
- traceId,
307
- httpStatus,
308
- error: JSON.stringify(error),
309
- ...args
310
- };
311
- const enrichedError = error;
312
- if (client.options?.enableExceptionAutocapture) {
313
- // assign a uuid that can be used to link the trace and exception events
314
- const exceptionId = core.uuidv7();
315
- client.captureException(error, undefined, {
316
- $ai_trace_id: traceId
317
- }, exceptionId);
318
- enrichedError.__posthog_previously_captured_error = true;
319
- properties.exceptionId = exceptionId;
320
- }
321
- await sendEventToPosthog(properties);
322
- return enrichedError;
323
- };
324
- const sendEventToPosthog = async ({
325
- client,
326
- eventType = AIEvent.Generation,
327
- distinctId,
328
- traceId,
329
- model,
330
- provider,
331
- input,
332
- output,
333
- latency,
334
- timeToFirstToken,
335
- baseURL,
336
- params,
337
- httpStatus = 200,
338
- usage = {},
339
- error,
340
- exceptionId,
341
- stopReason,
342
- tools,
343
- captureImmediate = false
344
- }) => {
299
+
300
+ var version = "7.17.1";
301
+
302
+ /**
303
+ * Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape
304
+ * directly so that any caller — first-party SDK wrappers and external code
305
+ * alike produces an identical event.
306
+ */
307
+
308
+ /**
309
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
310
+ *
311
+ * This is the canonical primitive that every `@posthog/ai` wrapper
312
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
313
+ * external code can use it directly to instrument LLM calls made through
314
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
315
+ * same events the SDK wrappers produce.
316
+ *
317
+ * When `error` is set, the event is captured as an error. If the error is an
318
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
319
+ * so callers can re-throw the original error reference safely.
320
+ */
321
+ const captureAiGeneration = async (client, options) => {
345
322
  if (!client.capture) {
346
- return Promise.resolve();
323
+ return;
347
324
  }
348
- // sanitize input and output for UTF-8 validity
349
- const safeInput = sanitizeValues(input);
350
- const safeOutput = sanitizeValues(output);
351
- const safeError = sanitizeValues(error);
325
+ const traceId = options.traceId ?? uuid.v4();
326
+ const eventType = options.eventType ?? AIEvent.Generation;
327
+ const privacyMode = options.privacyMode ?? false;
328
+ const usage = options.usage ?? {};
329
+ const safeInput = sanitizeValues(options.input);
330
+ const safeOutput = sanitizeValues(options.output);
331
+ let httpStatus = options.httpStatus;
352
332
  let errorData = {};
353
- if (error) {
333
+ if (options.error) {
334
+ if (httpStatus === undefined) {
335
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
336
+ httpStatus = options.error.status;
337
+ } else {
338
+ httpStatus = 500;
339
+ }
340
+ }
341
+ let exceptionId;
342
+ if (client.options?.enableExceptionAutocapture) {
343
+ exceptionId = core.uuidv7();
344
+ client.captureException(options.error, undefined, {
345
+ $ai_trace_id: traceId
346
+ }, exceptionId);
347
+ if (typeof options.error === 'object') {
348
+ options.error.__posthog_previously_captured_error = true;
349
+ }
350
+ }
354
351
  errorData = {
355
352
  $ai_is_error: true,
356
- $ai_error: safeError,
353
+ $ai_error: sanitizeValues(JSON.stringify(options.error)),
357
354
  $exception_event_id: exceptionId
358
355
  };
359
356
  }
357
+ httpStatus = httpStatus ?? 200;
360
358
  let costOverrideData = {};
361
- if (params.posthogCostOverride) {
362
- const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
363
- const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
359
+ if (options.costOverride) {
360
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
361
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
364
362
  costOverrideData = {
365
363
  $ai_input_cost_usd: inputCostUSD,
366
364
  $ai_output_cost_usd: outputCostUSD,
@@ -387,50 +385,48 @@ const sendEventToPosthog = async ({
387
385
  const properties = {
388
386
  $ai_lib: 'posthog-ai',
389
387
  $ai_lib_version: version,
390
- $ai_provider: params.posthogProviderOverride ?? provider,
391
- $ai_model: params.posthogModelOverride ?? model,
392
- $ai_model_parameters: getModelParams(params),
393
- $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
394
- $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
388
+ $ai_provider: options.providerOverride ?? options.provider,
389
+ $ai_model: options.modelOverride ?? options.model,
390
+ $ai_model_parameters: options.modelParameters ?? {},
391
+ $ai_input: withPrivacyMode(client, privacyMode, safeInput),
392
+ $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
395
393
  $ai_http_status: httpStatus,
396
394
  $ai_input_tokens: usage.inputTokens ?? 0,
397
395
  ...(usage.outputTokens !== undefined ? {
398
396
  $ai_output_tokens: usage.outputTokens
399
397
  } : {}),
400
398
  ...additionalTokenValues,
401
- $ai_latency: latency,
402
- ...(timeToFirstToken !== undefined ? {
403
- $ai_time_to_first_token: timeToFirstToken
399
+ $ai_latency: options.latency ?? 0,
400
+ ...(options.timeToFirstToken !== undefined ? {
401
+ $ai_time_to_first_token: options.timeToFirstToken
404
402
  } : {}),
405
403
  $ai_trace_id: traceId,
406
- $ai_base_url: baseURL,
407
- ...params.posthogProperties,
408
- $ai_tokens_source: getTokensSource(params.posthogProperties),
409
- ...(distinctId ? {} : {
404
+ $ai_base_url: options.baseURL ?? '',
405
+ ...options.properties,
406
+ $ai_tokens_source: getTokensSource(options.properties),
407
+ ...(options.distinctId ? {} : {
410
408
  $process_person_profile: false
411
409
  }),
412
- ...(stopReason ? {
413
- $ai_stop_reason: stopReason
410
+ ...(options.stopReason ? {
411
+ $ai_stop_reason: options.stopReason
414
412
  } : {}),
415
- ...(tools ? {
416
- $ai_tools: tools
413
+ ...(options.tools ? {
414
+ $ai_tools: options.tools
417
415
  } : {}),
418
416
  ...errorData,
419
417
  ...costOverrideData
420
418
  };
421
419
  const event = {
422
- distinctId: distinctId ?? traceId,
420
+ distinctId: options.distinctId ?? traceId,
423
421
  event: eventType,
424
422
  properties,
425
- groups: params.posthogGroups
423
+ groups: options.groups
426
424
  };
427
- if (captureImmediate) {
428
- // await capture promise to send single event in serverless environments
425
+ if (options.captureImmediate) {
429
426
  await client.captureImmediate(event);
430
427
  } else {
431
428
  client.capture(event);
432
429
  }
433
- return Promise.resolve();
434
430
  };
435
431
 
436
432
  // Union types for dual version support
@@ -530,18 +526,26 @@ const mapVercelPrompt = messages => {
530
526
  };
531
527
  });
532
528
  try {
533
- // Trim the inputs array until its JSON size fits within MAX_OUTPUT_SIZE
534
- const encoder = new TextEncoder();
535
- let serialized = JSON.stringify(inputs);
529
+ // Trim the inputs array until its serialized JSON size fits within MAX_OUTPUT_SIZE.
530
+ // Pre-compute each message's byte size once so we can shift by accumulated budget
531
+ // in a single linear pass, instead of re-stringifying the whole array per iteration.
532
+ const messageSizes = inputs.map(m => utf8ByteLength(JSON.stringify(m)));
533
+ // Account for the surrounding `[` `]` plus a comma between each pair of elements.
534
+ let totalBytes = 2 + Math.max(0, messageSizes.length - 1);
535
+ for (const size of messageSizes) {
536
+ totalBytes += size;
537
+ }
536
538
  let removedCount = 0;
537
- // We need to keep track of the initial size of the inputs array because we're going to be mutating it
538
- const initialSize = inputs.length;
539
- for (let i = 0; i < initialSize && encoder.encode(serialized).byteLength > MAX_OUTPUT_SIZE; i++) {
540
- inputs.shift();
539
+ while (totalBytes > MAX_OUTPUT_SIZE && removedCount < messageSizes.length) {
540
+ totalBytes -= messageSizes[removedCount];
541
+ // Each removed message past the first also drops the comma that joined it.
542
+ if (removedCount < messageSizes.length - 1) {
543
+ totalBytes -= 1;
544
+ }
541
545
  removedCount++;
542
- serialized = JSON.stringify(inputs);
543
546
  }
544
547
  if (removedCount > 0) {
548
+ inputs.splice(0, removedCount);
545
549
  // Add one placeholder to indicate how many were removed
546
550
  inputs.unshift({
547
551
  role: 'posthog',
@@ -772,6 +776,19 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
772
776
  }
773
777
  };
774
778
 
779
+ // Shared `captureAiGeneration` options for every call site in this wrapper.
780
+ const baseOptions = {
781
+ distinctId: mergedOptions.posthogDistinctId,
782
+ traceId,
783
+ properties: mergedOptions.posthogProperties,
784
+ groups: mergedOptions.posthogGroups,
785
+ privacyMode: mergedOptions.posthogPrivacyMode,
786
+ modelOverride: mergedOptions.posthogModelOverride,
787
+ providerOverride: mergedOptions.posthogProviderOverride,
788
+ costOverride: mergedOptions.posthogCostOverride,
789
+ captureImmediate: mergedOptions.posthogCaptureImmediate
790
+ };
791
+
775
792
  // Create wrapped model using Object.create to preserve the prototype chain
776
793
  // This automatically inherits all properties (including getters) from the model
777
794
  const wrappedModel = Object.create(model, {
@@ -828,46 +845,40 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
828
845
  // Extract finish reason - V2 returns a string, V3 returns an object with .unified
829
846
  const rawFinishReason = result.finishReason;
830
847
  const finishReasonStr = typeof rawFinishReason === 'string' ? rawFinishReason : rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason ? String(rawFinishReason.unified) : undefined;
831
- await sendEventToPosthog({
832
- client: phClient,
833
- distinctId: mergedOptions.posthogDistinctId,
834
- traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
848
+ await captureAiGeneration(phClient, {
849
+ ...baseOptions,
835
850
  model: modelId,
836
851
  provider: provider,
837
852
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
838
853
  output: content,
839
854
  latency,
840
855
  baseURL,
841
- params: mergedParams,
856
+ modelParameters: getModelParams(mergedParams),
842
857
  httpStatus: 200,
843
858
  usage,
844
859
  stopReason: finishReasonStr,
845
- tools: availableTools,
846
- captureImmediate: mergedOptions.posthogCaptureImmediate
860
+ tools: availableTools
847
861
  });
848
862
  return result;
849
863
  } catch (error) {
850
864
  const modelId = model.modelId;
851
- const enrichedError = await sendEventWithErrorToPosthog({
852
- client: phClient,
853
- distinctId: mergedOptions.posthogDistinctId,
854
- traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
865
+ await captureAiGeneration(phClient, {
866
+ ...baseOptions,
855
867
  model: modelId,
856
868
  provider: model.provider,
857
869
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
858
870
  output: [],
859
871
  latency: 0,
860
872
  baseURL: '',
861
- params: mergedParams,
873
+ modelParameters: getModelParams(mergedParams),
862
874
  usage: {
863
875
  inputTokens: 0,
864
876
  outputTokens: 0
865
877
  },
866
878
  error: error,
867
- tools: availableTools,
868
- captureImmediate: mergedOptions.posthogCaptureImmediate
879
+ tools: availableTools
869
880
  });
870
- throw enrichedError;
881
+ throw error;
871
882
  }
872
883
  },
873
884
  writable: true,
@@ -1019,10 +1030,8 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1019
1030
  }
1020
1031
  };
1021
1032
  adjustAnthropicV3CacheTokens(model, modelId, provider, finalUsage);
1022
- await sendEventToPosthog({
1023
- client: phClient,
1024
- distinctId: mergedOptions.posthogDistinctId,
1025
- traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
1033
+ await captureAiGeneration(phClient, {
1034
+ ...baseOptions,
1026
1035
  model: modelId,
1027
1036
  provider: provider,
1028
1037
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
@@ -1030,12 +1039,11 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1030
1039
  latency,
1031
1040
  timeToFirstToken,
1032
1041
  baseURL,
1033
- params: mergedParams,
1042
+ modelParameters: getModelParams(mergedParams),
1034
1043
  httpStatus: 200,
1035
1044
  usage: finalUsage,
1036
1045
  stopReason,
1037
- tools: availableTools,
1038
- captureImmediate: mergedOptions.posthogCaptureImmediate
1046
+ tools: availableTools
1039
1047
  });
1040
1048
  }
1041
1049
  });
@@ -1044,26 +1052,23 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1044
1052
  ...rest
1045
1053
  };
1046
1054
  } catch (error) {
1047
- const enrichedError = await sendEventWithErrorToPosthog({
1048
- client: phClient,
1049
- distinctId: mergedOptions.posthogDistinctId,
1050
- traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
1055
+ await captureAiGeneration(phClient, {
1056
+ ...baseOptions,
1051
1057
  model: modelId,
1052
1058
  provider: provider,
1053
1059
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1054
1060
  output: [],
1055
1061
  latency: 0,
1056
1062
  baseURL: '',
1057
- params: mergedParams,
1063
+ modelParameters: getModelParams(mergedParams),
1058
1064
  usage: {
1059
1065
  inputTokens: 0,
1060
1066
  outputTokens: 0
1061
1067
  },
1062
1068
  error: error,
1063
- tools: availableTools,
1064
- captureImmediate: mergedOptions.posthogCaptureImmediate
1069
+ tools: availableTools
1065
1070
  });
1066
- throw enrichedError;
1071
+ throw error;
1067
1072
  }
1068
1073
  },
1069
1074
  writable: true,