@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.
package/README.md CHANGED
@@ -39,6 +39,37 @@ console.log(completion.choices[0].message.content)
39
39
  await phClient.shutdown()
40
40
  ```
41
41
 
42
+ ## Custom and unsupported providers
43
+
44
+ For LLM calls that don't go through one of the wrapped clients — direct Cloudflare Workers AI bindings, TanStack AI adapters, custom HTTP clients — use `captureAiGeneration` to emit the same `$ai_generation` events the wrappers produce.
45
+
46
+ ```typescript
47
+ import { captureAiGeneration } from '@posthog/ai'
48
+ import { PostHog } from 'posthog-node'
49
+
50
+ const phClient = new PostHog('<YOUR_PROJECT_API_KEY>', { host: 'https://us.i.posthog.com' })
51
+
52
+ const start = Date.now()
53
+ const result = await env.AI.run('@cf/zai-org/glm-4.7-flash', { messages, reasoning_effort: 'high' })
54
+
55
+ await captureAiGeneration(phClient, {
56
+ distinctId: 'user_123',
57
+ traceId: 'trace_abc',
58
+ provider: 'cloudflare-workers-ai',
59
+ model: '@cf/zai-org/glm-4.7-flash',
60
+ input: messages,
61
+ output: result.response,
62
+ modelParameters: { reasoning_effort: 'high' },
63
+ usage: { inputTokens: result.usage?.prompt_tokens, outputTokens: result.usage?.completion_tokens },
64
+ latency: (Date.now() - start) / 1000,
65
+ properties: { feature: 'transcript-toc' },
66
+ })
67
+
68
+ await phClient.shutdown()
69
+ ```
70
+
71
+ `captureAiGeneration` is the same primitive that every other `@posthog/ai` wrapper funnels through, so the resulting events are indistinguishable from those produced by `withTracing`, `OpenAI`, `Anthropic`, etc.
72
+
42
73
  ## OpenTelemetry
43
74
 
44
75
  `@posthog/ai/otel` provides two ways to send AI traces to PostHog via OpenTelemetry. Both automatically filter to AI-related spans only (`gen_ai.*`, `llm.*`, `ai.*`, `traceloop.*`) and PostHog converts them into `$ai_generation` events server-side. This works with any LLM provider SDK that supports OpenTelemetry.
@@ -10,8 +10,6 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
 
11
11
  var AnthropicOriginal__default = /*#__PURE__*/_interopDefault(AnthropicOriginal);
12
12
 
13
- var version = "7.16.15";
14
-
15
13
  // Type guards for safer type checking
16
14
 
17
15
  const isObject = value => {
@@ -86,6 +84,14 @@ function getTokensSource(posthogProperties) {
86
84
  }
87
85
  return 'sdk';
88
86
  }
87
+ const STRING_FORMAT = 'utf8';
88
+
89
+ // Reused across calls to avoid per-invocation allocation; truncate() runs
90
+ // hundreds of times for prompts with many parts.
91
+ new TextEncoder();
92
+ new TextDecoder(STRING_FORMAT, {
93
+ fatal: false
94
+ });
89
95
  const getModelParams = params => {
90
96
  if (!params) {
91
97
  return {};
@@ -211,73 +217,69 @@ function addDefaults(params) {
211
217
  traceId: params.traceId ?? uuid.v4()
212
218
  };
213
219
  }
214
- const sendEventWithErrorToPosthog = async ({
215
- client,
216
- traceId,
217
- error,
218
- ...args
219
- }) => {
220
- const httpStatus = error && typeof error === 'object' && 'status' in error ? error.status ?? 500 : 500;
221
- const properties = {
222
- client,
223
- traceId,
224
- httpStatus,
225
- error: JSON.stringify(error),
226
- ...args
227
- };
228
- const enrichedError = error;
229
- if (client.options?.enableExceptionAutocapture) {
230
- // assign a uuid that can be used to link the trace and exception events
231
- const exceptionId = core.uuidv7();
232
- client.captureException(error, undefined, {
233
- $ai_trace_id: traceId
234
- }, exceptionId);
235
- enrichedError.__posthog_previously_captured_error = true;
236
- properties.exceptionId = exceptionId;
237
- }
238
- await sendEventToPosthog(properties);
239
- return enrichedError;
240
- };
241
- const sendEventToPosthog = async ({
242
- client,
243
- eventType = AIEvent.Generation,
244
- distinctId,
245
- traceId,
246
- model,
247
- provider,
248
- input,
249
- output,
250
- latency,
251
- timeToFirstToken,
252
- baseURL,
253
- params,
254
- httpStatus = 200,
255
- usage = {},
256
- error,
257
- exceptionId,
258
- stopReason,
259
- tools,
260
- captureImmediate = false
261
- }) => {
220
+
221
+ var version = "7.17.1";
222
+
223
+ /**
224
+ * Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape
225
+ * directly so that any caller — first-party SDK wrappers and external code
226
+ * alike produces an identical event.
227
+ */
228
+
229
+ /**
230
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
231
+ *
232
+ * This is the canonical primitive that every `@posthog/ai` wrapper
233
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
234
+ * external code can use it directly to instrument LLM calls made through
235
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
236
+ * same events the SDK wrappers produce.
237
+ *
238
+ * When `error` is set, the event is captured as an error. If the error is an
239
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
240
+ * so callers can re-throw the original error reference safely.
241
+ */
242
+ const captureAiGeneration = async (client, options) => {
262
243
  if (!client.capture) {
263
- return Promise.resolve();
244
+ return;
264
245
  }
265
- // sanitize input and output for UTF-8 validity
266
- const safeInput = sanitizeValues(input);
267
- const safeOutput = sanitizeValues(output);
268
- const safeError = sanitizeValues(error);
246
+ const traceId = options.traceId ?? uuid.v4();
247
+ const eventType = options.eventType ?? AIEvent.Generation;
248
+ const privacyMode = options.privacyMode ?? false;
249
+ const usage = options.usage ?? {};
250
+ const safeInput = sanitizeValues(options.input);
251
+ const safeOutput = sanitizeValues(options.output);
252
+ let httpStatus = options.httpStatus;
269
253
  let errorData = {};
270
- if (error) {
254
+ if (options.error) {
255
+ if (httpStatus === undefined) {
256
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
257
+ httpStatus = options.error.status;
258
+ } else {
259
+ httpStatus = 500;
260
+ }
261
+ }
262
+ let exceptionId;
263
+ if (client.options?.enableExceptionAutocapture) {
264
+ exceptionId = core.uuidv7();
265
+ client.captureException(options.error, undefined, {
266
+ $ai_trace_id: traceId
267
+ }, exceptionId);
268
+ if (typeof options.error === 'object') {
269
+ options.error.__posthog_previously_captured_error = true;
270
+ }
271
+ }
271
272
  errorData = {
272
273
  $ai_is_error: true,
273
- $ai_error: safeError,
274
+ $ai_error: sanitizeValues(JSON.stringify(options.error)),
274
275
  $exception_event_id: exceptionId
275
276
  };
276
277
  }
278
+ httpStatus = httpStatus ?? 200;
277
279
  let costOverrideData = {};
278
- if (params.posthogCostOverride) {
279
- const inputCostUSD = (params.posthogCostOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
280
- const outputCostUSD = (params.posthogCostOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
280
+ if (options.costOverride) {
281
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
282
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
281
283
  costOverrideData = {
282
284
  $ai_input_cost_usd: inputCostUSD,
283
285
  $ai_output_cost_usd: outputCostUSD,
@@ -304,50 +306,48 @@ const sendEventToPosthog = async ({
304
306
  const properties = {
305
307
  $ai_lib: 'posthog-ai',
306
308
  $ai_lib_version: version,
307
- $ai_provider: params.posthogProviderOverride ?? provider,
308
- $ai_model: params.posthogModelOverride ?? model,
309
- $ai_model_parameters: getModelParams(params),
310
- $ai_input: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeInput),
311
- $ai_output_choices: withPrivacyMode(client, params.posthogPrivacyMode ?? false, safeOutput),
309
+ $ai_provider: options.providerOverride ?? options.provider,
310
+ $ai_model: options.modelOverride ?? options.model,
311
+ $ai_model_parameters: options.modelParameters ?? {},
312
+ $ai_input: withPrivacyMode(client, privacyMode, safeInput),
313
+ $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
312
314
  $ai_http_status: httpStatus,
313
315
  $ai_input_tokens: usage.inputTokens ?? 0,
314
316
  ...(usage.outputTokens !== undefined ? {
315
317
  $ai_output_tokens: usage.outputTokens
316
318
  } : {}),
317
319
  ...additionalTokenValues,
318
- $ai_latency: latency,
319
- ...(timeToFirstToken !== undefined ? {
320
- $ai_time_to_first_token: timeToFirstToken
320
+ $ai_latency: options.latency ?? 0,
321
+ ...(options.timeToFirstToken !== undefined ? {
322
+ $ai_time_to_first_token: options.timeToFirstToken
321
323
  } : {}),
322
324
  $ai_trace_id: traceId,
323
- $ai_base_url: baseURL,
324
- ...params.posthogProperties,
325
- $ai_tokens_source: getTokensSource(params.posthogProperties),
326
- ...(distinctId ? {} : {
325
+ $ai_base_url: options.baseURL ?? '',
326
+ ...options.properties,
327
+ $ai_tokens_source: getTokensSource(options.properties),
328
+ ...(options.distinctId ? {} : {
327
329
  $process_person_profile: false
328
330
  }),
329
- ...(stopReason ? {
330
- $ai_stop_reason: stopReason
331
+ ...(options.stopReason ? {
332
+ $ai_stop_reason: options.stopReason
331
333
  } : {}),
332
- ...(tools ? {
333
- $ai_tools: tools
334
+ ...(options.tools ? {
335
+ $ai_tools: options.tools
334
336
  } : {}),
335
337
  ...errorData,
336
338
  ...costOverrideData
337
339
  };
338
340
  const event = {
339
- distinctId: distinctId ?? traceId,
341
+ distinctId: options.distinctId ?? traceId,
340
342
  event: eventType,
341
343
  properties,
342
- groups: params.posthogGroups
344
+ groups: options.groups
343
345
  };
344
- if (captureImmediate) {
345
- // await capture promise to send single event in serverless environments
346
+ if (options.captureImmediate) {
346
347
  await client.captureImmediate(event);
347
348
  } else {
348
349
  client.capture(event);
349
350
  }
350
- return Promise.resolve();
351
351
  };
352
352
 
353
353
  class PostHogAnthropic extends AnthropicOriginal__default.default {
@@ -509,8 +509,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
509
509
  text: accumulatedContent
510
510
  }]
511
511
  }];
512
- await sendEventToPosthog({
513
- client: this.phClient,
512
+ await captureAiGeneration(this.phClient, {
514
513
  ...posthogParams,
515
514
  model: anthropicParams.model,
516
515
  provider: 'anthropic',
@@ -519,15 +518,14 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
519
518
  latency,
520
519
  timeToFirstToken,
521
520
  baseURL: this.baseURL,
522
- params: body,
521
+ modelParameters: getModelParams(body),
523
522
  httpStatus: 200,
524
523
  usage,
525
524
  stopReason,
526
525
  tools: availableTools
527
526
  });
528
527
  } catch (error) {
529
- const enrichedError = await sendEventWithErrorToPosthog({
530
- client: this.phClient,
528
+ await captureAiGeneration(this.phClient, {
531
529
  ...posthogParams,
532
530
  model: anthropicParams.model,
533
531
  provider: 'anthropic',
@@ -535,14 +533,14 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
535
533
  output: [],
536
534
  latency: 0,
537
535
  baseURL: this.baseURL,
538
- params: body,
536
+ modelParameters: getModelParams(body),
539
537
  usage: {
540
538
  inputTokens: 0,
541
539
  outputTokens: 0
542
540
  },
543
541
  error: error
544
542
  });
545
- throw enrichedError;
543
+ throw error;
546
544
  }
547
545
  })();
548
546
 
@@ -556,8 +554,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
556
554
  if ('content' in result) {
557
555
  const latency = (Date.now() - startTime) / 1000;
558
556
  const availableTools = extractAvailableToolCalls('anthropic', anthropicParams);
559
- await sendEventToPosthog({
560
- client: this.phClient,
557
+ await captureAiGeneration(this.phClient, {
561
558
  ...posthogParams,
562
559
  model: anthropicParams.model,
563
560
  provider: 'anthropic',
@@ -565,7 +562,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
565
562
  output: formatResponseAnthropic(result),
566
563
  latency,
567
564
  baseURL: this.baseURL,
568
- params: body,
565
+ modelParameters: getModelParams(body),
569
566
  httpStatus: 200,
570
567
  usage: {
571
568
  inputTokens: result.usage.input_tokens ?? 0,
@@ -581,8 +578,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
581
578
  }
582
579
  return result;
583
580
  }, async error => {
584
- await sendEventToPosthog({
585
- client: this.phClient,
581
+ await captureAiGeneration(this.phClient, {
586
582
  ...posthogParams,
587
583
  model: anthropicParams.model,
588
584
  provider: 'anthropic',
@@ -590,13 +586,13 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
590
586
  output: [],
591
587
  latency: 0,
592
588
  baseURL: this.baseURL,
593
- params: body,
589
+ modelParameters: getModelParams(body),
594
590
  httpStatus: error?.status ? error.status : 500,
595
591
  usage: {
596
592
  inputTokens: 0,
597
593
  outputTokens: 0
598
594
  },
599
- error: JSON.stringify(error)
595
+ error: error
600
596
  });
601
597
  throw error;
602
598
  });