@posthog/ai 7.16.14 → 7.17.0

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.14";
14
-
15
13
  // Type guards for safer type checking
16
14
 
17
15
  const isObject = value => {
@@ -211,73 +209,69 @@ function addDefaults(params) {
211
209
  traceId: params.traceId ?? uuid.v4()
212
210
  };
213
211
  }
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
- }) => {
212
+
213
+ var version = "7.17.0";
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) => {
262
235
  if (!client.capture) {
263
- return Promise.resolve();
236
+ return;
264
237
  }
265
- // sanitize input and output for UTF-8 validity
266
- const safeInput = sanitizeValues(input);
267
- const safeOutput = sanitizeValues(output);
268
- const safeError = sanitizeValues(error);
238
+ const traceId = options.traceId ?? uuid.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;
269
245
  let errorData = {};
270
- 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 = core.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
+ }
271
264
  errorData = {
272
265
  $ai_is_error: true,
273
- $ai_error: safeError,
266
+ $ai_error: sanitizeValues(JSON.stringify(options.error)),
274
267
  $exception_event_id: exceptionId
275
268
  };
276
269
  }
270
+ httpStatus = httpStatus ?? 200;
277
271
  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);
272
+ if (options.costOverride) {
273
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
274
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
281
275
  costOverrideData = {
282
276
  $ai_input_cost_usd: inputCostUSD,
283
277
  $ai_output_cost_usd: outputCostUSD,
@@ -304,50 +298,48 @@ const sendEventToPosthog = async ({
304
298
  const properties = {
305
299
  $ai_lib: 'posthog-ai',
306
300
  $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),
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),
312
306
  $ai_http_status: httpStatus,
313
307
  $ai_input_tokens: usage.inputTokens ?? 0,
314
308
  ...(usage.outputTokens !== undefined ? {
315
309
  $ai_output_tokens: usage.outputTokens
316
310
  } : {}),
317
311
  ...additionalTokenValues,
318
- $ai_latency: latency,
319
- ...(timeToFirstToken !== undefined ? {
320
- $ai_time_to_first_token: timeToFirstToken
312
+ $ai_latency: options.latency ?? 0,
313
+ ...(options.timeToFirstToken !== undefined ? {
314
+ $ai_time_to_first_token: options.timeToFirstToken
321
315
  } : {}),
322
316
  $ai_trace_id: traceId,
323
- $ai_base_url: baseURL,
324
- ...params.posthogProperties,
325
- $ai_tokens_source: getTokensSource(params.posthogProperties),
326
- ...(distinctId ? {} : {
317
+ $ai_base_url: options.baseURL ?? '',
318
+ ...options.properties,
319
+ $ai_tokens_source: getTokensSource(options.properties),
320
+ ...(options.distinctId ? {} : {
327
321
  $process_person_profile: false
328
322
  }),
329
- ...(stopReason ? {
330
- $ai_stop_reason: stopReason
323
+ ...(options.stopReason ? {
324
+ $ai_stop_reason: options.stopReason
331
325
  } : {}),
332
- ...(tools ? {
333
- $ai_tools: tools
326
+ ...(options.tools ? {
327
+ $ai_tools: options.tools
334
328
  } : {}),
335
329
  ...errorData,
336
330
  ...costOverrideData
337
331
  };
338
332
  const event = {
339
- distinctId: distinctId ?? traceId,
333
+ distinctId: options.distinctId ?? traceId,
340
334
  event: eventType,
341
335
  properties,
342
- groups: params.posthogGroups
336
+ groups: options.groups
343
337
  };
344
- if (captureImmediate) {
345
- // await capture promise to send single event in serverless environments
338
+ if (options.captureImmediate) {
346
339
  await client.captureImmediate(event);
347
340
  } else {
348
341
  client.capture(event);
349
342
  }
350
- return Promise.resolve();
351
343
  };
352
344
 
353
345
  class PostHogAnthropic extends AnthropicOriginal__default.default {
@@ -509,8 +501,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
509
501
  text: accumulatedContent
510
502
  }]
511
503
  }];
512
- await sendEventToPosthog({
513
- client: this.phClient,
504
+ await captureAiGeneration(this.phClient, {
514
505
  ...posthogParams,
515
506
  model: anthropicParams.model,
516
507
  provider: 'anthropic',
@@ -519,15 +510,14 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
519
510
  latency,
520
511
  timeToFirstToken,
521
512
  baseURL: this.baseURL,
522
- params: body,
513
+ modelParameters: getModelParams(body),
523
514
  httpStatus: 200,
524
515
  usage,
525
516
  stopReason,
526
517
  tools: availableTools
527
518
  });
528
519
  } catch (error) {
529
- const enrichedError = await sendEventWithErrorToPosthog({
530
- client: this.phClient,
520
+ await captureAiGeneration(this.phClient, {
531
521
  ...posthogParams,
532
522
  model: anthropicParams.model,
533
523
  provider: 'anthropic',
@@ -535,14 +525,14 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
535
525
  output: [],
536
526
  latency: 0,
537
527
  baseURL: this.baseURL,
538
- params: body,
528
+ modelParameters: getModelParams(body),
539
529
  usage: {
540
530
  inputTokens: 0,
541
531
  outputTokens: 0
542
532
  },
543
533
  error: error
544
534
  });
545
- throw enrichedError;
535
+ throw error;
546
536
  }
547
537
  })();
548
538
 
@@ -556,8 +546,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
556
546
  if ('content' in result) {
557
547
  const latency = (Date.now() - startTime) / 1000;
558
548
  const availableTools = extractAvailableToolCalls('anthropic', anthropicParams);
559
- await sendEventToPosthog({
560
- client: this.phClient,
549
+ await captureAiGeneration(this.phClient, {
561
550
  ...posthogParams,
562
551
  model: anthropicParams.model,
563
552
  provider: 'anthropic',
@@ -565,7 +554,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
565
554
  output: formatResponseAnthropic(result),
566
555
  latency,
567
556
  baseURL: this.baseURL,
568
- params: body,
557
+ modelParameters: getModelParams(body),
569
558
  httpStatus: 200,
570
559
  usage: {
571
560
  inputTokens: result.usage.input_tokens ?? 0,
@@ -581,8 +570,7 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
581
570
  }
582
571
  return result;
583
572
  }, async error => {
584
- await sendEventToPosthog({
585
- client: this.phClient,
573
+ await captureAiGeneration(this.phClient, {
586
574
  ...posthogParams,
587
575
  model: anthropicParams.model,
588
576
  provider: 'anthropic',
@@ -590,13 +578,13 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
590
578
  output: [],
591
579
  latency: 0,
592
580
  baseURL: this.baseURL,
593
- params: body,
581
+ modelParameters: getModelParams(body),
594
582
  httpStatus: error?.status ? error.status : 500,
595
583
  usage: {
596
584
  inputTokens: 0,
597
585
  outputTokens: 0
598
586
  },
599
- error: JSON.stringify(error)
587
+ error: error
600
588
  });
601
589
  throw error;
602
590
  });