@posthog/ai 8.7.0 → 8.8.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.
@@ -3,6 +3,33 @@ import { v4 } from 'uuid';
3
3
  import { toJsonSafeValue, uuidv7 } from '@posthog/core';
4
4
  import { Stream } from '@anthropic-ai/sdk/streaming';
5
5
 
6
+ /** @internal */
7
+
8
+ /** @internal */
9
+
10
+ /** @internal */
11
+ function isFullAiCaptureEnabled(client) {
12
+ return client?.enableFullAiCapture === true;
13
+ }
14
+
15
+ /** @internal */
16
+ function captureAiEvent(client, event) {
17
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
18
+ client.captureAi(event);
19
+ return;
20
+ }
21
+ client.capture(event);
22
+ }
23
+
24
+ /** @internal */
25
+ async function captureAiEventImmediate(client, event) {
26
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
27
+ await client.captureAiImmediate(event);
28
+ return;
29
+ }
30
+ await client.captureImmediate(event);
31
+ }
32
+
6
33
  const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
7
34
  const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
8
35
  class Base64Recognizer {
@@ -108,7 +135,6 @@ class BinaryContentRedactor {
108
135
  this.recognizer = recognizer;
109
136
  }
110
137
  redact(value, mediaType) {
111
- if (this.isMultimodalEnabled()) return value;
112
138
  this.visited = new WeakSet();
113
139
  return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
114
140
  }
@@ -152,21 +178,23 @@ class BinaryContentRedactor {
152
178
  if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
153
179
  return `[base64 ${mediaType} redacted]`;
154
180
  }
155
- isMultimodalEnabled() {
156
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
157
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
158
- }
159
181
  }
160
182
 
161
183
  const redactor = new BinaryContentRedactor();
162
- const sanitizeAnthropic = data => redactor.redact(data);
184
+ const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
185
+ const sanitizeAnthropic = (data, client) => sanitize(data, client);
163
186
 
164
187
  const TOKEN_PROPERTY_KEYS = new Set(['$ai_input_tokens', '$ai_output_tokens', '$ai_cache_read_input_tokens', '$ai_cache_creation_input_tokens', '$ai_total_tokens', '$ai_reasoning_tokens']);
188
+
189
+ /**
190
+ * Whether the caller supplied their own token counts, which override the ones the SDK
191
+ * derived from the provider response.
192
+ */
193
+ function hasTokenOverrides(posthogProperties) {
194
+ return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
195
+ }
165
196
  function getTokensSource(posthogProperties) {
166
- if (posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key))) {
167
- return 'passthrough';
168
- }
169
- return 'sdk';
197
+ return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
170
198
  }
171
199
  const STRING_FORMAT = 'utf8';
172
200
 
@@ -302,7 +330,7 @@ function addDefaults(params) {
302
330
  };
303
331
  }
304
332
 
305
- var version = "8.7.0";
333
+ var version = "8.8.0";
306
334
 
307
335
  const DEFAULT_MAX_DEPTH = 3;
308
336
  const MAX_STACK_LINES = 20;
@@ -469,6 +497,10 @@ const captureAiGeneration = async (client, options) => {
469
497
  $ai_total_cost_usd: inputCostUSD + outputCostUSD
470
498
  };
471
499
  }
500
+
501
+ // The caller's own token counts override the SDK-derived ones further down, via the
502
+ // `options.properties` spread.
503
+ const tokensOverridden = hasTokenOverrides(options.properties);
472
504
  const additionalTokenValues = {
473
505
  ...(usage.reasoningTokens ? {
474
506
  $ai_reasoning_tokens: usage.reasoningTokens
@@ -479,6 +511,18 @@ const captureAiGeneration = async (client, options) => {
479
511
  ...(usage.cacheCreationInputTokens ? {
480
512
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
481
513
  } : {}),
514
+ // Checked against undefined rather than truthiness, because false is the meaningful
515
+ // value here and a truthiness guard would drop it.
516
+ //
517
+ // Dropped entirely when the caller overrides the token counts: the flag describes how
518
+ // the SDK-derived counts relate to each other, so against passthrough counts it can be
519
+ // wrong in the expensive direction. Declaring inclusive over counts that are actually
520
+ // exclusive makes ingestion subtract the cache pool that was never in the input. A
521
+ // caller who knows their own accounting model can still pass
522
+ // `$ai_cache_reporting_exclusive` themselves, and that value wins.
523
+ ...(usage.cacheReportingExclusive !== undefined && !tokensOverridden ? {
524
+ $ai_cache_reporting_exclusive: usage.cacheReportingExclusive
525
+ } : {}),
482
526
  ...(usage.webSearchCount ? {
483
527
  $ai_web_search_count: usage.webSearchCount
484
528
  } : {}),
@@ -535,9 +579,9 @@ const captureAiGeneration = async (client, options) => {
535
579
  groups: options.groups
536
580
  };
537
581
  if (options.captureImmediate) {
538
- await client.captureImmediate(event);
582
+ await captureAiEventImmediate(client, event);
539
583
  } else {
540
- client.capture(event);
584
+ captureAiEvent(client, event);
541
585
  }
542
586
  } catch (error) {
543
587
  // Telemetry failures must never affect the instrumented provider call.
@@ -998,7 +1042,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
998
1042
  ...posthogParams,
999
1043
  model: anthropicParams.model,
1000
1044
  provider: 'anthropic',
1001
- input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams, 'anthropic')),
1045
+ input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams, 'anthropic'), this.phClient),
1002
1046
  output: formattedOutput,
1003
1047
  latency,
1004
1048
  timeToFirstToken,
@@ -1014,7 +1058,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
1014
1058
  ...posthogParams,
1015
1059
  model: anthropicParams.model,
1016
1060
  provider: 'anthropic',
1017
- input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams)),
1061
+ input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams), this.phClient),
1018
1062
  output: [],
1019
1063
  latency: 0,
1020
1064
  baseURL: this.baseURL,
@@ -1050,7 +1094,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
1050
1094
  ...posthogParams,
1051
1095
  model: anthropicParams.model,
1052
1096
  provider: 'anthropic',
1053
- input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams)),
1097
+ input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams), this.phClient),
1054
1098
  output: formatResponseAnthropic(result),
1055
1099
  latency,
1056
1100
  baseURL: this.baseURL,
@@ -1074,7 +1118,7 @@ class WrappedMessages extends AnthropicOriginal.Messages {
1074
1118
  ...posthogParams,
1075
1119
  model: anthropicParams.model,
1076
1120
  provider: 'anthropic',
1077
- input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams)),
1121
+ input: sanitizeAnthropic(mergeSystemPrompt(anthropicParams), this.phClient),
1078
1122
  output: [],
1079
1123
  latency: 0,
1080
1124
  baseURL: this.baseURL,