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