@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.
@@ -12,6 +12,33 @@ const isString = value => {
12
12
  return typeof value === 'string';
13
13
  };
14
14
 
15
+ /** @internal */
16
+
17
+ /** @internal */
18
+
19
+ /** @internal */
20
+ function isFullAiCaptureEnabled(client) {
21
+ return client?.enableFullAiCapture === true;
22
+ }
23
+
24
+ /** @internal */
25
+ function captureAiEvent(client, event) {
26
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
27
+ client.captureAi(event);
28
+ return;
29
+ }
30
+ client.capture(event);
31
+ }
32
+
33
+ /** @internal */
34
+ async function captureAiEventImmediate(client, event) {
35
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
36
+ await client.captureAiImmediate(event);
37
+ return;
38
+ }
39
+ await client.captureImmediate(event);
40
+ }
41
+
15
42
  const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
16
43
  const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
17
44
  class Base64Recognizer {
@@ -117,7 +144,6 @@ class BinaryContentRedactor {
117
144
  this.recognizer = recognizer;
118
145
  }
119
146
  redact(value, mediaType) {
120
- if (this.isMultimodalEnabled()) return value;
121
147
  this.visited = new WeakSet();
122
148
  return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
123
149
  }
@@ -161,24 +187,26 @@ class BinaryContentRedactor {
161
187
  if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
162
188
  return `[base64 ${mediaType} redacted]`;
163
189
  }
164
- isMultimodalEnabled() {
165
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
166
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
167
- }
168
190
  }
169
191
 
170
192
  const redactor = new BinaryContentRedactor();
171
193
  function redactBase64DataUrl(str, mediaType) {
172
194
  return redactor.redact(str, mediaType);
173
195
  }
174
- const sanitizeGemini = data => redactor.redact(data);
196
+ const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
197
+ const sanitizeGemini = (data, client) => sanitize(data, client);
175
198
 
176
199
  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']);
200
+
201
+ /**
202
+ * Whether the caller supplied their own token counts, which override the ones the SDK
203
+ * derived from the provider response.
204
+ */
205
+ function hasTokenOverrides(posthogProperties) {
206
+ return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
207
+ }
177
208
  function getTokensSource(posthogProperties) {
178
- if (posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key))) {
179
- return 'passthrough';
180
- }
181
- return 'sdk';
209
+ return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
182
210
  }
183
211
  const STRING_FORMAT = 'utf8';
184
212
 
@@ -252,7 +280,7 @@ const buildInlineDataBlock = (mimeType, data) => {
252
280
  }
253
281
  };
254
282
  };
255
- const formatResponseGemini = response => {
283
+ const formatResponseGemini = (response, client) => {
256
284
  const output = [];
257
285
  if (response.candidates && Array.isArray(response.candidates)) {
258
286
  for (const candidate of response.candidates) {
@@ -291,7 +319,7 @@ const formatResponseGemini = response => {
291
319
  }
292
320
 
293
321
  // Sanitize base64 data for images and other large inline data
294
- data = redactBase64DataUrl(data, mimeType);
322
+ data = isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mimeType);
295
323
  content.push(buildInlineDataBlock(mimeType, data));
296
324
  }
297
325
  }
@@ -394,7 +422,7 @@ function addDefaults(params) {
394
422
  };
395
423
  }
396
424
 
397
- var version = "8.7.0";
425
+ var version = "8.8.0";
398
426
 
399
427
  const DEFAULT_MAX_DEPTH = 3;
400
428
  const MAX_STACK_LINES = 20;
@@ -561,6 +589,10 @@ const captureAiGeneration = async (client, options) => {
561
589
  $ai_total_cost_usd: inputCostUSD + outputCostUSD
562
590
  };
563
591
  }
592
+
593
+ // The caller's own token counts override the SDK-derived ones further down, via the
594
+ // `options.properties` spread.
595
+ const tokensOverridden = hasTokenOverrides(options.properties);
564
596
  const additionalTokenValues = {
565
597
  ...(usage.reasoningTokens ? {
566
598
  $ai_reasoning_tokens: usage.reasoningTokens
@@ -571,6 +603,18 @@ const captureAiGeneration = async (client, options) => {
571
603
  ...(usage.cacheCreationInputTokens ? {
572
604
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
573
605
  } : {}),
606
+ // Checked against undefined rather than truthiness, because false is the meaningful
607
+ // value here and a truthiness guard would drop it.
608
+ //
609
+ // Dropped entirely when the caller overrides the token counts: the flag describes how
610
+ // the SDK-derived counts relate to each other, so against passthrough counts it can be
611
+ // wrong in the expensive direction. Declaring inclusive over counts that are actually
612
+ // exclusive makes ingestion subtract the cache pool that was never in the input. A
613
+ // caller who knows their own accounting model can still pass
614
+ // `$ai_cache_reporting_exclusive` themselves, and that value wins.
615
+ ...(usage.cacheReportingExclusive !== undefined && !tokensOverridden ? {
616
+ $ai_cache_reporting_exclusive: usage.cacheReportingExclusive
617
+ } : {}),
574
618
  ...(usage.webSearchCount ? {
575
619
  $ai_web_search_count: usage.webSearchCount
576
620
  } : {}),
@@ -627,9 +671,9 @@ const captureAiGeneration = async (client, options) => {
627
671
  groups: options.groups
628
672
  };
629
673
  if (options.captureImmediate) {
630
- await client.captureImmediate(event);
674
+ await captureAiEventImmediate(client, event);
631
675
  } else {
632
- client.capture(event);
676
+ captureAiEvent(client, event);
633
677
  }
634
678
  } catch (error) {
635
679
  // Telemetry failures must never affect the instrumented provider call.
@@ -670,7 +714,7 @@ class WrappedModels {
670
714
  model: geminiParams.model,
671
715
  provider: 'gemini',
672
716
  input: this.formatInputForPostHog(geminiParams),
673
- output: formatResponseGemini(response),
717
+ output: formatResponseGemini(response, this.phClient),
674
718
  latency,
675
719
  baseURL: 'https://generativelanguage.googleapis.com',
676
720
  modelParameters: getModelParams(params),
@@ -680,6 +724,13 @@ class WrappedModels {
680
724
  outputTokens: metadata?.candidatesTokenCount ?? 0,
681
725
  reasoningTokens: metadata?.thoughtsTokenCount ?? 0,
682
726
  cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0,
727
+ // Gemini counts cachedContentTokenCount inside promptTokenCount, so declare the
728
+ // accounting model rather than leaving ingestion to infer it. Under explicit
729
+ // context caching the two counts come from separate measurements and can disagree
730
+ // by a few percent, which makes inference from the counts alone unreliable.
731
+ ...(metadata?.cachedContentTokenCount ? {
732
+ cacheReportingExclusive: false
733
+ } : {}),
683
734
  webSearchCount: calculateGoogleWebSearchCount(response),
684
735
  rawUsage: metadata
685
736
  },
@@ -793,6 +844,11 @@ class WrappedModels {
793
844
  outputTokens: metadata.candidatesTokenCount ?? 0,
794
845
  reasoningTokens: metadata.thoughtsTokenCount ?? 0,
795
846
  cacheReadInputTokens: metadata.cachedContentTokenCount ?? 0,
847
+ // See the non-streaming path: Gemini counts cachedContentTokenCount inside
848
+ // promptTokenCount, so the accounting model is declared rather than inferred.
849
+ ...(metadata.cachedContentTokenCount ? {
850
+ cacheReportingExclusive: false
851
+ } : {}),
796
852
  webSearchCount: usage.webSearchCount,
797
853
  rawUsage: metadata
798
854
  };
@@ -1025,7 +1081,7 @@ class WrappedModels {
1025
1081
  return null;
1026
1082
  }
1027
1083
  formatInputForPostHog(params) {
1028
- const sanitized = sanitizeGemini(params.contents);
1084
+ const sanitized = sanitizeGemini(params.contents, this.phClient);
1029
1085
  const messages = this.formatInput(sanitized);
1030
1086
  const systemInstruction = this.extractSystemInstruction(params);
1031
1087
  if (systemInstruction) {