@posthog/ai 7.17.4 → 7.18.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.
@@ -11,153 +11,155 @@ var core = require('@posthog/core');
11
11
  const isString = value => {
12
12
  return typeof value === 'string';
13
13
  };
14
- const isObject = value => {
15
- return value !== null && typeof value === 'object' && !Array.isArray(value);
16
- };
17
-
18
- const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
19
-
20
- // ============================================
21
- // Multimodal Feature Toggle
22
- // ============================================
23
-
24
- const isMultimodalEnabled = () => {
25
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
26
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
27
- };
28
-
29
- // ============================================
30
- // Base64 Detection Helpers
31
- // ============================================
32
14
 
33
- const isBase64DataUrl = str => {
34
- return /^data:([^;]+);base64,/.test(str);
35
- };
36
- const isValidUrl = str => {
37
- try {
38
- new URL(str);
39
- return true;
40
- } catch {
41
- // Not an absolute URL, check if it's a relative URL or path
42
- return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
43
- }
44
- };
45
- const isRawBase64 = str => {
46
- // Skip if it's a valid URL or path
47
- if (isValidUrl(str)) {
48
- return false;
15
+ const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
16
+ const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
17
+ class Base64Recognizer {
18
+ recognize(value, minLength) {
19
+ const dataUrl = DATA_URL_PREFIX_RE.exec(value);
20
+ if (dataUrl) return {
21
+ kind: 'data-url',
22
+ mediaType: dataUrl[1]
23
+ };
24
+ if (value.length < minLength) return {
25
+ kind: 'none'
26
+ };
27
+ const confidencePrefix = value.slice(0, minLength);
28
+ if (BASE64_ALPHABET_RE.test(confidencePrefix)) {
29
+ return {
30
+ kind: 'raw'
31
+ };
32
+ } else {
33
+ return {
34
+ kind: 'none'
35
+ };
36
+ }
49
37
  }
38
+ }
50
39
 
51
- // Check if it's a valid base64 string
52
- // Base64 images are typically at least a few hundred chars, but we'll be conservative
53
- return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
54
- };
55
- function redactBase64DataUrl(str) {
56
- if (isMultimodalEnabled()) return str;
57
- if (!isString(str)) return str;
58
-
59
- // Check for data URL format
60
- if (isBase64DataUrl(str)) {
61
- return REDACTED_IMAGE_PLACEHOLDER;
40
+ const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'];
41
+ const STRONG_CONTEXT_KEYS = new Set(['data', 'file_data', 'fileData', 'image_url', 'imageUrl', 'video_url', 'videoUrl', 'audio', 'audio_data', 'audioData', 'inline_data', 'inlineData', 'source', 'result']);
42
+ const STRONG_CONTEXT_TYPES = new Set(['image', 'image_url', 'input_image', 'audio', 'input_audio', 'video', 'video_url', 'file', 'input_file', 'document', 'media', 'file-data']);
43
+ const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data']);
44
+ const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
45
+ class MediaTypeContext {
46
+ static EMPTY = new MediaTypeContext(undefined, undefined);
47
+ constructor(parent, key) {
48
+ this.parent = parent;
49
+ this.key = key;
62
50
  }
63
-
64
- // Check for raw base64 (Vercel sends raw base64 for inline images)
65
- if (isRawBase64(str)) {
66
- return REDACTED_IMAGE_PLACEHOLDER;
51
+ inferMediaType() {
52
+ return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
67
53
  }
68
- return str;
69
- }
70
-
71
- // ============================================
72
- // Common Message Processing
73
- // ============================================
74
-
75
- const processMessages = (messages, transformContent) => {
76
- if (!messages) return messages;
77
- const processContent = content => {
78
- if (typeof content === 'string') return content;
79
- if (!content) return content;
80
- if (Array.isArray(content)) {
81
- return content.map(transformContent);
54
+ inferFromSiblingMime() {
55
+ if (!this.parent) return undefined;
56
+ for (const hint of MIME_HINT_KEYS) {
57
+ const v = this.parent[hint];
58
+ if (typeof v === 'string') return v;
82
59
  }
83
-
84
- // Handle single object content
85
- return transformContent(content);
86
- };
87
- const processMessage = msg => {
88
- if (!isObject(msg) || !('content' in msg)) return msg;
89
- return {
90
- ...msg,
91
- content: processContent(msg.content)
92
- };
93
- };
94
-
95
- // Handle both arrays and single messages
96
- if (Array.isArray(messages)) {
97
- return messages.map(processMessage);
60
+ return undefined;
98
61
  }
99
- return processMessage(messages);
100
- };
101
-
102
- // ============================================
103
- // Provider-Specific Image Sanitizers
104
- // ============================================
105
-
106
- const sanitizeOpenAIImage = item => {
107
- if (!isObject(item)) return item;
108
-
109
- // Handle image_url format
110
- if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
111
- return {
112
- ...item,
113
- image_url: {
114
- ...item.image_url,
115
- url: redactBase64DataUrl(item.image_url.url)
116
- }
117
- };
62
+ inferFromSiblingFormat() {
63
+ if (!this.parent) return undefined;
64
+ const fmt = this.parent.format;
65
+ if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {
66
+ return `audio/${fmt.toLowerCase()}`;
67
+ }
68
+ return undefined;
118
69
  }
119
-
120
- // Handle video_url format
121
- if (item.type === 'video_url' && 'video_url' in item && isObject(item.video_url) && 'url' in item.video_url) {
122
- return {
123
- ...item,
124
- video_url: {
125
- ...item.video_url,
126
- url: redactBase64DataUrl(item.video_url.url)
70
+ inferFromParentType() {
71
+ if (!this.parent) return undefined;
72
+ const t = this.parent.type;
73
+ if (typeof t !== 'string') return undefined;
74
+ if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image';
75
+ if (t === 'audio' || t === 'input_audio') return 'audio';
76
+ if (t === 'video' || t === 'video_url') return 'video';
77
+ if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream';
78
+ return undefined;
79
+ }
80
+ inferFromKey() {
81
+ if (!this.key) return undefined;
82
+ const key = this.key.toLowerCase();
83
+ if (key.includes('audio')) return 'audio';
84
+ if (key.includes('video')) return 'video';
85
+ if (key.includes('image')) return 'image';
86
+ if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
87
+ return undefined;
88
+ }
89
+ signalsBinary() {
90
+ if (this.parent) {
91
+ for (const hint of MIME_HINT_KEYS) {
92
+ if (typeof this.parent[hint] === 'string') return true;
127
93
  }
128
- };
94
+ const fmt = this.parent.format;
95
+ if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
96
+ const t = this.parent.type;
97
+ if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true;
98
+ }
99
+ if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
100
+ return false;
129
101
  }
102
+ }
130
103
 
131
- // Handle audio format
132
- if (item.type === 'audio' && 'data' in item) {
133
- if (isMultimodalEnabled()) return item;
134
- return {
135
- ...item,
136
- data: REDACTED_IMAGE_PLACEHOLDER
137
- };
104
+ const STRONG_CONTEXT_MIN_LENGTH = 64;
105
+ const WEAK_CONTEXT_MIN_LENGTH = 1024;
106
+ class BinaryContentRedactor {
107
+ visited = new WeakSet();
108
+ constructor(recognizer = new Base64Recognizer()) {
109
+ this.recognizer = recognizer;
138
110
  }
139
- return item;
140
- };
141
- const sanitizeOpenAIResponseImage = item => {
142
- if (!isObject(item)) return item;
143
-
144
- // Handle input_image format
145
- if (item.type === 'input_image' && 'image_url' in item) {
146
- return {
147
- ...item,
148
- image_url: redactBase64DataUrl(item.image_url)
149
- };
111
+ redact(value) {
112
+ if (this.isMultimodalEnabled()) return value;
113
+ this.visited = new WeakSet();
114
+ return this.walk(value, MediaTypeContext.EMPTY);
150
115
  }
151
- return item;
152
- };
116
+ walk(value, ctx) {
117
+ if (value === null || value === undefined) return value;
118
+ if (typeof value === 'string') return this.redactString(value, ctx);
119
+ if (typeof value !== 'object') return value;
120
+
121
+ // Buffer extends Uint8Array, so this branch catches both.
122
+ if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
123
+ return this.placeholderFor(ctx.inferMediaType());
124
+ }
125
+ if (this.visited.has(value)) return null;
126
+ this.visited.add(value);
127
+ if (Array.isArray(value)) {
128
+ return value.map(item => this.walk(item, ctx));
129
+ }
130
+ const obj = value;
131
+ const out = {};
132
+ for (const k of Object.keys(obj)) {
133
+ out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
134
+ }
135
+ return out;
136
+ }
137
+ redactString(value, ctx) {
138
+ const minLength = ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
139
+ const recognition = this.recognizer.recognize(value, minLength);
140
+ switch (recognition.kind) {
141
+ case 'data-url':
142
+ return this.placeholderFor(recognition.mediaType);
143
+ case 'raw':
144
+ return this.placeholderFor(ctx.inferMediaType());
145
+ case 'none':
146
+ return value;
147
+ }
148
+ }
149
+ placeholderFor(mediaType) {
150
+ if (!mediaType) return '[base64 redacted]';
151
+ if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
152
+ return `[base64 ${mediaType} redacted]`;
153
+ }
154
+ isMultimodalEnabled() {
155
+ const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
156
+ return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
157
+ }
158
+ }
153
159
 
154
- // Export individual sanitizers for tree-shaking
155
- const sanitizeOpenAI = data => {
156
- return processMessages(data, sanitizeOpenAIImage);
157
- };
158
- const sanitizeOpenAIResponse = data => {
159
- return processMessages(data, sanitizeOpenAIResponseImage);
160
- };
160
+ const redactor = new BinaryContentRedactor();
161
+ const sanitizeOpenAI = data => redactor.redact(data);
162
+ const sanitizeOpenAIResponse = data => redactor.redact(data);
161
163
 
162
164
  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']);
163
165
  function getTokensSource(posthogProperties) {
@@ -532,7 +534,7 @@ function formatOpenAIResponsesInput(input, instructions) {
532
534
  return messages;
533
535
  }
534
536
 
535
- var version = "7.17.4";
537
+ var version = "7.18.0";
536
538
 
537
539
  /**
538
540
  * Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape