@posthog/ai 7.5.4 → 7.6.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.
@@ -2,7 +2,7 @@ import 'buffer';
2
2
  import * as uuid from 'uuid';
3
3
  import '@posthog/core';
4
4
 
5
- var version = "7.5.4";
5
+ var version = "7.6.0";
6
6
 
7
7
  // Type guards for safer type checking
8
8
 
@@ -13,6 +13,136 @@ const isObject = value => {
13
13
  return value !== null && typeof value === 'object' && !Array.isArray(value);
14
14
  };
15
15
 
16
+ const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
17
+
18
+ // ============================================
19
+ // Multimodal Feature Toggle
20
+ // ============================================
21
+
22
+ const isMultimodalEnabled = () => {
23
+ const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
24
+ return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
25
+ };
26
+
27
+ // ============================================
28
+ // Base64 Detection Helpers
29
+ // ============================================
30
+
31
+ const isBase64DataUrl = str => {
32
+ return /^data:([^;]+);base64,/.test(str);
33
+ };
34
+ const isValidUrl = str => {
35
+ try {
36
+ new URL(str);
37
+ return true;
38
+ } catch {
39
+ // Not an absolute URL, check if it's a relative URL or path
40
+ return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
41
+ }
42
+ };
43
+ const isRawBase64 = str => {
44
+ // Skip if it's a valid URL or path
45
+ if (isValidUrl(str)) {
46
+ return false;
47
+ }
48
+
49
+ // Check if it's a valid base64 string
50
+ // Base64 images are typically at least a few hundred chars, but we'll be conservative
51
+ return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
52
+ };
53
+ function redactBase64DataUrl(str) {
54
+ if (isMultimodalEnabled()) return str;
55
+ if (!isString(str)) return str;
56
+
57
+ // Check for data URL format
58
+ if (isBase64DataUrl(str)) {
59
+ return REDACTED_IMAGE_PLACEHOLDER;
60
+ }
61
+
62
+ // Check for raw base64 (Vercel sends raw base64 for inline images)
63
+ if (isRawBase64(str)) {
64
+ return REDACTED_IMAGE_PLACEHOLDER;
65
+ }
66
+ return str;
67
+ }
68
+
69
+ // ============================================
70
+ // Common Message Processing
71
+ // ============================================
72
+
73
+ const processMessages = (messages, transformContent) => {
74
+ if (!messages) return messages;
75
+ const processContent = content => {
76
+ if (typeof content === 'string') return content;
77
+ if (!content) return content;
78
+ if (Array.isArray(content)) {
79
+ return content.map(transformContent);
80
+ }
81
+
82
+ // Handle single object content
83
+ return transformContent(content);
84
+ };
85
+ const processMessage = msg => {
86
+ if (!isObject(msg) || !('content' in msg)) return msg;
87
+ return {
88
+ ...msg,
89
+ content: processContent(msg.content)
90
+ };
91
+ };
92
+
93
+ // Handle both arrays and single messages
94
+ if (Array.isArray(messages)) {
95
+ return messages.map(processMessage);
96
+ }
97
+ return processMessage(messages);
98
+ };
99
+ const sanitizeLangChainImage = item => {
100
+ if (!isObject(item)) return item;
101
+
102
+ // OpenAI style
103
+ if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
104
+ return {
105
+ ...item,
106
+ image_url: {
107
+ ...item.image_url,
108
+ url: redactBase64DataUrl(item.image_url.url)
109
+ }
110
+ };
111
+ }
112
+
113
+ // Direct image with data field
114
+ if (item.type === 'image' && 'data' in item) {
115
+ return {
116
+ ...item,
117
+ data: redactBase64DataUrl(item.data)
118
+ };
119
+ }
120
+
121
+ // Anthropic style
122
+ if (item.type === 'image' && 'source' in item && isObject(item.source) && 'data' in item.source) {
123
+ if (isMultimodalEnabled()) return item;
124
+ return {
125
+ ...item,
126
+ source: {
127
+ ...item.source,
128
+ data: redactBase64DataUrl(item.source.data)
129
+ }
130
+ };
131
+ }
132
+
133
+ // Google style
134
+ if (item.type === 'media' && 'data' in item) {
135
+ return {
136
+ ...item,
137
+ data: redactBase64DataUrl(item.data)
138
+ };
139
+ }
140
+ return item;
141
+ };
142
+ const sanitizeLangChain = data => {
143
+ return processMessages(data, sanitizeLangChainImage);
144
+ };
145
+
16
146
  /**
17
147
  * Safely converts content to a string, preserving structure for objects/arrays.
18
148
  * - If content is already a string, returns it as-is
@@ -262,6 +392,20 @@ function isSerializableLike(obj) {
262
392
  return obj !== null && typeof obj === "object" && "lc_serializable" in obj && typeof obj.toJSON === "function";
263
393
  }
264
394
  /**
395
+ * Create a "not_implemented" serialization result for objects that cannot be serialized.
396
+ */
397
+ function createNotImplemented(obj) {
398
+ let id;
399
+ if (obj !== null && typeof obj === "object") if ("lc_id" in obj && Array.isArray(obj.lc_id)) id = obj.lc_id;
400
+ else id = [obj.constructor?.name ?? "Object"];
401
+ else id = [typeof obj];
402
+ return {
403
+ lc: 1,
404
+ type: "not_implemented",
405
+ id
406
+ };
407
+ }
408
+ /**
265
409
  * Escape a value if it needs escaping (contains `lc` key).
266
410
  *
267
411
  * This is a simpler version of `serializeValue` that doesn't handle Serializable
@@ -269,18 +413,27 @@ function isSerializableLike(obj) {
269
413
  * processed by `toJSON()`.
270
414
  *
271
415
  * @param value - The value to potentially escape.
416
+ * @param pathSet - WeakSet to track ancestor objects in the current path to detect circular references.
417
+ * Objects are removed after processing to allow shared references (same object in
418
+ * multiple places) while still detecting true circular references (ancestor in descendant).
272
419
  * @returns The value with any `lc`-containing objects wrapped in escape markers.
273
420
  */
274
- function escapeIfNeeded(value) {
421
+ function escapeIfNeeded(value, pathSet = /* @__PURE__ */ new WeakSet()) {
275
422
  if (value !== null && typeof value === "object" && !Array.isArray(value)) {
423
+ if (pathSet.has(value)) return createNotImplemented(value);
276
424
  if (isSerializableLike(value)) return value;
425
+ pathSet.add(value);
277
426
  const record = value;
278
- if (needsEscaping(record)) return escapeObject(record);
427
+ if (needsEscaping(record)) {
428
+ pathSet.delete(value);
429
+ return escapeObject(record);
430
+ }
279
431
  const result = {};
280
- for (const [key, val] of Object.entries(record)) result[key] = escapeIfNeeded(val);
432
+ for (const [key, val] of Object.entries(record)) result[key] = escapeIfNeeded(val, pathSet);
433
+ pathSet.delete(value);
281
434
  return result;
282
435
  }
283
- if (Array.isArray(value)) return value.map((item) => escapeIfNeeded(item));
436
+ if (Array.isArray(value)) return value.map((item) => escapeIfNeeded(item, pathSet));
284
437
  return value;
285
438
  }
286
439
 
@@ -406,7 +559,9 @@ var Serializable = class Serializable {
406
559
  if (last in read && read[last] !== void 0) write[last] = write[last] || read[last];
407
560
  });
408
561
  const escapedKwargs = {};
409
- for (const [key, value] of Object.entries(kwargs)) escapedKwargs[key] = escapeIfNeeded(value);
562
+ const pathSet = /* @__PURE__ */ new WeakSet();
563
+ pathSet.add(this);
564
+ for (const [key, value] of Object.entries(kwargs)) escapedKwargs[key] = escapeIfNeeded(value, pathSet);
410
565
  const kwargsWithSecrets = Object.keys(secrets).length ? replaceSecrets(escapedKwargs, secrets) : escapedKwargs;
411
566
  const processedKwargs = mapKeys(kwargsWithSecrets, keyToJson, aliases);
412
567
  return {
@@ -577,136 +732,6 @@ const isBaseCallbackHandler = (x) => {
577
732
  return callbackHandler !== void 0 && typeof callbackHandler.copy === "function" && typeof callbackHandler.name === "string" && typeof callbackHandler.awaitHandlers === "boolean";
578
733
  };
579
734
 
580
- const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
581
-
582
- // ============================================
583
- // Multimodal Feature Toggle
584
- // ============================================
585
-
586
- const isMultimodalEnabled = () => {
587
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
588
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
589
- };
590
-
591
- // ============================================
592
- // Base64 Detection Helpers
593
- // ============================================
594
-
595
- const isBase64DataUrl = str => {
596
- return /^data:([^;]+);base64,/.test(str);
597
- };
598
- const isValidUrl = str => {
599
- try {
600
- new URL(str);
601
- return true;
602
- } catch {
603
- // Not an absolute URL, check if it's a relative URL or path
604
- return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
605
- }
606
- };
607
- const isRawBase64 = str => {
608
- // Skip if it's a valid URL or path
609
- if (isValidUrl(str)) {
610
- return false;
611
- }
612
-
613
- // Check if it's a valid base64 string
614
- // Base64 images are typically at least a few hundred chars, but we'll be conservative
615
- return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
616
- };
617
- function redactBase64DataUrl(str) {
618
- if (isMultimodalEnabled()) return str;
619
- if (!isString(str)) return str;
620
-
621
- // Check for data URL format
622
- if (isBase64DataUrl(str)) {
623
- return REDACTED_IMAGE_PLACEHOLDER;
624
- }
625
-
626
- // Check for raw base64 (Vercel sends raw base64 for inline images)
627
- if (isRawBase64(str)) {
628
- return REDACTED_IMAGE_PLACEHOLDER;
629
- }
630
- return str;
631
- }
632
-
633
- // ============================================
634
- // Common Message Processing
635
- // ============================================
636
-
637
- const processMessages = (messages, transformContent) => {
638
- if (!messages) return messages;
639
- const processContent = content => {
640
- if (typeof content === 'string') return content;
641
- if (!content) return content;
642
- if (Array.isArray(content)) {
643
- return content.map(transformContent);
644
- }
645
-
646
- // Handle single object content
647
- return transformContent(content);
648
- };
649
- const processMessage = msg => {
650
- if (!isObject(msg) || !('content' in msg)) return msg;
651
- return {
652
- ...msg,
653
- content: processContent(msg.content)
654
- };
655
- };
656
-
657
- // Handle both arrays and single messages
658
- if (Array.isArray(messages)) {
659
- return messages.map(processMessage);
660
- }
661
- return processMessage(messages);
662
- };
663
- const sanitizeLangChainImage = item => {
664
- if (!isObject(item)) return item;
665
-
666
- // OpenAI style
667
- if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
668
- return {
669
- ...item,
670
- image_url: {
671
- ...item.image_url,
672
- url: redactBase64DataUrl(item.image_url.url)
673
- }
674
- };
675
- }
676
-
677
- // Direct image with data field
678
- if (item.type === 'image' && 'data' in item) {
679
- return {
680
- ...item,
681
- data: redactBase64DataUrl(item.data)
682
- };
683
- }
684
-
685
- // Anthropic style
686
- if (item.type === 'image' && 'source' in item && isObject(item.source) && 'data' in item.source) {
687
- if (isMultimodalEnabled()) return item;
688
- return {
689
- ...item,
690
- source: {
691
- ...item.source,
692
- data: redactBase64DataUrl(item.source.data)
693
- }
694
- };
695
- }
696
-
697
- // Google style
698
- if (item.type === 'media' && 'data' in item) {
699
- return {
700
- ...item,
701
- data: redactBase64DataUrl(item.data)
702
- };
703
- }
704
- return item;
705
- };
706
- const sanitizeLangChain = data => {
707
- return processMessages(data, sanitizeLangChainImage);
708
- };
709
-
710
735
  /** A run may either be a Span or a Generation */
711
736
 
712
737
  /** Storage for run metadata */