@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 AnthropicOriginal from '@anthropic-ai/sdk';
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 isObject = value => {
@@ -78,6 +76,14 @@ function getTokensSource(posthogProperties) {
78
76
  }
79
77
  return 'sdk';
80
78
  }
79
+ const STRING_FORMAT = 'utf8';
80
+
81
+ // Reused across calls to avoid per-invocation allocation; truncate() runs
82
+ // hundreds of times for prompts with many parts.
83
+ new TextEncoder();
84
+ new TextDecoder(STRING_FORMAT, {
85
+ fatal: false
86
+ });
81
87
  const getModelParams = params => {
82
88
  if (!params) {
83
89
  return {};
@@ -203,73 +209,69 @@ function addDefaults(params) {
203
209
  traceId: params.traceId ?? v4()
204
210
  };
205
211
  }
206
- const sendEventWithErrorToPosthog = async ({
207
- client,
208
- traceId,
209
- error,
210
- ...args
211
- }) => {
212
- const httpStatus = error && typeof error === 'object' && 'status' in error ? error.status ?? 500 : 500;
213
- const properties = {
214
- client,
215
- traceId,
216
- httpStatus,
217
- error: JSON.stringify(error),
218
- ...args
219
- };
220
- const enrichedError = error;
221
- if (client.options?.enableExceptionAutocapture) {
222
- // assign a uuid that can be used to link the trace and exception events
223
- const exceptionId = uuidv7();
224
- client.captureException(error, undefined, {
225
- $ai_trace_id: traceId
226
- }, exceptionId);
227
- enrichedError.__posthog_previously_captured_error = true;
228
- properties.exceptionId = exceptionId;
229
- }
230
- await sendEventToPosthog(properties);
231
- return enrichedError;
232
- };
233
- const sendEventToPosthog = async ({
234
- client,
235
- eventType = AIEvent.Generation,
236
- distinctId,
237
- traceId,
238
- model,
239
- provider,
240
- input,
241
- output,
242
- latency,
243
- timeToFirstToken,
244
- baseURL,
245
- params,
246
- httpStatus = 200,
247
- usage = {},
248
- error,
249
- exceptionId,
250
- stopReason,
251
- tools,
252
- captureImmediate = false
253
- }) => {
212
+
213
+ var version = "7.17.1";
214
+
215
+ /**
216
+ * Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape
217
+ * directly so that any caller — first-party SDK wrappers and external code
218
+ * alike produces an identical event.
219
+ */
220
+
221
+ /**
222
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
223
+ *
224
+ * This is the canonical primitive that every `@posthog/ai` wrapper
225
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
226
+ * external code can use it directly to instrument LLM calls made through
227
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
228
+ * same events the SDK wrappers produce.
229
+ *
230
+ * When `error` is set, the event is captured as an error. If the error is an
231
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
232
+ * so callers can re-throw the original error reference safely.
233
+ */
234
+ const captureAiGeneration = async (client, options) => {
254
235
  if (!client.capture) {
255
- return Promise.resolve();
236
+ return;
256
237
  }
257
- // sanitize input and output for UTF-8 validity
258
- const safeInput = sanitizeValues(input);
259
- const safeOutput = sanitizeValues(output);
260
- const safeError = sanitizeValues(error);
238
+ const traceId = options.traceId ?? v4();
239
+ const eventType = options.eventType ?? AIEvent.Generation;
240
+ const privacyMode = options.privacyMode ?? false;
241
+ const usage = options.usage ?? {};
242
+ const safeInput = sanitizeValues(options.input);
243
+ const safeOutput = sanitizeValues(options.output);
244
+ let httpStatus = options.httpStatus;
261
245
  let errorData = {};
262
- if (error) {
246
+ if (options.error) {
247
+ if (httpStatus === undefined) {
248
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
249
+ httpStatus = options.error.status;
250
+ } else {
251
+ httpStatus = 500;
252
+ }
253
+ }
254
+ let exceptionId;
255
+ if (client.options?.enableExceptionAutocapture) {
256
+ exceptionId = uuidv7();
257
+ client.captureException(options.error, undefined, {
258
+ $ai_trace_id: traceId
259
+ }, exceptionId);
260
+ if (typeof options.error === 'object') {
261
+ options.error.__posthog_previously_captured_error = true;
262
+ }
263
+ }
263
264
  errorData = {
264
265
  $ai_is_error: true,
265
- $ai_error: safeError,
266
+ $ai_error: sanitizeValues(JSON.stringify(options.error)),
266
267
  $exception_event_id: exceptionId
267
268
  };
268
269
  }
270
+ httpStatus = httpStatus ?? 200;
269
271
  let costOverrideData = {};
270
- if (params.posthogCostOverride) {
271
- const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
272
- const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
272
+ if (options.costOverride) {
273
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
274
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
273
275
  costOverrideData = {
274
276
  $ai_input_cost_usd: inputCostUSD,
275
277
  $ai_output_cost_usd: outputCostUSD,
@@ -296,50 +298,48 @@ const sendEventToPosthog = async ({
296
298
  const properties = {
297
299
  $ai_lib: 'posthog-ai',
298
300
  $ai_lib_version: version,
299
- $ai_provider: params.posthogProviderOverride ?? provider,
300
- $ai_model: params.posthogModelOverride ?? model,
301
- $ai_model_parameters: getModelParams(params),
302
- $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
303
- $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
301
+ $ai_provider: options.providerOverride ?? options.provider,
302
+ $ai_model: options.modelOverride ?? options.model,
303
+ $ai_model_parameters: options.modelParameters ?? {},
304
+ $ai_input: withPrivacyMode(client, privacyMode, safeInput),
305
+ $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
304
306
  $ai_http_status: httpStatus,
305
307
  $ai_input_tokens: usage.inputTokens ?? 0,
306
308
  ...(usage.outputTokens !== undefined ? {
307
309
  $ai_output_tokens: usage.outputTokens
308
310
  } : {}),
309
311
  ...additionalTokenValues,
310
- $ai_latency: latency,
311
- ...(timeToFirstToken !== undefined ? {
312
- $ai_time_to_first_token: timeToFirstToken
312
+ $ai_latency: options.latency ?? 0,
313
+ ...(options.timeToFirstToken !== undefined ? {
314
+ $ai_time_to_first_token: options.timeToFirstToken
313
315
  } : {}),
314
316
  $ai_trace_id: traceId,
315
- $ai_base_url: baseURL,
316
- ...params.posthogProperties,
317
- $ai_tokens_source: getTokensSource(params.posthogProperties),
318
- ...(distinctId ? {} : {
317
+ $ai_base_url: options.baseURL ?? '',
318
+ ...options.properties,
319
+ $ai_tokens_source: getTokensSource(options.properties),
320
+ ...(options.distinctId ? {} : {
319
321
  $process_person_profile: false
320
322
  }),
321
- ...(stopReason ? {
322
- $ai_stop_reason: stopReason
323
+ ...(options.stopReason ? {
324
+ $ai_stop_reason: options.stopReason
323
325
  } : {}),
324
- ...(tools ? {
325
- $ai_tools: tools
326
+ ...(options.tools ? {
327
+ $ai_tools: options.tools
326
328
  } : {}),
327
329
  ...errorData,
328
330
  ...costOverrideData
329
331
  };
330
332
  const event = {
331
- distinctId: distinctId ?? traceId,
333
+ distinctId: options.distinctId ?? traceId,
332
334
  event: eventType,
333
335
  properties,
334
- groups: params.posthogGroups
336
+ groups: options.groups
335
337
  };
336
- if (captureImmediate) {
337
- // await capture promise to send single event in serverless environments
338
+ if (options.captureImmediate) {
338
339
  await client.captureImmediate(event);
339
340
  } else {
340
341
  client.capture(event);
341
342
  }
342
- return Promise.resolve();
343
343
  };
344
344
 
345
345
  class PostHogAnthropic extends AnthropicOriginal {
@@ -501,8 +501,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
501
501
  text: accumulatedContent
502
502
  }]
503
503
  }];
504
- await sendEventToPosthog({
505
- client: this.phClient,
504
+ await captureAiGeneration(this.phClient, {
506
505
  ...posthogParams,
507
506
  model: anthropicParams.model,
508
507
  provider: 'anthropic',
@@ -511,15 +510,14 @@ class WrappedMessages extends AnthropicOriginal.Messages {
511
510
  latency,
512
511
  timeToFirstToken,
513
512
  baseURL: this.baseURL,
514
- params: body,
513
+ modelParameters: getModelParams(body),
515
514
  httpStatus: 200,
516
515
  usage,
517
516
  stopReason,
518
517
  tools: availableTools
519
518
  });
520
519
  } catch (error) {
521
- const enrichedError = await sendEventWithErrorToPosthog({
522
- client: this.phClient,
520
+ await captureAiGeneration(this.phClient, {
523
521
  ...posthogParams,
524
522
  model: anthropicParams.model,
525
523
  provider: 'anthropic',
@@ -527,14 +525,14 @@ class WrappedMessages extends AnthropicOriginal.Messages {
527
525
  output: [],
528
526
  latency: 0,
529
527
  baseURL: this.baseURL,
530
- params: body,
528
+ modelParameters: getModelParams(body),
531
529
  usage: {
532
530
  inputTokens: 0,
533
531
  outputTokens: 0
534
532
  },
535
533
  error: error
536
534
  });
537
- throw enrichedError;
535
+ throw error;
538
536
  }
539
537
  })();
540
538
 
@@ -548,8 +546,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
548
546
  if ('content' in result) {
549
547
  const latency = (Date.now() - startTime) / 1000;
550
548
  const availableTools = extractAvailableToolCalls('anthropic', anthropicParams);
551
- await sendEventToPosthog({
552
- client: this.phClient,
549
+ await captureAiGeneration(this.phClient, {
553
550
  ...posthogParams,
554
551
  model: anthropicParams.model,
555
552
  provider: 'anthropic',
@@ -557,7 +554,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
557
554
  output: formatResponseAnthropic(result),
558
555
  latency,
559
556
  baseURL: this.baseURL,
560
- params: body,
557
+ modelParameters: getModelParams(body),
561
558
  httpStatus: 200,
562
559
  usage: {
563
560
  inputTokens: result.usage.input_tokens ?? 0,
@@ -573,8 +570,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
573
570
  }
574
571
  return result;
575
572
  }, async error => {
576
- await sendEventToPosthog({
577
- client: this.phClient,
573
+ await captureAiGeneration(this.phClient, {
578
574
  ...posthogParams,
579
575
  model: anthropicParams.model,
580
576
  provider: 'anthropic',
@@ -582,13 +578,13 @@ class WrappedMessages extends AnthropicOriginal.Messages {
582
578
  output: [],
583
579
  latency: 0,
584
580
  baseURL: this.baseURL,
585
- params: body,
581
+ modelParameters: getModelParams(body),
586
582
  httpStatus: error?.status ? error.status : 500,
587
583
  usage: {
588
584
  inputTokens: 0,
589
585
  outputTokens: 0
590
586
  },
591
- error: JSON.stringify(error)
587
+ error: error
592
588
  });
593
589
  throw error;
594
590
  });