@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.
package/dist/index.cjs CHANGED
@@ -33,228 +33,160 @@ var AnthropicOriginal__default = /*#__PURE__*/_interopDefault(AnthropicOriginal)
33
33
  const isString = value => {
34
34
  return typeof value === 'string';
35
35
  };
36
- const isObject = value => {
37
- return value !== null && typeof value === 'object' && !Array.isArray(value);
38
- };
39
36
 
40
- const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
41
- // ============================================
42
- // Multimodal Feature Toggle
43
- // ============================================
44
- const isMultimodalEnabled = () => {
45
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
46
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
47
- };
48
- // ============================================
49
- // Base64 Detection Helpers
50
- // ============================================
51
- const isBase64DataUrl = str => {
52
- return /^data:([^;]+);base64,/.test(str);
53
- };
54
- const isValidUrl = str => {
55
- try {
56
- new URL(str);
57
- return true;
58
- } catch {
59
- // Not an absolute URL, check if it's a relative URL or path
60
- return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
61
- }
62
- };
63
- const isRawBase64 = str => {
64
- // Skip if it's a valid URL or path
65
- if (isValidUrl(str)) {
66
- return false;
67
- }
68
- // Check if it's a valid base64 string
69
- // Base64 images are typically at least a few hundred chars, but we'll be conservative
70
- return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
71
- };
72
- function redactBase64DataUrl(str) {
73
- if (isMultimodalEnabled()) return str;
74
- if (!isString(str)) return str;
75
- // Check for data URL format
76
- if (isBase64DataUrl(str)) {
77
- return REDACTED_IMAGE_PLACEHOLDER;
78
- }
79
- // Check for raw base64 (Vercel sends raw base64 for inline images)
80
- if (isRawBase64(str)) {
81
- return REDACTED_IMAGE_PLACEHOLDER;
82
- }
83
- return str;
84
- }
85
- const processMessages = (messages, transformContent) => {
86
- if (!messages) return messages;
87
- const processContent = content => {
88
- if (typeof content === 'string') return content;
89
- if (!content) return content;
90
- if (Array.isArray(content)) {
91
- return content.map(transformContent);
92
- }
93
- // Handle single object content
94
- return transformContent(content);
95
- };
96
- const processMessage = msg => {
97
- if (!isObject(msg) || !('content' in msg)) return msg;
98
- return {
99
- ...msg,
100
- content: processContent(msg.content)
37
+ const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
38
+ const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
39
+ class Base64Recognizer {
40
+ recognize(value, minLength) {
41
+ const dataUrl = DATA_URL_PREFIX_RE.exec(value);
42
+ if (dataUrl) return {
43
+ kind: 'data-url',
44
+ mediaType: dataUrl[1]
101
45
  };
102
- };
103
- // Handle both arrays and single messages
104
- if (Array.isArray(messages)) {
105
- return messages.map(processMessage);
106
- }
107
- return processMessage(messages);
108
- };
109
- // ============================================
110
- // Provider-Specific Image Sanitizers
111
- // ============================================
112
- const sanitizeOpenAIImage = item => {
113
- if (!isObject(item)) return item;
114
- // Handle image_url format
115
- if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
116
- return {
117
- ...item,
118
- image_url: {
119
- ...item.image_url,
120
- url: redactBase64DataUrl(item.image_url.url)
121
- }
122
- };
123
- }
124
- // Handle video_url format
125
- if (item.type === 'video_url' && 'video_url' in item && isObject(item.video_url) && 'url' in item.video_url) {
126
- return {
127
- ...item,
128
- video_url: {
129
- ...item.video_url,
130
- url: redactBase64DataUrl(item.video_url.url)
131
- }
132
- };
133
- }
134
- // Handle audio format
135
- if (item.type === 'audio' && 'data' in item) {
136
- if (isMultimodalEnabled()) return item;
137
- return {
138
- ...item,
139
- data: REDACTED_IMAGE_PLACEHOLDER
46
+ if (value.length < minLength) return {
47
+ kind: 'none'
140
48
  };
49
+ const confidencePrefix = value.slice(0, minLength);
50
+ if (BASE64_ALPHABET_RE.test(confidencePrefix)) {
51
+ return {
52
+ kind: 'raw'
53
+ };
54
+ } else {
55
+ return {
56
+ kind: 'none'
57
+ };
58
+ }
141
59
  }
142
- return item;
143
- };
144
- const sanitizeOpenAIResponseImage = item => {
145
- if (!isObject(item)) return item;
146
- // Handle input_image format
147
- if (item.type === 'input_image' && 'image_url' in item) {
148
- return {
149
- ...item,
150
- image_url: redactBase64DataUrl(item.image_url)
151
- };
60
+ }
61
+
62
+ const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'];
63
+ 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']);
64
+ const STRONG_CONTEXT_TYPES = new Set(['image', 'image_url', 'input_image', 'audio', 'input_audio', 'video', 'video_url', 'file', 'input_file', 'document', 'media', 'file-data']);
65
+ const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data']);
66
+ const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
67
+ class MediaTypeContext {
68
+ constructor(parent, key) {
69
+ this.parent = parent;
70
+ this.key = key;
71
+ }
72
+ inferMediaType() {
73
+ return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
74
+ }
75
+ inferFromSiblingMime() {
76
+ if (!this.parent) return undefined;
77
+ for (const hint of MIME_HINT_KEYS) {
78
+ const v = this.parent[hint];
79
+ if (typeof v === 'string') return v;
80
+ }
81
+ return undefined;
152
82
  }
153
- return item;
154
- };
155
- const sanitizeAnthropicImage = item => {
156
- if (isMultimodalEnabled()) return item;
157
- if (!isObject(item)) return item;
158
- // Handle Anthropic's image and document formats (same structure, different type field)
159
- if ((item.type === 'image' || item.type === 'document') && 'source' in item && isObject(item.source) && item.source.type === 'base64' && 'data' in item.source) {
160
- return {
161
- ...item,
162
- source: {
163
- ...item.source,
164
- data: REDACTED_IMAGE_PLACEHOLDER
165
- }
166
- };
83
+ inferFromSiblingFormat() {
84
+ if (!this.parent) return undefined;
85
+ const fmt = this.parent.format;
86
+ if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {
87
+ return `audio/${fmt.toLowerCase()}`;
88
+ }
89
+ return undefined;
167
90
  }
168
- return item;
169
- };
170
- const sanitizeGeminiPart = part => {
171
- if (isMultimodalEnabled()) return part;
172
- if (!isObject(part)) return part;
173
- // Handle Gemini's inline data format (images, audio, PDFs all use inlineData)
174
- if ('inlineData' in part && isObject(part.inlineData) && 'data' in part.inlineData) {
175
- return {
176
- ...part,
177
- inlineData: {
178
- ...part.inlineData,
179
- data: REDACTED_IMAGE_PLACEHOLDER
180
- }
181
- };
91
+ inferFromParentType() {
92
+ if (!this.parent) return undefined;
93
+ const t = this.parent.type;
94
+ if (typeof t !== 'string') return undefined;
95
+ if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image';
96
+ if (t === 'audio' || t === 'input_audio') return 'audio';
97
+ if (t === 'video' || t === 'video_url') return 'video';
98
+ if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream';
99
+ return undefined;
182
100
  }
183
- return part;
184
- };
185
- const processGeminiItem = item => {
186
- if (!isObject(item)) return item;
187
- // If it has parts, process them
188
- if ('parts' in item && item.parts) {
189
- const parts = Array.isArray(item.parts) ? item.parts.map(sanitizeGeminiPart) : sanitizeGeminiPart(item.parts);
190
- return {
191
- ...item,
192
- parts
193
- };
101
+ inferFromKey() {
102
+ if (!this.key) return undefined;
103
+ const key = this.key.toLowerCase();
104
+ if (key.includes('audio')) return 'audio';
105
+ if (key.includes('video')) return 'video';
106
+ if (key.includes('image')) return 'image';
107
+ if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
108
+ return undefined;
194
109
  }
195
- return item;
196
- };
197
- const sanitizeLangChainImage = item => {
198
- if (!isObject(item)) return item;
199
- // OpenAI style
200
- if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
201
- return {
202
- ...item,
203
- image_url: {
204
- ...item.image_url,
205
- url: redactBase64DataUrl(item.image_url.url)
110
+ signalsBinary() {
111
+ if (this.parent) {
112
+ for (const hint of MIME_HINT_KEYS) {
113
+ if (typeof this.parent[hint] === 'string') return true;
206
114
  }
207
- };
115
+ const fmt = this.parent.format;
116
+ if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
117
+ const t = this.parent.type;
118
+ if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true;
119
+ }
120
+ if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
121
+ return false;
208
122
  }
209
- // Direct image with data field
210
- if (item.type === 'image' && 'data' in item) {
211
- return {
212
- ...item,
213
- data: redactBase64DataUrl(item.data)
214
- };
123
+ }
124
+ MediaTypeContext.EMPTY = new MediaTypeContext(undefined, undefined);
125
+
126
+ const STRONG_CONTEXT_MIN_LENGTH = 64;
127
+ const WEAK_CONTEXT_MIN_LENGTH = 1024;
128
+ class BinaryContentRedactor {
129
+ constructor(recognizer = new Base64Recognizer()) {
130
+ this.recognizer = recognizer;
131
+ this.visited = new WeakSet();
132
+ }
133
+ redact(value) {
134
+ if (this.isMultimodalEnabled()) return value;
135
+ this.visited = new WeakSet();
136
+ return this.walk(value, MediaTypeContext.EMPTY);
137
+ }
138
+ walk(value, ctx) {
139
+ if (value === null || value === undefined) return value;
140
+ if (typeof value === 'string') return this.redactString(value, ctx);
141
+ if (typeof value !== 'object') return value;
142
+ // Buffer extends Uint8Array, so this branch catches both.
143
+ if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
144
+ return this.placeholderFor(ctx.inferMediaType());
145
+ }
146
+ if (this.visited.has(value)) return null;
147
+ this.visited.add(value);
148
+ if (Array.isArray(value)) {
149
+ return value.map(item => this.walk(item, ctx));
150
+ }
151
+ const obj = value;
152
+ const out = {};
153
+ for (const k of Object.keys(obj)) {
154
+ out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
155
+ }
156
+ return out;
157
+ }
158
+ redactString(value, ctx) {
159
+ const minLength = ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
160
+ const recognition = this.recognizer.recognize(value, minLength);
161
+ switch (recognition.kind) {
162
+ case 'data-url':
163
+ return this.placeholderFor(recognition.mediaType);
164
+ case 'raw':
165
+ return this.placeholderFor(ctx.inferMediaType());
166
+ case 'none':
167
+ return value;
168
+ }
215
169
  }
216
- // Anthropic style
217
- if (item.type === 'image' && 'source' in item && isObject(item.source) && 'data' in item.source) {
218
- if (isMultimodalEnabled()) return item;
219
- return {
220
- ...item,
221
- source: {
222
- ...item.source,
223
- data: redactBase64DataUrl(item.source.data)
224
- }
225
- };
170
+ placeholderFor(mediaType) {
171
+ if (!mediaType) return '[base64 redacted]';
172
+ if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
173
+ return `[base64 ${mediaType} redacted]`;
226
174
  }
227
- // Google style
228
- if (item.type === 'media' && 'data' in item) {
229
- return {
230
- ...item,
231
- data: redactBase64DataUrl(item.data)
232
- };
175
+ isMultimodalEnabled() {
176
+ const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
177
+ return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
233
178
  }
234
- return item;
235
- };
236
- // Export individual sanitizers for tree-shaking
237
- const sanitizeOpenAI = data => {
238
- return processMessages(data, sanitizeOpenAIImage);
239
- };
240
- const sanitizeOpenAIResponse = data => {
241
- return processMessages(data, sanitizeOpenAIResponseImage);
242
- };
243
- const sanitizeAnthropic = data => {
244
- return processMessages(data, sanitizeAnthropicImage);
245
- };
246
- const sanitizeGemini = data => {
247
- // Gemini has a different structure with 'parts' directly on items instead of 'content'
248
- // So we need custom processing instead of using processMessages
249
- if (!data) return data;
250
- if (Array.isArray(data)) {
251
- return data.map(processGeminiItem);
252
- }
253
- return processGeminiItem(data);
254
- };
255
- const sanitizeLangChain = data => {
256
- return processMessages(data, sanitizeLangChainImage);
257
- };
179
+ }
180
+
181
+ const redactor = new BinaryContentRedactor();
182
+ function redactBase64DataUrl(str) {
183
+ return redactor.redact(str);
184
+ }
185
+ const sanitizeOpenAI = data => redactor.redact(data);
186
+ const sanitizeOpenAIResponse = data => redactor.redact(data);
187
+ const sanitizeAnthropic = data => redactor.redact(data);
188
+ const sanitizeGemini = data => redactor.redact(data);
189
+ const sanitizeLangChain = data => redactor.redact(data);
258
190
 
259
191
  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']);
260
192
  function getTokensSource(posthogProperties) {
@@ -801,7 +733,7 @@ function formatOpenAIResponsesInput(input, instructions) {
801
733
  return messages;
802
734
  }
803
735
 
804
- var version = "7.17.4";
736
+ var version = "7.18.0";
805
737
 
806
738
  /**
807
739
  * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.