@posthog/ai 8.9.2 → 8.10.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/adk/index.cjs +1267 -0
- package/dist/adk/index.cjs.map +1 -0
- package/dist/adk/index.d.ts +149 -0
- package/dist/adk/index.mjs +1265 -0
- package/dist/adk/index.mjs.map +1 -0
- package/dist/anthropic/index.cjs +9 -2
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.mjs +9 -2
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/gemini/index.cjs +51 -51
- package/dist/gemini/index.cjs.map +1 -1
- package/dist/gemini/index.mjs +51 -51
- package/dist/gemini/index.mjs.map +1 -1
- package/dist/index.cjs +9 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.mjs +9 -2
- package/dist/index.mjs.map +1 -1
- package/dist/langchain/index.cjs +6 -2
- package/dist/langchain/index.cjs.map +1 -1
- package/dist/langchain/index.mjs +6 -2
- package/dist/langchain/index.mjs.map +1 -1
- package/dist/langchain/middleware/index.cjs +6 -2
- package/dist/langchain/middleware/index.cjs.map +1 -1
- package/dist/langchain/middleware/index.mjs +6 -2
- package/dist/langchain/middleware/index.mjs.map +1 -1
- package/dist/openai/index.cjs +9 -2
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.mjs +9 -2
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openai-agents/index.cjs +1 -1
- package/dist/openai-agents/index.cjs.map +1 -1
- package/dist/openai-agents/index.mjs +1 -1
- package/dist/openai-agents/index.mjs.map +1 -1
- package/dist/vercel/index.cjs +9 -2
- package/dist/vercel/index.cjs.map +1 -1
- package/dist/vercel/index.mjs +9 -2
- package/dist/vercel/index.mjs.map +1 -1
- package/package.json +13 -3
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var adk = require('@google/adk');
|
|
4
|
+
var uuid = require('uuid');
|
|
5
|
+
var core = require('@posthog/core');
|
|
6
|
+
|
|
7
|
+
var version = "8.10.0";
|
|
8
|
+
|
|
9
|
+
/** @internal */
|
|
10
|
+
|
|
11
|
+
/** @internal */
|
|
12
|
+
|
|
13
|
+
/** @internal */
|
|
14
|
+
function isFullAiCaptureEnabled(client) {
|
|
15
|
+
return client?.enableFullAiCapture === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @internal */
|
|
19
|
+
function captureAiEvent(client, event) {
|
|
20
|
+
if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
|
|
21
|
+
client.captureAi(event);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
client.capture(event);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @internal */
|
|
28
|
+
async function captureAiEventImmediate(client, event) {
|
|
29
|
+
if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
|
|
30
|
+
await client.captureAiImmediate(event);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
await client.captureImmediate(event);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
|
|
37
|
+
const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
|
|
38
|
+
class Base64Recognizer {
|
|
39
|
+
recognize(value, minLength) {
|
|
40
|
+
const dataUrl = DATA_URL_PREFIX_RE.exec(value);
|
|
41
|
+
if (dataUrl) return {
|
|
42
|
+
kind: 'data-url',
|
|
43
|
+
mediaType: dataUrl[1]
|
|
44
|
+
};
|
|
45
|
+
if (value.length < minLength) return {
|
|
46
|
+
kind: 'none'
|
|
47
|
+
};
|
|
48
|
+
const confidencePrefix = value.slice(0, minLength);
|
|
49
|
+
if (BASE64_ALPHABET_RE.test(confidencePrefix)) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'raw'
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
return {
|
|
55
|
+
kind: 'none'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'];
|
|
62
|
+
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']);
|
|
63
|
+
const STRONG_CONTEXT_TYPES = new Set(['image', 'image_url', 'input_image', 'audio', 'input_audio', 'video', 'video_url', 'file', 'input_file', 'document', 'media', 'file-data']);
|
|
64
|
+
const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data']);
|
|
65
|
+
const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
|
|
66
|
+
class MediaTypeContext {
|
|
67
|
+
static EMPTY = new MediaTypeContext(undefined, undefined);
|
|
68
|
+
constructor(parent, key, explicitMediaType) {
|
|
69
|
+
this.parent = parent;
|
|
70
|
+
this.key = key;
|
|
71
|
+
this.explicitMediaType = explicitMediaType;
|
|
72
|
+
}
|
|
73
|
+
inferMediaType() {
|
|
74
|
+
return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
|
|
75
|
+
}
|
|
76
|
+
inferFromSiblingMime() {
|
|
77
|
+
if (this.explicitMediaType) return this.explicitMediaType;
|
|
78
|
+
if (!this.parent) return undefined;
|
|
79
|
+
for (const hint of MIME_HINT_KEYS) {
|
|
80
|
+
const v = this.parent[hint];
|
|
81
|
+
if (typeof v === 'string') return v;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
inferFromSiblingFormat() {
|
|
86
|
+
if (!this.parent) return undefined;
|
|
87
|
+
const fmt = this.parent.format;
|
|
88
|
+
if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {
|
|
89
|
+
return `audio/${fmt.toLowerCase()}`;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
inferFromParentType() {
|
|
94
|
+
if (!this.parent) return undefined;
|
|
95
|
+
const t = this.parent.type;
|
|
96
|
+
if (typeof t !== 'string') return undefined;
|
|
97
|
+
if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image';
|
|
98
|
+
if (t === 'audio' || t === 'input_audio') return 'audio';
|
|
99
|
+
if (t === 'video' || t === 'video_url') return 'video';
|
|
100
|
+
if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream';
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
inferFromKey() {
|
|
104
|
+
if (!this.key) return undefined;
|
|
105
|
+
const key = this.key.toLowerCase();
|
|
106
|
+
if (key.includes('audio')) return 'audio';
|
|
107
|
+
if (key.includes('video')) return 'video';
|
|
108
|
+
if (key.includes('image')) return 'image';
|
|
109
|
+
if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
hasExplicitBinaryMediaType() {
|
|
113
|
+
if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
|
|
114
|
+
const mediaType = this.inferFromSiblingMime();
|
|
115
|
+
return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/');
|
|
116
|
+
}
|
|
117
|
+
signalsBinary() {
|
|
118
|
+
if (this.explicitMediaType) return true;
|
|
119
|
+
if (this.parent) {
|
|
120
|
+
for (const hint of MIME_HINT_KEYS) {
|
|
121
|
+
if (typeof this.parent[hint] === 'string') return true;
|
|
122
|
+
}
|
|
123
|
+
const fmt = this.parent.format;
|
|
124
|
+
if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
|
|
125
|
+
const t = this.parent.type;
|
|
126
|
+
if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true;
|
|
127
|
+
}
|
|
128
|
+
if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const STRONG_CONTEXT_MIN_LENGTH = 64;
|
|
134
|
+
const WEAK_CONTEXT_MIN_LENGTH = 1024;
|
|
135
|
+
class BinaryContentRedactor {
|
|
136
|
+
visited = new WeakSet();
|
|
137
|
+
constructor(recognizer = new Base64Recognizer()) {
|
|
138
|
+
this.recognizer = recognizer;
|
|
139
|
+
}
|
|
140
|
+
redact(value, mediaType) {
|
|
141
|
+
this.visited = new WeakSet();
|
|
142
|
+
return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
|
|
143
|
+
}
|
|
144
|
+
walk(value, ctx) {
|
|
145
|
+
if (value === null || value === undefined) return value;
|
|
146
|
+
if (typeof value === 'string') return this.redactString(value, ctx);
|
|
147
|
+
if (typeof value !== 'object') return value;
|
|
148
|
+
|
|
149
|
+
// Buffer extends Uint8Array, so this branch catches both.
|
|
150
|
+
if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
|
|
151
|
+
return this.placeholderFor(ctx.inferMediaType());
|
|
152
|
+
}
|
|
153
|
+
if (this.visited.has(value)) return null;
|
|
154
|
+
this.visited.add(value);
|
|
155
|
+
if (Array.isArray(value)) {
|
|
156
|
+
return value.map(item => this.walk(item, ctx));
|
|
157
|
+
}
|
|
158
|
+
const obj = value;
|
|
159
|
+
const out = {};
|
|
160
|
+
for (const k of Object.keys(obj)) {
|
|
161
|
+
out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
redactString(value, ctx) {
|
|
166
|
+
const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
|
|
167
|
+
const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, '') : value;
|
|
168
|
+
const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
|
|
169
|
+
const recognition = this.recognizer.recognize(recognitionValue, minLength);
|
|
170
|
+
switch (recognition.kind) {
|
|
171
|
+
case 'data-url':
|
|
172
|
+
return this.placeholderFor(recognition.mediaType);
|
|
173
|
+
case 'raw':
|
|
174
|
+
return this.placeholderFor(ctx.inferMediaType());
|
|
175
|
+
case 'none':
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
placeholderFor(mediaType) {
|
|
180
|
+
if (!mediaType) return '[base64 redacted]';
|
|
181
|
+
if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
|
|
182
|
+
return `[base64 ${mediaType} redacted]`;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const redactor = new BinaryContentRedactor();
|
|
187
|
+
function redactBase64DataUrl(str, mediaType) {
|
|
188
|
+
return redactor.redact(str, mediaType);
|
|
189
|
+
}
|
|
190
|
+
const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
|
|
191
|
+
const sanitizeGemini = (data, client) => sanitize(data, client);
|
|
192
|
+
|
|
193
|
+
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']);
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Whether the caller supplied their own token counts, which override the ones the SDK
|
|
197
|
+
* derived from the provider response.
|
|
198
|
+
*/
|
|
199
|
+
function hasTokenOverrides(posthogProperties) {
|
|
200
|
+
return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
|
|
201
|
+
}
|
|
202
|
+
function getTokensSource(posthogProperties) {
|
|
203
|
+
return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
|
|
204
|
+
}
|
|
205
|
+
const STRING_FORMAT = 'utf8';
|
|
206
|
+
|
|
207
|
+
// Reused across calls to avoid per-invocation allocation; truncate() runs
|
|
208
|
+
// hundreds of times for prompts with many parts.
|
|
209
|
+
new TextEncoder();
|
|
210
|
+
new TextDecoder(STRING_FORMAT, {
|
|
211
|
+
fatal: false
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Safely converts content to a string, preserving structure for objects/arrays.
|
|
216
|
+
* - If content is already a string, returns it as-is
|
|
217
|
+
* - If content is an object or array, stringifies it with JSON.stringify to preserve structure
|
|
218
|
+
* - Otherwise, converts to string with String()
|
|
219
|
+
*
|
|
220
|
+
* This prevents the "[object Object]" bug when objects are naively converted to strings.
|
|
221
|
+
*
|
|
222
|
+
* @param content - The content to convert to a string
|
|
223
|
+
* @returns A string representation that preserves structure for complex types
|
|
224
|
+
*/
|
|
225
|
+
function toContentString(content) {
|
|
226
|
+
if (typeof content === 'string') {
|
|
227
|
+
return content;
|
|
228
|
+
}
|
|
229
|
+
if (content !== undefined && content !== null && typeof content === 'object') {
|
|
230
|
+
try {
|
|
231
|
+
return JSON.stringify(content);
|
|
232
|
+
} catch {
|
|
233
|
+
// Fallback for circular refs, BigInt, or objects with throwing toJSON
|
|
234
|
+
return String(content);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return String(content);
|
|
238
|
+
}
|
|
239
|
+
const buildInlineDataBlock = (mimeType, data) => {
|
|
240
|
+
if (mimeType.startsWith('audio/')) {
|
|
241
|
+
return {
|
|
242
|
+
type: 'audio',
|
|
243
|
+
mime_type: mimeType,
|
|
244
|
+
data
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
if (mimeType.startsWith('image/')) {
|
|
248
|
+
return {
|
|
249
|
+
type: 'image',
|
|
250
|
+
inline_data: {
|
|
251
|
+
mime_type: mimeType,
|
|
252
|
+
data
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
type: 'document',
|
|
258
|
+
inline_data: {
|
|
259
|
+
mime_type: mimeType,
|
|
260
|
+
data
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
const formatInlineDataBlock = (inlineData, client) => {
|
|
265
|
+
const mimeType = inlineData.mimeType || inlineData.mime_type || 'application/octet-stream';
|
|
266
|
+
let data = inlineData.data;
|
|
267
|
+
if (data instanceof Uint8Array) {
|
|
268
|
+
if (typeof Buffer !== 'undefined') {
|
|
269
|
+
data = Buffer.from(data).toString('base64');
|
|
270
|
+
} else {
|
|
271
|
+
let binary = '';
|
|
272
|
+
for (let i = 0; i < data.length; i++) {
|
|
273
|
+
binary += String.fromCharCode(data[i]);
|
|
274
|
+
}
|
|
275
|
+
data = btoa(binary);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
data = isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mimeType);
|
|
279
|
+
return buildInlineDataBlock(mimeType, String(data ?? ''));
|
|
280
|
+
};
|
|
281
|
+
const formatResponseGemini = (response, client) => {
|
|
282
|
+
const output = [];
|
|
283
|
+
if (response.candidates && Array.isArray(response.candidates)) {
|
|
284
|
+
for (const candidate of response.candidates) {
|
|
285
|
+
if (candidate.content && candidate.content.parts) {
|
|
286
|
+
const content = [];
|
|
287
|
+
for (const part of candidate.content.parts) {
|
|
288
|
+
if (part.text) {
|
|
289
|
+
content.push({
|
|
290
|
+
type: 'text',
|
|
291
|
+
text: part.text
|
|
292
|
+
});
|
|
293
|
+
} else if (part.functionCall) {
|
|
294
|
+
content.push({
|
|
295
|
+
type: 'function',
|
|
296
|
+
function: {
|
|
297
|
+
name: part.functionCall.name,
|
|
298
|
+
arguments: part.functionCall.args
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
} else if (part.inlineData) {
|
|
302
|
+
content.push(formatInlineDataBlock(part.inlineData, client));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (content.length > 0) {
|
|
306
|
+
output.push({
|
|
307
|
+
role: 'assistant',
|
|
308
|
+
content
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} else if (candidate.text) {
|
|
312
|
+
output.push({
|
|
313
|
+
role: 'assistant',
|
|
314
|
+
content: [{
|
|
315
|
+
type: 'text',
|
|
316
|
+
text: candidate.text
|
|
317
|
+
}]
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
} else if (response.text) {
|
|
322
|
+
output.push({
|
|
323
|
+
role: 'assistant',
|
|
324
|
+
content: [{
|
|
325
|
+
type: 'text',
|
|
326
|
+
text: response.text
|
|
327
|
+
}]
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return output;
|
|
331
|
+
};
|
|
332
|
+
const withPrivacyMode = (client, privacyMode, input) => {
|
|
333
|
+
return client.privacy_mode || privacyMode ? null : input;
|
|
334
|
+
};
|
|
335
|
+
let AIEvent = /*#__PURE__*/function (AIEvent) {
|
|
336
|
+
AIEvent["Generation"] = "$ai_generation";
|
|
337
|
+
AIEvent["Embedding"] = "$ai_embedding";
|
|
338
|
+
return AIEvent;
|
|
339
|
+
}({});
|
|
340
|
+
function sanitizeValues(obj) {
|
|
341
|
+
if (obj === undefined || obj === null) {
|
|
342
|
+
return obj;
|
|
343
|
+
}
|
|
344
|
+
const jsonSafe = JSON.parse(JSON.stringify(obj));
|
|
345
|
+
if (typeof jsonSafe === 'string') {
|
|
346
|
+
// Sanitize lone surrogates by round-tripping through UTF-8
|
|
347
|
+
return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
|
|
348
|
+
} else if (Array.isArray(jsonSafe)) {
|
|
349
|
+
return jsonSafe.map(sanitizeValues);
|
|
350
|
+
} else if (jsonSafe && typeof jsonSafe === 'object') {
|
|
351
|
+
return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
|
|
352
|
+
}
|
|
353
|
+
return jsonSafe;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
357
|
+
const MAX_STACK_LINES = 20;
|
|
358
|
+
function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
|
|
359
|
+
if (depth < 0 || value === null || typeof value !== 'object') {
|
|
360
|
+
return value;
|
|
361
|
+
}
|
|
362
|
+
if (value instanceof Error) {
|
|
363
|
+
const out = {
|
|
364
|
+
name: value.name,
|
|
365
|
+
message: value.message,
|
|
366
|
+
stack: truncateStack(value.stack)
|
|
367
|
+
};
|
|
368
|
+
for (const key of Object.keys(value)) {
|
|
369
|
+
out[key] = serializeError(value[key], depth - 1);
|
|
370
|
+
}
|
|
371
|
+
if (value.cause !== undefined) {
|
|
372
|
+
out.cause = serializeError(value.cause, depth - 1);
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
if (Array.isArray(value)) {
|
|
377
|
+
return value.map(item => serializeError(item, depth - 1));
|
|
378
|
+
}
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
function stringifyError(error) {
|
|
382
|
+
try {
|
|
383
|
+
return JSON.stringify(sanitizeValues(serializeError(error)));
|
|
384
|
+
} catch {
|
|
385
|
+
if (error instanceof Error) {
|
|
386
|
+
return JSON.stringify({
|
|
387
|
+
name: error.name,
|
|
388
|
+
message: error.message
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return JSON.stringify({
|
|
392
|
+
message: String(error)
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function truncateStack(stack) {
|
|
397
|
+
if (!stack) {
|
|
398
|
+
return stack;
|
|
399
|
+
}
|
|
400
|
+
const lines = stack.split('\n');
|
|
401
|
+
if (lines.length <= MAX_STACK_LINES) {
|
|
402
|
+
return stack;
|
|
403
|
+
}
|
|
404
|
+
return [...lines.slice(0, MAX_STACK_LINES), '... (truncated)'].join('\n');
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway
|
|
408
|
+
// emits its own $ai_generation, so each call would be captured (and, for billable
|
|
409
|
+
// products, billed) twice. We only warn — the wrapper's event carries data the
|
|
410
|
+
// gateway never sees (groups, custom properties, trace hierarchy).
|
|
411
|
+
|
|
412
|
+
// Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the
|
|
413
|
+
// main repo). gateway.us.posthog.com is live today; the rest are listed ahead of
|
|
414
|
+
// any traffic moving to them.
|
|
415
|
+
const POSTHOG_AI_GATEWAY_HOSTS = ['gateway.posthog.com', 'gateway.us.posthog.com', 'gateway.eu.posthog.com', 'ai-gateway.us.posthog.com', 'ai-gateway.eu.posthog.com'];
|
|
416
|
+
|
|
417
|
+
// Swap for the dedicated AI Gateway page once it ships.
|
|
418
|
+
const GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability';
|
|
419
|
+
const extractHost = baseURL => {
|
|
420
|
+
try {
|
|
421
|
+
// Tolerate bare hosts that omit a scheme, e.g. "gateway.us.posthog.com/v1".
|
|
422
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
|
|
423
|
+
return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
|
|
424
|
+
} catch {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const isPostHogAiGatewayUrl = baseURL => {
|
|
429
|
+
if (!baseURL) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
const host = extractHost(baseURL);
|
|
433
|
+
return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
// Warns on every gateway call by design: the misconfiguration is impossible to
|
|
437
|
+
// miss that way, and a doubled bill is worse than noisy logs.
|
|
438
|
+
const warnIfPostHogAiGateway = baseURL => {
|
|
439
|
+
if (!isPostHogAiGatewayUrl(baseURL)) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
console.warn('[PostHog] The PostHog AI wrapper is pointed at the PostHog AI Gateway. ' + 'Both capture $ai_generation, so every call is double-counted and double-billed. ' + `Use one or the other — see ${GATEWAY_DOCS_URL}.`);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Options for `captureAiGeneration`. Mirrors the `$ai_generation` event shape
|
|
447
|
+
* directly so that any caller — first-party SDK wrappers and external code
|
|
448
|
+
* alike — produces an identical event.
|
|
449
|
+
*/
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
|
|
453
|
+
*
|
|
454
|
+
* This is the canonical primitive that every `@posthog/ai` wrapper
|
|
455
|
+
* (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
|
|
456
|
+
* external code can use it directly to instrument LLM calls made through
|
|
457
|
+
* arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
|
|
458
|
+
* same events the SDK wrappers produce.
|
|
459
|
+
*
|
|
460
|
+
* When `error` is set, the event is captured as an error. If the error is an
|
|
461
|
+
* object, it is mutated in place to set `__posthog_previously_captured_error`
|
|
462
|
+
* so callers can re-throw the original error reference safely.
|
|
463
|
+
*/
|
|
464
|
+
const captureAiGeneration = async (client, options) => {
|
|
465
|
+
try {
|
|
466
|
+
if (!client.capture) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
warnIfPostHogAiGateway(options.baseURL);
|
|
470
|
+
const traceId = options.traceId ?? uuid.v4();
|
|
471
|
+
const eventType = options.eventType ?? AIEvent.Generation;
|
|
472
|
+
const privacyMode = options.privacyMode ?? false;
|
|
473
|
+
const usage = options.usage ?? {};
|
|
474
|
+
|
|
475
|
+
// Check privacy before reading or traversing input/output. Besides avoiding
|
|
476
|
+
// needless work, this ensures hostile getters/proxies cannot observe a value
|
|
477
|
+
// that the caller explicitly requested us to redact.
|
|
478
|
+
const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
|
|
479
|
+
const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
|
|
480
|
+
const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
|
|
481
|
+
let httpStatus = options.httpStatus;
|
|
482
|
+
let errorData = {};
|
|
483
|
+
if (options.error) {
|
|
484
|
+
if (httpStatus === undefined) {
|
|
485
|
+
if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
|
|
486
|
+
httpStatus = options.error.status;
|
|
487
|
+
} else if (typeof options.error === 'object' && 'statusCode' in options.error && typeof options.error.statusCode === 'number') {
|
|
488
|
+
httpStatus = options.error.statusCode;
|
|
489
|
+
} else {
|
|
490
|
+
httpStatus = 500;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
let exceptionId;
|
|
494
|
+
if (client.options?.enableExceptionAutocapture) {
|
|
495
|
+
exceptionId = core.uuidv7();
|
|
496
|
+
client.captureException(options.error, undefined, {
|
|
497
|
+
$ai_trace_id: traceId
|
|
498
|
+
}, exceptionId);
|
|
499
|
+
if (typeof options.error === 'object') {
|
|
500
|
+
;
|
|
501
|
+
options.error.__posthog_previously_captured_error = true;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
errorData = {
|
|
505
|
+
$ai_is_error: true,
|
|
506
|
+
$ai_error: stringifyError(options.error),
|
|
507
|
+
$exception_event_id: exceptionId
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
httpStatus = httpStatus ?? 200;
|
|
511
|
+
|
|
512
|
+
// A configured price applies only to a count the provider reported, so a call with no
|
|
513
|
+
// reported usage sends no cost instead of asserting $0. $ai_total_cost_usd sums the sides
|
|
514
|
+
// that were priced, which makes it the cost of the known side alone when the other side
|
|
515
|
+
// went unreported: a lower bound on the true total, not an assertion of it.
|
|
516
|
+
const costOverrideData = {};
|
|
517
|
+
if (options.costOverride) {
|
|
518
|
+
if (usage.inputTokens !== undefined) {
|
|
519
|
+
costOverrideData.$ai_input_cost_usd = (options.costOverride.inputCost ?? 0) * usage.inputTokens;
|
|
520
|
+
}
|
|
521
|
+
if (usage.outputTokens !== undefined) {
|
|
522
|
+
costOverrideData.$ai_output_cost_usd = (options.costOverride.outputCost ?? 0) * usage.outputTokens;
|
|
523
|
+
}
|
|
524
|
+
if (Object.keys(costOverrideData).length > 0) {
|
|
525
|
+
costOverrideData.$ai_total_cost_usd = (costOverrideData.$ai_input_cost_usd ?? 0) + (costOverrideData.$ai_output_cost_usd ?? 0);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// The caller's own token counts override the SDK-derived ones further down, via the
|
|
530
|
+
// `options.properties` spread.
|
|
531
|
+
const tokensOverridden = hasTokenOverrides(options.properties);
|
|
532
|
+
const additionalTokenValues = {
|
|
533
|
+
...(usage.reasoningTokens ? {
|
|
534
|
+
$ai_reasoning_tokens: usage.reasoningTokens
|
|
535
|
+
} : {}),
|
|
536
|
+
...(usage.cacheReadInputTokens ? {
|
|
537
|
+
$ai_cache_read_input_tokens: usage.cacheReadInputTokens
|
|
538
|
+
} : {}),
|
|
539
|
+
...(usage.cacheCreationInputTokens ? {
|
|
540
|
+
$ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
|
|
541
|
+
} : {}),
|
|
542
|
+
// Checked against undefined rather than truthiness, because false is the meaningful
|
|
543
|
+
// value here and a truthiness guard would drop it.
|
|
544
|
+
//
|
|
545
|
+
// Dropped entirely when the caller overrides the token counts: the flag describes how
|
|
546
|
+
// the SDK-derived counts relate to each other, so against passthrough counts it can be
|
|
547
|
+
// wrong in the expensive direction. Declaring inclusive over counts that are actually
|
|
548
|
+
// exclusive makes ingestion subtract the cache pool that was never in the input. A
|
|
549
|
+
// caller who knows their own accounting model can still pass
|
|
550
|
+
// `$ai_cache_reporting_exclusive` themselves, and that value wins.
|
|
551
|
+
...(usage.cacheReportingExclusive !== undefined && !tokensOverridden ? {
|
|
552
|
+
$ai_cache_reporting_exclusive: usage.cacheReportingExclusive
|
|
553
|
+
} : {}),
|
|
554
|
+
...(usage.webSearchCount ? {
|
|
555
|
+
$ai_web_search_count: usage.webSearchCount
|
|
556
|
+
} : {}),
|
|
557
|
+
...(usage.rawUsage ? {
|
|
558
|
+
$ai_usage: usage.rawUsage
|
|
559
|
+
} : {})
|
|
560
|
+
};
|
|
561
|
+
const properties = {
|
|
562
|
+
$ai_lib: 'posthog-ai',
|
|
563
|
+
$ai_lib_version: version,
|
|
564
|
+
$ai_provider: options.providerOverride ?? options.provider,
|
|
565
|
+
$ai_model: options.modelOverride ?? options.model,
|
|
566
|
+
$ai_model_parameters: options.modelParameters ?? {},
|
|
567
|
+
$ai_input: safeInput,
|
|
568
|
+
$ai_output_choices: safeOutput,
|
|
569
|
+
$ai_http_status: httpStatus,
|
|
570
|
+
...(usage.inputTokens !== undefined ? {
|
|
571
|
+
$ai_input_tokens: usage.inputTokens
|
|
572
|
+
} : {}),
|
|
573
|
+
...(usage.outputTokens !== undefined ? {
|
|
574
|
+
$ai_output_tokens: usage.outputTokens
|
|
575
|
+
} : {}),
|
|
576
|
+
...additionalTokenValues,
|
|
577
|
+
...(options.latency !== undefined ? {
|
|
578
|
+
$ai_latency: options.latency
|
|
579
|
+
} : {}),
|
|
580
|
+
...(options.timeToFirstToken !== undefined ? {
|
|
581
|
+
$ai_time_to_first_token: options.timeToFirstToken
|
|
582
|
+
} : {}),
|
|
583
|
+
$ai_trace_id: traceId,
|
|
584
|
+
...(options.baseURL === null ? {} : {
|
|
585
|
+
$ai_base_url: options.baseURL ?? ''
|
|
586
|
+
}),
|
|
587
|
+
...options.properties,
|
|
588
|
+
$ai_tokens_source: getTokensSource(options.properties),
|
|
589
|
+
...(options.distinctId ? {} : {
|
|
590
|
+
$process_person_profile: false
|
|
591
|
+
}),
|
|
592
|
+
...(options.stopReason ? {
|
|
593
|
+
$ai_stop_reason: options.stopReason
|
|
594
|
+
} : {}),
|
|
595
|
+
...(options.tools ? {
|
|
596
|
+
$ai_tools: options.tools
|
|
597
|
+
} : {}),
|
|
598
|
+
...(options.completionId ? {
|
|
599
|
+
$ai_completion_id: options.completionId
|
|
600
|
+
} : {}),
|
|
601
|
+
...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
|
|
602
|
+
$ai_provider_metadata: options.providerMetadata
|
|
603
|
+
} : {}),
|
|
604
|
+
...errorData,
|
|
605
|
+
...costOverrideData
|
|
606
|
+
};
|
|
607
|
+
const event = {
|
|
608
|
+
distinctId: options.distinctId ?? traceId,
|
|
609
|
+
event: eventType,
|
|
610
|
+
properties,
|
|
611
|
+
groups: options.groups
|
|
612
|
+
};
|
|
613
|
+
if (options.captureImmediate) {
|
|
614
|
+
await captureAiEventImmediate(client, event);
|
|
615
|
+
} else {
|
|
616
|
+
captureAiEvent(client, event);
|
|
617
|
+
}
|
|
618
|
+
} catch (error) {
|
|
619
|
+
// Telemetry failures must never affect the instrumented provider call.
|
|
620
|
+
try {
|
|
621
|
+
options.onError?.(error);
|
|
622
|
+
} catch {
|
|
623
|
+
// Error reporting must not affect the instrumented provider call either.
|
|
624
|
+
}
|
|
625
|
+
console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
/** Map Gemini usage metadata to PostHog's provider-agnostic token fields. */
|
|
630
|
+
function mapGeminiUsage(metadata, additionalUsage = {}) {
|
|
631
|
+
return {
|
|
632
|
+
inputTokens: metadata?.promptTokenCount ?? 0,
|
|
633
|
+
outputTokens: metadata?.candidatesTokenCount ?? 0,
|
|
634
|
+
reasoningTokens: metadata?.thoughtsTokenCount ?? 0,
|
|
635
|
+
cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0,
|
|
636
|
+
// Gemini counts cachedContentTokenCount inside promptTokenCount, so declare
|
|
637
|
+
// the accounting model rather than leaving ingestion to infer it. Under
|
|
638
|
+
// explicit context caching the two measurements can differ by a few percent.
|
|
639
|
+
...(metadata?.cachedContentTokenCount ? {
|
|
640
|
+
cacheReportingExclusive: false
|
|
641
|
+
} : {}),
|
|
642
|
+
...additionalUsage,
|
|
643
|
+
rawUsage: metadata
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Resolver for the PostHog distinct ID. Either a static string, or a function
|
|
649
|
+
* that derives it from the ADK model callback context (e.g. from
|
|
650
|
+
* `context.userId`). Return `null`/`undefined` to fall back to the ADK
|
|
651
|
+
* `userId`, and finally to anonymous (personless) capture keyed by trace ID.
|
|
652
|
+
*/
|
|
653
|
+
|
|
654
|
+
/** Calls older than this are treated as abandoned rather than evicting live calls by count. */
|
|
655
|
+
const MAX_PENDING_AGE_MS = 60 * 60 * 1000;
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* A Google ADK (`@google/adk`) `BasePlugin` that captures PostHog AI traces,
|
|
659
|
+
* agent and tool spans, and a full `$ai_generation` event for every model call.
|
|
660
|
+
*
|
|
661
|
+
* Run, agent, and tool callbacks build the trace hierarchy. Model callbacks
|
|
662
|
+
* record input, output, model, token usage, latency, and finish reason through
|
|
663
|
+
* the shared {@link captureAiGeneration} primitive so PostHog derives cost from
|
|
664
|
+
* the model and tokens (never hardcoded here).
|
|
665
|
+
*
|
|
666
|
+
* ADK already emits OpenTelemetry `gen_ai.*` spans; this plugin is the
|
|
667
|
+
* complement for users who capture LLM analytics through the PostHog SDK rather
|
|
668
|
+
* than an OTEL exporter.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```typescript
|
|
672
|
+
* import { PostHogADKPlugin } from '@posthog/ai/adk'
|
|
673
|
+
* import { Runner } from '@google/adk'
|
|
674
|
+
* import { PostHog } from 'posthog-node'
|
|
675
|
+
*
|
|
676
|
+
* const phClient = new PostHog('<POSTHOG_API_KEY>')
|
|
677
|
+
*
|
|
678
|
+
* const runner = new Runner({
|
|
679
|
+
* appName: 'my-app',
|
|
680
|
+
* agent,
|
|
681
|
+
* sessionService,
|
|
682
|
+
* plugins: [new PostHogADKPlugin({ client: phClient, distinctId: 'user@example.com' })],
|
|
683
|
+
* })
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
class PostHogADKPlugin extends adk.BasePlugin {
|
|
687
|
+
/** FIFO of in-flight model calls for each invocation branch and agent. */
|
|
688
|
+
_pending = new Map();
|
|
689
|
+
_traces = new Map();
|
|
690
|
+
_pendingAgents = new Map();
|
|
691
|
+
_pendingTools = new Map();
|
|
692
|
+
constructor(options) {
|
|
693
|
+
super('posthog');
|
|
694
|
+
this._client = options.client;
|
|
695
|
+
this._distinctId = options.distinctId;
|
|
696
|
+
this._provider = options.provider ?? 'gemini';
|
|
697
|
+
this._privacyMode = options.privacyMode ?? false;
|
|
698
|
+
this._groups = options.groups;
|
|
699
|
+
this._properties = options.properties ?? {};
|
|
700
|
+
this._captureImmediate = options.captureImmediate ?? false;
|
|
701
|
+
this._onError = options.onError;
|
|
702
|
+
}
|
|
703
|
+
async beforeRunCallback({
|
|
704
|
+
invocationContext
|
|
705
|
+
}) {
|
|
706
|
+
try {
|
|
707
|
+
this._evictStalePending();
|
|
708
|
+
this._traces.set(invocationContext.invocationId, {
|
|
709
|
+
startTime: Date.now(),
|
|
710
|
+
spanId: invocationContext.invocationId,
|
|
711
|
+
name: invocationContext.agent?.name ?? 'ADK invocation',
|
|
712
|
+
input: invocationContext.userContent,
|
|
713
|
+
distinctId: this._resolveInvocationDistinctId(invocationContext),
|
|
714
|
+
sessionId: invocationContext.session?.id
|
|
715
|
+
});
|
|
716
|
+
} catch (error) {
|
|
717
|
+
this._handleError(error);
|
|
718
|
+
}
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
async afterRunCallback({
|
|
722
|
+
invocationContext
|
|
723
|
+
}) {
|
|
724
|
+
try {
|
|
725
|
+
const trace = this._traces.get(invocationContext.invocationId);
|
|
726
|
+
this._traces.delete(invocationContext.invocationId);
|
|
727
|
+
this._clearPendingInvocation(invocationContext.invocationId);
|
|
728
|
+
if (!trace) {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
await this._captureLifecycleEvent('$ai_trace', trace.distinctId, {
|
|
732
|
+
$ai_trace_id: invocationContext.invocationId,
|
|
733
|
+
$ai_span_id: trace.spanId,
|
|
734
|
+
$ai_span_name: trace.name,
|
|
735
|
+
$ai_input_state: withPrivacyMode(this._client, this._privacyMode, trace.input),
|
|
736
|
+
$ai_latency: (Date.now() - trace.startTime) / 1000,
|
|
737
|
+
...(trace.sessionId ? {
|
|
738
|
+
$ai_session_id: trace.sessionId
|
|
739
|
+
} : {})
|
|
740
|
+
});
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this._handleError(error);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async beforeAgentCallback({
|
|
746
|
+
agent,
|
|
747
|
+
callbackContext
|
|
748
|
+
}) {
|
|
749
|
+
try {
|
|
750
|
+
this._evictStalePending();
|
|
751
|
+
this._rememberContext(callbackContext);
|
|
752
|
+
const key = this._pendingKey(callbackContext);
|
|
753
|
+
const pending = {
|
|
754
|
+
startTime: Date.now(),
|
|
755
|
+
spanId: uuid.v4(),
|
|
756
|
+
name: agent.name,
|
|
757
|
+
input: callbackContext.userContent
|
|
758
|
+
};
|
|
759
|
+
const queue = this._pendingAgents.get(key);
|
|
760
|
+
if (queue) {
|
|
761
|
+
queue.push(pending);
|
|
762
|
+
} else {
|
|
763
|
+
this._pendingAgents.set(key, [pending]);
|
|
764
|
+
}
|
|
765
|
+
} catch (error) {
|
|
766
|
+
this._handleError(error);
|
|
767
|
+
}
|
|
768
|
+
return undefined;
|
|
769
|
+
}
|
|
770
|
+
async afterAgentCallback({
|
|
771
|
+
callbackContext
|
|
772
|
+
}) {
|
|
773
|
+
try {
|
|
774
|
+
const pending = this._takePendingAgent(this._pendingKey(callbackContext));
|
|
775
|
+
if (pending) {
|
|
776
|
+
await this._captureLifecycleEvent('$ai_span', this._resolveDistinctId(callbackContext), {
|
|
777
|
+
$ai_trace_id: callbackContext.invocationId,
|
|
778
|
+
$ai_span_id: pending.spanId,
|
|
779
|
+
...(this._traces.get(callbackContext.invocationId)?.spanId ? {
|
|
780
|
+
$ai_parent_id: this._traces.get(callbackContext.invocationId)?.spanId
|
|
781
|
+
} : {}),
|
|
782
|
+
$ai_span_name: pending.name,
|
|
783
|
+
$ai_input_state: withPrivacyMode(this._client, this._privacyMode, pending.input),
|
|
784
|
+
$ai_latency: (Date.now() - pending.startTime) / 1000,
|
|
785
|
+
...(callbackContext.sessionId ? {
|
|
786
|
+
$ai_session_id: callbackContext.sessionId
|
|
787
|
+
} : {}),
|
|
788
|
+
...(callbackContext.agentName ? {
|
|
789
|
+
$ai_agent_name: callbackContext.agentName
|
|
790
|
+
} : {})
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
} catch (error) {
|
|
794
|
+
this._handleError(error);
|
|
795
|
+
}
|
|
796
|
+
return undefined;
|
|
797
|
+
}
|
|
798
|
+
async beforeModelCallback({
|
|
799
|
+
callbackContext,
|
|
800
|
+
llmRequest
|
|
801
|
+
}) {
|
|
802
|
+
try {
|
|
803
|
+
this._evictStalePending();
|
|
804
|
+
this._rememberContext(callbackContext);
|
|
805
|
+
const pending = {
|
|
806
|
+
startTime: Date.now(),
|
|
807
|
+
spanId: uuid.v4(),
|
|
808
|
+
input: this._formatInput(llmRequest),
|
|
809
|
+
model: llmRequest.model,
|
|
810
|
+
modelParameters: extractModelParameters(llmRequest.config),
|
|
811
|
+
tools: extractTools(llmRequest),
|
|
812
|
+
streamedOutput: []
|
|
813
|
+
};
|
|
814
|
+
const key = this._pendingKey(callbackContext);
|
|
815
|
+
const queue = this._pending.get(key);
|
|
816
|
+
if (queue) {
|
|
817
|
+
queue.push(pending);
|
|
818
|
+
} else {
|
|
819
|
+
this._pending.set(key, [pending]);
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
this._handleError(error);
|
|
823
|
+
}
|
|
824
|
+
return undefined;
|
|
825
|
+
}
|
|
826
|
+
async afterModelCallback({
|
|
827
|
+
callbackContext,
|
|
828
|
+
llmResponse
|
|
829
|
+
}) {
|
|
830
|
+
try {
|
|
831
|
+
// Streaming delivers partial responses before the terminal one; the
|
|
832
|
+
// terminal response carries the full content and usage, so only emit then.
|
|
833
|
+
if (llmResponse.partial) {
|
|
834
|
+
return undefined;
|
|
835
|
+
}
|
|
836
|
+
const key = this._pendingKey(callbackContext);
|
|
837
|
+
const pending = this._peekPending(key);
|
|
838
|
+
if (this._isNonTerminalStreamResponse(llmResponse)) {
|
|
839
|
+
if (pending) {
|
|
840
|
+
pending.streamedOutput.push(...this._formatOutput(llmResponse));
|
|
841
|
+
}
|
|
842
|
+
return undefined;
|
|
843
|
+
}
|
|
844
|
+
const completedPending = this._takePending(key);
|
|
845
|
+
const error = llmResponse.errorCode ? new Error(llmResponse.errorMessage ?? String(llmResponse.errorCode)) : undefined;
|
|
846
|
+
const output = error ? [] : this._formatOutput(llmResponse);
|
|
847
|
+
await this._capture(callbackContext, {
|
|
848
|
+
pending: completedPending,
|
|
849
|
+
output: !error && output.length === 0 && completedPending?.streamedOutput.length ? completedPending.streamedOutput : output,
|
|
850
|
+
model: llmResponse.modelVersion ?? completedPending?.model,
|
|
851
|
+
usage: llmResponse.usageMetadata,
|
|
852
|
+
stopReason: llmResponse.finishReason ? String(llmResponse.finishReason) : undefined,
|
|
853
|
+
error
|
|
854
|
+
});
|
|
855
|
+
} catch (error) {
|
|
856
|
+
this._handleError(error);
|
|
857
|
+
}
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
async onModelErrorCallback({
|
|
861
|
+
callbackContext,
|
|
862
|
+
llmRequest,
|
|
863
|
+
error
|
|
864
|
+
}) {
|
|
865
|
+
try {
|
|
866
|
+
const pending = this._takePending(this._pendingKey(callbackContext));
|
|
867
|
+
await this._capture(callbackContext, {
|
|
868
|
+
pending: pending ?? {
|
|
869
|
+
startTime: Date.now(),
|
|
870
|
+
spanId: uuid.v4(),
|
|
871
|
+
input: this._formatInput(llmRequest),
|
|
872
|
+
model: llmRequest.model,
|
|
873
|
+
modelParameters: extractModelParameters(llmRequest.config),
|
|
874
|
+
tools: extractTools(llmRequest),
|
|
875
|
+
streamedOutput: []
|
|
876
|
+
},
|
|
877
|
+
output: [],
|
|
878
|
+
model: llmRequest.model,
|
|
879
|
+
usage: undefined,
|
|
880
|
+
error
|
|
881
|
+
});
|
|
882
|
+
} catch (captureError) {
|
|
883
|
+
this._handleError(captureError);
|
|
884
|
+
}
|
|
885
|
+
return undefined;
|
|
886
|
+
}
|
|
887
|
+
async beforeToolCallback({
|
|
888
|
+
tool,
|
|
889
|
+
toolArgs,
|
|
890
|
+
toolContext
|
|
891
|
+
}) {
|
|
892
|
+
try {
|
|
893
|
+
this._evictStalePending();
|
|
894
|
+
this._rememberContext(toolContext);
|
|
895
|
+
const key = this._toolKey(toolContext, tool.name);
|
|
896
|
+
const pending = {
|
|
897
|
+
startTime: Date.now(),
|
|
898
|
+
spanId: toolContext.functionCallId ?? uuid.v4(),
|
|
899
|
+
name: tool.name,
|
|
900
|
+
input: toolArgs
|
|
901
|
+
};
|
|
902
|
+
const queue = this._pendingTools.get(key);
|
|
903
|
+
if (queue) {
|
|
904
|
+
queue.push(pending);
|
|
905
|
+
} else {
|
|
906
|
+
this._pendingTools.set(key, [pending]);
|
|
907
|
+
}
|
|
908
|
+
} catch (error) {
|
|
909
|
+
this._handleError(error);
|
|
910
|
+
}
|
|
911
|
+
return undefined;
|
|
912
|
+
}
|
|
913
|
+
async afterToolCallback({
|
|
914
|
+
tool,
|
|
915
|
+
toolContext,
|
|
916
|
+
result
|
|
917
|
+
}) {
|
|
918
|
+
try {
|
|
919
|
+
const pending = this._takePendingTool(this._toolKey(toolContext, tool.name));
|
|
920
|
+
if (pending) {
|
|
921
|
+
await this._captureToolSpan(toolContext, pending, result);
|
|
922
|
+
}
|
|
923
|
+
} catch (error) {
|
|
924
|
+
this._handleError(error);
|
|
925
|
+
}
|
|
926
|
+
return undefined;
|
|
927
|
+
}
|
|
928
|
+
async onToolErrorCallback({
|
|
929
|
+
tool,
|
|
930
|
+
toolContext,
|
|
931
|
+
error
|
|
932
|
+
}) {
|
|
933
|
+
try {
|
|
934
|
+
const pending = this._takePendingTool(this._toolKey(toolContext, tool.name));
|
|
935
|
+
if (pending) {
|
|
936
|
+
await this._captureToolSpan(toolContext, pending, undefined, error);
|
|
937
|
+
}
|
|
938
|
+
} catch (captureError) {
|
|
939
|
+
this._handleError(captureError);
|
|
940
|
+
}
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
async _capture(callbackContext, args) {
|
|
944
|
+
const {
|
|
945
|
+
pending,
|
|
946
|
+
output,
|
|
947
|
+
model,
|
|
948
|
+
usage,
|
|
949
|
+
stopReason,
|
|
950
|
+
error
|
|
951
|
+
} = args;
|
|
952
|
+
const latency = pending ? (Date.now() - pending.startTime) / 1000 : undefined;
|
|
953
|
+
await captureAiGeneration(this._client, {
|
|
954
|
+
distinctId: this._resolveDistinctId(callbackContext),
|
|
955
|
+
traceId: callbackContext.invocationId,
|
|
956
|
+
model,
|
|
957
|
+
provider: this._provider,
|
|
958
|
+
baseURL: null,
|
|
959
|
+
input: pending?.input ?? [],
|
|
960
|
+
output,
|
|
961
|
+
latency,
|
|
962
|
+
modelParameters: pending?.modelParameters,
|
|
963
|
+
usage: mapGeminiUsage(usage),
|
|
964
|
+
stopReason,
|
|
965
|
+
tools: pending?.tools,
|
|
966
|
+
groups: this._groups,
|
|
967
|
+
privacyMode: this._privacyMode,
|
|
968
|
+
captureImmediate: this._captureImmediate,
|
|
969
|
+
onError: this._onError,
|
|
970
|
+
properties: {
|
|
971
|
+
$ai_framework: 'google-adk',
|
|
972
|
+
$ai_span_id: pending?.spanId ?? uuid.v4(),
|
|
973
|
+
...(this._parentSpanId(callbackContext) ? {
|
|
974
|
+
$ai_parent_id: this._parentSpanId(callbackContext)
|
|
975
|
+
} : {}),
|
|
976
|
+
...(callbackContext.sessionId ? {
|
|
977
|
+
$ai_session_id: callbackContext.sessionId
|
|
978
|
+
} : {}),
|
|
979
|
+
...(callbackContext.agentName ? {
|
|
980
|
+
$ai_agent_name: callbackContext.agentName,
|
|
981
|
+
$ai_span_name: callbackContext.agentName
|
|
982
|
+
} : {}),
|
|
983
|
+
...this._properties
|
|
984
|
+
},
|
|
985
|
+
error
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
async _captureToolSpan(context, pending, result, error) {
|
|
989
|
+
await this._captureLifecycleEvent('$ai_span', this._resolveDistinctId(context), {
|
|
990
|
+
$ai_trace_id: context.invocationId,
|
|
991
|
+
$ai_span_id: pending.spanId,
|
|
992
|
+
...(this._parentSpanId(context) ? {
|
|
993
|
+
$ai_parent_id: this._parentSpanId(context)
|
|
994
|
+
} : {}),
|
|
995
|
+
$ai_span_name: pending.name,
|
|
996
|
+
$ai_input_state: withPrivacyMode(this._client, this._privacyMode, pending.input),
|
|
997
|
+
...(result !== undefined ? {
|
|
998
|
+
$ai_output_state: withPrivacyMode(this._client, this._privacyMode, result)
|
|
999
|
+
} : {}),
|
|
1000
|
+
$ai_latency: (Date.now() - pending.startTime) / 1000,
|
|
1001
|
+
...(context.sessionId ? {
|
|
1002
|
+
$ai_session_id: context.sessionId
|
|
1003
|
+
} : {}),
|
|
1004
|
+
...(context.agentName ? {
|
|
1005
|
+
$ai_agent_name: context.agentName
|
|
1006
|
+
} : {}),
|
|
1007
|
+
...(error ? {
|
|
1008
|
+
$ai_is_error: true,
|
|
1009
|
+
$ai_error: stringifyError(error)
|
|
1010
|
+
} : {})
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
async _captureLifecycleEvent(event, distinctId, properties) {
|
|
1014
|
+
const message = {
|
|
1015
|
+
distinctId: distinctId ?? String(properties.$ai_trace_id),
|
|
1016
|
+
event,
|
|
1017
|
+
properties: {
|
|
1018
|
+
$ai_lib: 'posthog-ai',
|
|
1019
|
+
$ai_lib_version: version,
|
|
1020
|
+
$ai_framework: 'google-adk',
|
|
1021
|
+
...properties,
|
|
1022
|
+
...this._properties,
|
|
1023
|
+
...(distinctId ? {} : {
|
|
1024
|
+
$process_person_profile: false
|
|
1025
|
+
})
|
|
1026
|
+
},
|
|
1027
|
+
groups: this._groups
|
|
1028
|
+
};
|
|
1029
|
+
if (this._captureImmediate) {
|
|
1030
|
+
await captureAiEventImmediate(this._client, message);
|
|
1031
|
+
} else {
|
|
1032
|
+
captureAiEvent(this._client, message);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
_resolveDistinctId(context) {
|
|
1036
|
+
if (typeof this._distinctId === 'function') {
|
|
1037
|
+
const resolved = this._distinctId(context);
|
|
1038
|
+
if (resolved) {
|
|
1039
|
+
return String(resolved);
|
|
1040
|
+
}
|
|
1041
|
+
} else if (this._distinctId) {
|
|
1042
|
+
return String(this._distinctId);
|
|
1043
|
+
}
|
|
1044
|
+
return context.userId ? String(context.userId) : undefined;
|
|
1045
|
+
}
|
|
1046
|
+
_resolveInvocationDistinctId(context) {
|
|
1047
|
+
if (typeof this._distinctId === 'string' && this._distinctId) {
|
|
1048
|
+
return String(this._distinctId);
|
|
1049
|
+
}
|
|
1050
|
+
return context.userId ? String(context.userId) : undefined;
|
|
1051
|
+
}
|
|
1052
|
+
_rememberContext(context) {
|
|
1053
|
+
const trace = this._traces.get(context.invocationId);
|
|
1054
|
+
if (trace) {
|
|
1055
|
+
trace.distinctId = this._resolveDistinctId(context);
|
|
1056
|
+
trace.sessionId = context.sessionId || trace.sessionId;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
_parentSpanId(context) {
|
|
1060
|
+
return this._pendingAgents.get(this._pendingKey(context))?.[0]?.spanId ?? this._traces.get(context.invocationId)?.spanId;
|
|
1061
|
+
}
|
|
1062
|
+
_pendingKey(context) {
|
|
1063
|
+
return [context.invocationId, context.invocationContext?.branch ?? '', context.agentName].join('\0');
|
|
1064
|
+
}
|
|
1065
|
+
_toolKey(context, toolName) {
|
|
1066
|
+
return [this._pendingKey(context), context.functionCallId ?? '', toolName].join('\0');
|
|
1067
|
+
}
|
|
1068
|
+
_peekPending(key) {
|
|
1069
|
+
return this._pending.get(key)?.[0];
|
|
1070
|
+
}
|
|
1071
|
+
_takePending(key) {
|
|
1072
|
+
const queue = this._pending.get(key);
|
|
1073
|
+
if (!queue || queue.length === 0) {
|
|
1074
|
+
return undefined;
|
|
1075
|
+
}
|
|
1076
|
+
const pending = queue.shift();
|
|
1077
|
+
if (queue.length === 0) {
|
|
1078
|
+
this._pending.delete(key);
|
|
1079
|
+
}
|
|
1080
|
+
return pending;
|
|
1081
|
+
}
|
|
1082
|
+
_takePendingAgent(key) {
|
|
1083
|
+
const queue = this._pendingAgents.get(key);
|
|
1084
|
+
if (!queue || queue.length === 0) {
|
|
1085
|
+
return undefined;
|
|
1086
|
+
}
|
|
1087
|
+
const pending = queue.shift();
|
|
1088
|
+
if (queue.length === 0) {
|
|
1089
|
+
this._pendingAgents.delete(key);
|
|
1090
|
+
}
|
|
1091
|
+
return pending;
|
|
1092
|
+
}
|
|
1093
|
+
_takePendingTool(key) {
|
|
1094
|
+
const queue = this._pendingTools.get(key);
|
|
1095
|
+
if (!queue || queue.length === 0) {
|
|
1096
|
+
return undefined;
|
|
1097
|
+
}
|
|
1098
|
+
const pending = queue.shift();
|
|
1099
|
+
if (queue.length === 0) {
|
|
1100
|
+
this._pendingTools.delete(key);
|
|
1101
|
+
}
|
|
1102
|
+
return pending;
|
|
1103
|
+
}
|
|
1104
|
+
_isNonTerminalStreamResponse(llmResponse) {
|
|
1105
|
+
if (llmResponse.turnComplete === false) {
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return llmResponse.partial === false && llmResponse.turnComplete !== true && llmResponse.content !== undefined && llmResponse.finishReason === undefined && llmResponse.errorCode === undefined;
|
|
1109
|
+
}
|
|
1110
|
+
_evictStalePending() {
|
|
1111
|
+
const cutoff = Date.now() - MAX_PENDING_AGE_MS;
|
|
1112
|
+
this._evictStaleQueueEntries(this._pending, cutoff);
|
|
1113
|
+
this._evictStaleQueueEntries(this._pendingAgents, cutoff);
|
|
1114
|
+
this._evictStaleQueueEntries(this._pendingTools, cutoff);
|
|
1115
|
+
for (const [invocationId, trace] of this._traces) {
|
|
1116
|
+
if (trace.startTime < cutoff) {
|
|
1117
|
+
this._traces.delete(invocationId);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
_evictStaleQueueEntries(queues, cutoff) {
|
|
1122
|
+
for (const [key, queue] of queues) {
|
|
1123
|
+
const active = queue.filter(entry => entry.startTime >= cutoff);
|
|
1124
|
+
if (active.length > 0) {
|
|
1125
|
+
queues.set(key, active);
|
|
1126
|
+
} else {
|
|
1127
|
+
queues.delete(key);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
_clearPendingInvocation(invocationId) {
|
|
1132
|
+
const prefix = `${invocationId}\0`;
|
|
1133
|
+
for (const key of this._pending.keys()) {
|
|
1134
|
+
if (key.startsWith(prefix)) {
|
|
1135
|
+
this._pending.delete(key);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
for (const key of this._pendingAgents.keys()) {
|
|
1139
|
+
if (key.startsWith(prefix)) {
|
|
1140
|
+
this._pendingAgents.delete(key);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
for (const key of this._pendingTools.keys()) {
|
|
1144
|
+
if (key.startsWith(prefix)) {
|
|
1145
|
+
this._pendingTools.delete(key);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
_formatInput(llmRequest) {
|
|
1150
|
+
const contents = sanitizeGemini(llmRequest.contents, this._client) ?? [];
|
|
1151
|
+
const messages = Array.isArray(contents) ? contents.map(content => formatContent(content, this._client)) : [];
|
|
1152
|
+
const systemInstruction = extractSystemInstruction(llmRequest);
|
|
1153
|
+
if (systemInstruction && !messages.some(message => message.role === 'system')) {
|
|
1154
|
+
return [{
|
|
1155
|
+
role: 'system',
|
|
1156
|
+
content: systemInstruction
|
|
1157
|
+
}, ...messages];
|
|
1158
|
+
}
|
|
1159
|
+
return messages;
|
|
1160
|
+
}
|
|
1161
|
+
_formatOutput(llmResponse) {
|
|
1162
|
+
// Reuse the Gemini response formatter (text/functionCall/inlineData +
|
|
1163
|
+
// base64 redaction) by adapting the ADK response into a candidates shape.
|
|
1164
|
+
return formatResponseGemini({
|
|
1165
|
+
candidates: llmResponse.content ? [{
|
|
1166
|
+
content: llmResponse.content
|
|
1167
|
+
}] : []
|
|
1168
|
+
}, this._client);
|
|
1169
|
+
}
|
|
1170
|
+
_handleError(error) {
|
|
1171
|
+
try {
|
|
1172
|
+
this._onError?.(error);
|
|
1173
|
+
} catch {
|
|
1174
|
+
// The plugin must never throw into the ADK model flow.
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** Map a genai content role to PostHog's convention (`model` -> `assistant`). */
|
|
1180
|
+
function mapRole(role) {
|
|
1181
|
+
if (role === 'model') {
|
|
1182
|
+
return 'assistant';
|
|
1183
|
+
}
|
|
1184
|
+
return role ?? 'user';
|
|
1185
|
+
}
|
|
1186
|
+
function formatContent(content, client) {
|
|
1187
|
+
const parts = Array.isArray(content?.parts) ? content.parts : [];
|
|
1188
|
+
const blocks = [];
|
|
1189
|
+
for (const part of parts) {
|
|
1190
|
+
if (part == null) {
|
|
1191
|
+
continue;
|
|
1192
|
+
}
|
|
1193
|
+
if (part.text) {
|
|
1194
|
+
blocks.push({
|
|
1195
|
+
type: 'text',
|
|
1196
|
+
text: String(part.text)
|
|
1197
|
+
});
|
|
1198
|
+
} else if (part.functionCall) {
|
|
1199
|
+
blocks.push({
|
|
1200
|
+
type: 'function',
|
|
1201
|
+
id: part.functionCall.id,
|
|
1202
|
+
function: {
|
|
1203
|
+
name: part.functionCall.name,
|
|
1204
|
+
arguments: part.functionCall.args ?? {}
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
} else if (part.functionResponse) {
|
|
1208
|
+
blocks.push({
|
|
1209
|
+
type: 'text',
|
|
1210
|
+
text: toContentString(part.functionResponse.response ?? part.functionResponse)
|
|
1211
|
+
});
|
|
1212
|
+
} else if (part.inlineData) {
|
|
1213
|
+
blocks.push(formatInlineDataBlock(part.inlineData, client));
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
role: mapRole(content?.role),
|
|
1218
|
+
content: blocks
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/** Extract the system instruction text from an LlmRequest's config, if any. */
|
|
1223
|
+
function extractSystemInstruction(llmRequest) {
|
|
1224
|
+
const systemInstruction = llmRequest.config?.systemInstruction;
|
|
1225
|
+
if (!systemInstruction) {
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
1228
|
+
if (typeof systemInstruction === 'string') {
|
|
1229
|
+
return systemInstruction;
|
|
1230
|
+
}
|
|
1231
|
+
const asObject = systemInstruction;
|
|
1232
|
+
if (typeof asObject.text === 'string') {
|
|
1233
|
+
return asObject.text;
|
|
1234
|
+
}
|
|
1235
|
+
const parts = Array.isArray(asObject.parts) ? asObject.parts : Array.isArray(systemInstruction) ? systemInstruction : [];
|
|
1236
|
+
const textParts = parts.flatMap(part => {
|
|
1237
|
+
if (typeof part === 'string') {
|
|
1238
|
+
return [part];
|
|
1239
|
+
}
|
|
1240
|
+
if (part && typeof part === 'object' && typeof part.text === 'string') {
|
|
1241
|
+
return [part.text];
|
|
1242
|
+
}
|
|
1243
|
+
return [];
|
|
1244
|
+
});
|
|
1245
|
+
return textParts.length > 0 ? textParts.join('') : null;
|
|
1246
|
+
}
|
|
1247
|
+
const MODEL_PARAM_KEYS = ['temperature', 'topP', 'topK', 'maxOutputTokens', 'candidateCount', 'stopSequences', 'presencePenalty', 'frequencyPenalty', 'seed'];
|
|
1248
|
+
function extractModelParameters(config) {
|
|
1249
|
+
const params = {};
|
|
1250
|
+
if (!config || typeof config !== 'object') {
|
|
1251
|
+
return params;
|
|
1252
|
+
}
|
|
1253
|
+
const source = config;
|
|
1254
|
+
for (const key of MODEL_PARAM_KEYS) {
|
|
1255
|
+
if (source[key] !== undefined) {
|
|
1256
|
+
params[key] = source[key];
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
return params;
|
|
1260
|
+
}
|
|
1261
|
+
function extractTools(llmRequest) {
|
|
1262
|
+
const tools = llmRequest.config?.tools;
|
|
1263
|
+
return Array.isArray(tools) && tools.length > 0 ? tools : null;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
exports.PostHogADKPlugin = PostHogADKPlugin;
|
|
1267
|
+
//# sourceMappingURL=index.cjs.map
|