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