@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.
@@ -8,6 +8,33 @@ const isString = value => {
8
8
  return typeof value === 'string';
9
9
  };
10
10
 
11
+ /** @internal */
12
+
13
+ /** @internal */
14
+
15
+ /** @internal */
16
+ function isFullAiCaptureEnabled(client) {
17
+ return client?.enableFullAiCapture === true;
18
+ }
19
+
20
+ /** @internal */
21
+ function captureAiEvent(client, event) {
22
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
23
+ client.captureAi(event);
24
+ return;
25
+ }
26
+ client.capture(event);
27
+ }
28
+
29
+ /** @internal */
30
+ async function captureAiEventImmediate(client, event) {
31
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
32
+ await client.captureAiImmediate(event);
33
+ return;
34
+ }
35
+ await client.captureImmediate(event);
36
+ }
37
+
11
38
  const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
12
39
  const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
13
40
  class Base64Recognizer {
@@ -113,7 +140,6 @@ class BinaryContentRedactor {
113
140
  this.recognizer = recognizer;
114
141
  }
115
142
  redact(value, mediaType) {
116
- if (this.isMultimodalEnabled()) return value;
117
143
  this.visited = new WeakSet();
118
144
  return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
119
145
  }
@@ -157,24 +183,26 @@ class BinaryContentRedactor {
157
183
  if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
158
184
  return `[base64 ${mediaType} redacted]`;
159
185
  }
160
- isMultimodalEnabled() {
161
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
162
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
163
- }
164
186
  }
165
187
 
166
188
  const redactor = new BinaryContentRedactor();
167
189
  function redactBase64DataUrl(str, mediaType) {
168
190
  return redactor.redact(str, mediaType);
169
191
  }
170
- const sanitizeGemini = data => redactor.redact(data);
192
+ const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
193
+ const sanitizeGemini = (data, client) => sanitize(data, client);
171
194
 
172
195
  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']);
196
+
197
+ /**
198
+ * Whether the caller supplied their own token counts, which override the ones the SDK
199
+ * derived from the provider response.
200
+ */
201
+ function hasTokenOverrides(posthogProperties) {
202
+ return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
203
+ }
173
204
  function getTokensSource(posthogProperties) {
174
- if (posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key))) {
175
- return 'passthrough';
176
- }
177
- return 'sdk';
205
+ return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
178
206
  }
179
207
  const STRING_FORMAT = 'utf8';
180
208
 
@@ -248,7 +276,7 @@ const buildInlineDataBlock = (mimeType, data) => {
248
276
  }
249
277
  };
250
278
  };
251
- const formatResponseGemini = response => {
279
+ const formatResponseGemini = (response, client) => {
252
280
  const output = [];
253
281
  if (response.candidates && Array.isArray(response.candidates)) {
254
282
  for (const candidate of response.candidates) {
@@ -287,7 +315,7 @@ const formatResponseGemini = response => {
287
315
  }
288
316
 
289
317
  // Sanitize base64 data for images and other large inline data
290
- data = redactBase64DataUrl(data, mimeType);
318
+ data = isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mimeType);
291
319
  content.push(buildInlineDataBlock(mimeType, data));
292
320
  }
293
321
  }
@@ -390,7 +418,7 @@ function addDefaults(params) {
390
418
  };
391
419
  }
392
420
 
393
- var version = "8.7.0";
421
+ var version = "8.8.0";
394
422
 
395
423
  const DEFAULT_MAX_DEPTH = 3;
396
424
  const MAX_STACK_LINES = 20;
@@ -557,6 +585,10 @@ const captureAiGeneration = async (client, options) => {
557
585
  $ai_total_cost_usd: inputCostUSD + outputCostUSD
558
586
  };
559
587
  }
588
+
589
+ // The caller's own token counts override the SDK-derived ones further down, via the
590
+ // `options.properties` spread.
591
+ const tokensOverridden = hasTokenOverrides(options.properties);
560
592
  const additionalTokenValues = {
561
593
  ...(usage.reasoningTokens ? {
562
594
  $ai_reasoning_tokens: usage.reasoningTokens
@@ -567,6 +599,18 @@ const captureAiGeneration = async (client, options) => {
567
599
  ...(usage.cacheCreationInputTokens ? {
568
600
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
569
601
  } : {}),
602
+ // Checked against undefined rather than truthiness, because false is the meaningful
603
+ // value here and a truthiness guard would drop it.
604
+ //
605
+ // Dropped entirely when the caller overrides the token counts: the flag describes how
606
+ // the SDK-derived counts relate to each other, so against passthrough counts it can be
607
+ // wrong in the expensive direction. Declaring inclusive over counts that are actually
608
+ // exclusive makes ingestion subtract the cache pool that was never in the input. A
609
+ // caller who knows their own accounting model can still pass
610
+ // `$ai_cache_reporting_exclusive` themselves, and that value wins.
611
+ ...(usage.cacheReportingExclusive !== undefined && !tokensOverridden ? {
612
+ $ai_cache_reporting_exclusive: usage.cacheReportingExclusive
613
+ } : {}),
570
614
  ...(usage.webSearchCount ? {
571
615
  $ai_web_search_count: usage.webSearchCount
572
616
  } : {}),
@@ -623,9 +667,9 @@ const captureAiGeneration = async (client, options) => {
623
667
  groups: options.groups
624
668
  };
625
669
  if (options.captureImmediate) {
626
- await client.captureImmediate(event);
670
+ await captureAiEventImmediate(client, event);
627
671
  } else {
628
- client.capture(event);
672
+ captureAiEvent(client, event);
629
673
  }
630
674
  } catch (error) {
631
675
  // Telemetry failures must never affect the instrumented provider call.
@@ -666,7 +710,7 @@ class WrappedModels {
666
710
  model: geminiParams.model,
667
711
  provider: 'gemini',
668
712
  input: this.formatInputForPostHog(geminiParams),
669
- output: formatResponseGemini(response),
713
+ output: formatResponseGemini(response, this.phClient),
670
714
  latency,
671
715
  baseURL: 'https://generativelanguage.googleapis.com',
672
716
  modelParameters: getModelParams(params),
@@ -676,6 +720,13 @@ class WrappedModels {
676
720
  outputTokens: metadata?.candidatesTokenCount ?? 0,
677
721
  reasoningTokens: metadata?.thoughtsTokenCount ?? 0,
678
722
  cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0,
723
+ // Gemini counts cachedContentTokenCount inside promptTokenCount, so declare the
724
+ // accounting model rather than leaving ingestion to infer it. Under explicit
725
+ // context caching the two counts come from separate measurements and can disagree
726
+ // by a few percent, which makes inference from the counts alone unreliable.
727
+ ...(metadata?.cachedContentTokenCount ? {
728
+ cacheReportingExclusive: false
729
+ } : {}),
679
730
  webSearchCount: calculateGoogleWebSearchCount(response),
680
731
  rawUsage: metadata
681
732
  },
@@ -789,6 +840,11 @@ class WrappedModels {
789
840
  outputTokens: metadata.candidatesTokenCount ?? 0,
790
841
  reasoningTokens: metadata.thoughtsTokenCount ?? 0,
791
842
  cacheReadInputTokens: metadata.cachedContentTokenCount ?? 0,
843
+ // See the non-streaming path: Gemini counts cachedContentTokenCount inside
844
+ // promptTokenCount, so the accounting model is declared rather than inferred.
845
+ ...(metadata.cachedContentTokenCount ? {
846
+ cacheReportingExclusive: false
847
+ } : {}),
792
848
  webSearchCount: usage.webSearchCount,
793
849
  rawUsage: metadata
794
850
  };
@@ -1021,7 +1077,7 @@ class WrappedModels {
1021
1077
  return null;
1022
1078
  }
1023
1079
  formatInputForPostHog(params) {
1024
- const sanitized = sanitizeGemini(params.contents);
1080
+ const sanitized = sanitizeGemini(params.contents, this.phClient);
1025
1081
  const messages = this.formatInput(sanitized);
1026
1082
  const systemInstruction = this.extractSystemInstruction(params);
1027
1083
  if (systemInstruction) {