@posthog/ai 6.0.1 → 6.1.1

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.
@@ -535,6 +535,134 @@ class BaseCallbackHandler extends BaseCallbackHandlerMethodsClass {
535
535
  }
536
536
  }
537
537
 
538
+ // Type guards for safer type checking
539
+
540
+ const isString = value => {
541
+ return typeof value === 'string';
542
+ };
543
+ const isObject = value => {
544
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
545
+ };
546
+
547
+ const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
548
+
549
+ // ============================================
550
+ // Base64 Detection Helpers
551
+ // ============================================
552
+
553
+ const isBase64DataUrl = str => {
554
+ return /^data:([^;]+);base64,/.test(str);
555
+ };
556
+ const isValidUrl = str => {
557
+ try {
558
+ new URL(str);
559
+ return true;
560
+ } catch {
561
+ // Not an absolute URL, check if it's a relative URL or path
562
+ return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
563
+ }
564
+ };
565
+ const isRawBase64 = str => {
566
+ // Skip if it's a valid URL or path
567
+ if (isValidUrl(str)) {
568
+ return false;
569
+ }
570
+
571
+ // Check if it's a valid base64 string
572
+ // Base64 images are typically at least a few hundred chars, but we'll be conservative
573
+ return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
574
+ };
575
+ function redactBase64DataUrl(str) {
576
+ if (!isString(str)) return str;
577
+
578
+ // Check for data URL format
579
+ if (isBase64DataUrl(str)) {
580
+ return REDACTED_IMAGE_PLACEHOLDER;
581
+ }
582
+
583
+ // Check for raw base64 (Vercel sends raw base64 for inline images)
584
+ if (isRawBase64(str)) {
585
+ return REDACTED_IMAGE_PLACEHOLDER;
586
+ }
587
+ return str;
588
+ }
589
+
590
+ // ============================================
591
+ // Common Message Processing
592
+ // ============================================
593
+
594
+ const processMessages = (messages, transformContent) => {
595
+ if (!messages) return messages;
596
+ const processContent = content => {
597
+ if (typeof content === 'string') return content;
598
+ if (!content) return content;
599
+ if (Array.isArray(content)) {
600
+ return content.map(transformContent);
601
+ }
602
+
603
+ // Handle single object content
604
+ return transformContent(content);
605
+ };
606
+ const processMessage = msg => {
607
+ if (!isObject(msg) || !('content' in msg)) return msg;
608
+ return {
609
+ ...msg,
610
+ content: processContent(msg.content)
611
+ };
612
+ };
613
+
614
+ // Handle both arrays and single messages
615
+ if (Array.isArray(messages)) {
616
+ return messages.map(processMessage);
617
+ }
618
+ return processMessage(messages);
619
+ };
620
+ const sanitizeLangChainImage = item => {
621
+ if (!isObject(item)) return item;
622
+
623
+ // OpenAI style
624
+ if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
625
+ return {
626
+ ...item,
627
+ image_url: {
628
+ ...item.image_url,
629
+ url: redactBase64DataUrl(item.image_url.url)
630
+ }
631
+ };
632
+ }
633
+
634
+ // Direct image with data field
635
+ if (item.type === 'image' && 'data' in item) {
636
+ return {
637
+ ...item,
638
+ data: redactBase64DataUrl(item.data)
639
+ };
640
+ }
641
+
642
+ // Anthropic style
643
+ if (item.type === 'image' && 'source' in item && isObject(item.source) && 'data' in item.source) {
644
+ return {
645
+ ...item,
646
+ source: {
647
+ ...item.source,
648
+ data: redactBase64DataUrl(item.source.data)
649
+ }
650
+ };
651
+ }
652
+
653
+ // Google style
654
+ if (item.type === 'media' && 'data' in item) {
655
+ return {
656
+ ...item,
657
+ data: redactBase64DataUrl(item.data)
658
+ };
659
+ }
660
+ return item;
661
+ };
662
+ const sanitizeLangChain = data => {
663
+ return processMessages(data, sanitizeLangChainImage);
664
+ };
665
+
538
666
  /** A run may either be a Span or a Generation */
539
667
 
540
668
  /** Storage for run metadata */
@@ -715,7 +843,7 @@ class LangChainCallbackHandler extends BaseCallbackHandler {
715
843
  }) || 'generation';
716
844
  const generation = {
717
845
  name: runNameFound,
718
- input: messages,
846
+ input: sanitizeLangChain(messages),
719
847
  startTime: Date.now()
720
848
  };
721
849
  if (extraParams) {
@@ -980,7 +1108,9 @@ class LangChainCallbackHandler extends BaseCallbackHandler {
980
1108
  ...message.additional_kwargs
981
1109
  };
982
1110
  }
983
- return messageDict;
1111
+
1112
+ // Sanitize the message content to redact base64 images
1113
+ return sanitizeLangChain(messageDict);
984
1114
  }
985
1115
  _parseUsageModel(usage) {
986
1116
  const conversionList = [['promptTokens', 'input'], ['completionTokens', 'output'], ['input_tokens', 'input'], ['output_tokens', 'output'], ['prompt_token_count', 'input'], ['candidates_token_count', 'output'], ['inputTokenCount', 'input'], ['outputTokenCount', 'output'], ['input_token_count', 'input'], ['generated_token_count', 'output']];