@posthog/ai 8.10.0 → 8.10.2

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.
Files changed (51) hide show
  1. package/dist/adk/index.cjs +908 -1198
  2. package/dist/adk/index.cjs.map +1 -1
  3. package/dist/adk/index.d.ts +109 -109
  4. package/dist/adk/index.mjs +907 -1196
  5. package/dist/adk/index.mjs.map +1 -1
  6. package/dist/anthropic/index.cjs +927 -1111
  7. package/dist/anthropic/index.cjs.map +1 -1
  8. package/dist/anthropic/index.d.ts +34 -33
  9. package/dist/anthropic/index.mjs +899 -1102
  10. package/dist/anthropic/index.mjs.map +1 -1
  11. package/dist/gemini/index.cjs +863 -1108
  12. package/dist/gemini/index.cjs.map +1 -1
  13. package/dist/gemini/index.d.ts +38 -35
  14. package/dist/gemini/index.mjs +858 -1103
  15. package/dist/gemini/index.mjs.map +1 -1
  16. package/dist/index.cjs +1218 -1546
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +170 -163
  19. package/dist/index.mjs +1216 -1544
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/langchain/index.cjs +851 -1029
  22. package/dist/langchain/index.cjs.map +1 -1
  23. package/dist/langchain/index.d.ts +75 -75
  24. package/dist/langchain/index.mjs +850 -1027
  25. package/dist/langchain/index.mjs.map +1 -1
  26. package/dist/langchain/middleware/index.cjs +1016 -1225
  27. package/dist/langchain/middleware/index.cjs.map +1 -1
  28. package/dist/langchain/middleware/index.d.ts +29 -25
  29. package/dist/langchain/middleware/index.mjs +1015 -1223
  30. package/dist/langchain/middleware/index.mjs.map +1 -1
  31. package/dist/openai/index.cjs +1990 -2523
  32. package/dist/openai/index.cjs.map +1 -1
  33. package/dist/openai/index.d.ts +106 -104
  34. package/dist/openai/index.mjs +1985 -2518
  35. package/dist/openai/index.mjs.map +1 -1
  36. package/dist/openai-agents/index.cjs +745 -827
  37. package/dist/openai-agents/index.cjs.map +1 -1
  38. package/dist/openai-agents/index.d.ts +48 -47
  39. package/dist/openai-agents/index.mjs +744 -825
  40. package/dist/openai-agents/index.mjs.map +1 -1
  41. package/dist/otel/index.cjs +427 -486
  42. package/dist/otel/index.cjs.map +1 -1
  43. package/dist/otel/index.d.ts +36 -35
  44. package/dist/otel/index.mjs +426 -484
  45. package/dist/otel/index.mjs.map +1 -1
  46. package/dist/vercel/index.cjs +992 -1343
  47. package/dist/vercel/index.cjs.map +1 -1
  48. package/dist/vercel/index.d.ts +21 -16
  49. package/dist/vercel/index.mjs +991 -1341
  50. package/dist/vercel/index.mjs.map +1 -1
  51. package/package.json +15 -14
@@ -1,1267 +1,977 @@
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
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _google_adk = require("@google/adk");
3
+ let uuid = require("uuid");
4
+ let _posthog_core = require("@posthog/core");
5
+ //#region package.json
6
+ var version = "8.10.2";
7
+ //#endregion
8
+ //#region src/captureAiEvent.ts
13
9
  /** @internal */
14
10
  function isFullAiCaptureEnabled(client) {
15
- return client?.enableFullAiCapture === true;
11
+ return client?.enableFullAiCapture === true;
16
12
  }
17
-
18
13
  /** @internal */
19
14
  function captureAiEvent(client, event) {
20
- if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
21
- client.captureAi(event);
22
- return;
23
- }
24
- client.capture(event);
15
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === "function") {
16
+ client.captureAi(event);
17
+ return;
18
+ }
19
+ client.capture(event);
25
20
  }
26
-
27
21
  /** @internal */
28
22
  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);
23
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === "function") {
24
+ await client.captureAiImmediate(event);
25
+ return;
26
+ }
27
+ await client.captureImmediate(event);
34
28
  }
35
-
29
+ //#endregion
30
+ //#region src/sanitization/base64_recognizer.ts
36
31
  const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
37
32
  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
-
33
+ var Base64Recognizer = class {
34
+ recognize(value, minLength) {
35
+ const dataUrl = DATA_URL_PREFIX_RE.exec(value);
36
+ if (dataUrl) return {
37
+ kind: "data-url",
38
+ mediaType: dataUrl[1]
39
+ };
40
+ if (value.length < minLength) return { kind: "none" };
41
+ const confidencePrefix = value.slice(0, minLength);
42
+ if (BASE64_ALPHABET_RE.test(confidencePrefix)) return { kind: "raw" };
43
+ else return { kind: "none" };
44
+ }
45
+ };
46
+ //#endregion
47
+ //#region src/sanitization/media_type_context.ts
48
+ const MIME_HINT_KEYS = [
49
+ "mediaType",
50
+ "media_type",
51
+ "mimeType",
52
+ "mime_type"
53
+ ];
54
+ const STRONG_CONTEXT_KEYS = /* @__PURE__ */ new Set([
55
+ "data",
56
+ "file_data",
57
+ "fileData",
58
+ "image_url",
59
+ "imageUrl",
60
+ "video_url",
61
+ "videoUrl",
62
+ "audio",
63
+ "audio_data",
64
+ "audioData",
65
+ "inline_data",
66
+ "inlineData",
67
+ "source",
68
+ "result"
69
+ ]);
70
+ const STRONG_CONTEXT_TYPES = /* @__PURE__ */ new Set([
71
+ "image",
72
+ "image_url",
73
+ "input_image",
74
+ "audio",
75
+ "input_audio",
76
+ "video",
77
+ "video_url",
78
+ "file",
79
+ "input_file",
80
+ "document",
81
+ "media",
82
+ "file-data"
83
+ ]);
84
+ const FILE_FAMILY_TYPES = /* @__PURE__ */ new Set([
85
+ "file",
86
+ "input_file",
87
+ "document",
88
+ "media",
89
+ "file-data"
90
+ ]);
91
+ const KNOWN_AUDIO_FORMATS = /* @__PURE__ */ new Set([
92
+ "wav",
93
+ "mp3",
94
+ "ogg",
95
+ "flac",
96
+ "m4a",
97
+ "aac",
98
+ "webm"
99
+ ]);
100
+ var MediaTypeContext = class MediaTypeContext {
101
+ static {
102
+ this.EMPTY = new MediaTypeContext(void 0, void 0);
103
+ }
104
+ constructor(parent, key, explicitMediaType) {
105
+ this.parent = parent;
106
+ this.key = key;
107
+ this.explicitMediaType = explicitMediaType;
108
+ }
109
+ inferMediaType() {
110
+ return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
111
+ }
112
+ inferFromSiblingMime() {
113
+ if (this.explicitMediaType) return this.explicitMediaType;
114
+ if (!this.parent) return void 0;
115
+ for (const hint of MIME_HINT_KEYS) {
116
+ const v = this.parent[hint];
117
+ if (typeof v === "string") return v;
118
+ }
119
+ }
120
+ inferFromSiblingFormat() {
121
+ if (!this.parent) return void 0;
122
+ const fmt = this.parent.format;
123
+ if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return `audio/${fmt.toLowerCase()}`;
124
+ }
125
+ inferFromParentType() {
126
+ if (!this.parent) return void 0;
127
+ const t = this.parent.type;
128
+ if (typeof t !== "string") return void 0;
129
+ if (t === "image" || t === "image_url" || t === "input_image") return "image";
130
+ if (t === "audio" || t === "input_audio") return "audio";
131
+ if (t === "video" || t === "video_url") return "video";
132
+ if (FILE_FAMILY_TYPES.has(t)) return "application/octet-stream";
133
+ }
134
+ inferFromKey() {
135
+ if (!this.key) return void 0;
136
+ const key = this.key.toLowerCase();
137
+ if (key.includes("audio")) return "audio";
138
+ if (key.includes("video")) return "video";
139
+ if (key.includes("image")) return "image";
140
+ if (key.includes("file") || key.includes("document")) return "application/octet-stream";
141
+ }
142
+ hasExplicitBinaryMediaType() {
143
+ if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
144
+ const mediaType = this.inferFromSiblingMime();
145
+ return mediaType !== void 0 && !mediaType.toLowerCase().startsWith("text/");
146
+ }
147
+ signalsBinary() {
148
+ if (this.explicitMediaType) return true;
149
+ if (this.parent) {
150
+ for (const hint of MIME_HINT_KEYS) if (typeof this.parent[hint] === "string") return true;
151
+ const fmt = this.parent.format;
152
+ if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
153
+ const t = this.parent.type;
154
+ if (typeof t === "string" && STRONG_CONTEXT_TYPES.has(t)) return true;
155
+ }
156
+ if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
157
+ return false;
158
+ }
159
+ };
160
+ //#endregion
161
+ //#region src/sanitization/binary_content_redactor.ts
133
162
  const STRONG_CONTEXT_MIN_LENGTH = 64;
134
163
  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
-
164
+ var BinaryContentRedactor = class {
165
+ constructor(recognizer = new Base64Recognizer()) {
166
+ this.recognizer = recognizer;
167
+ this.visited = /* @__PURE__ */ new WeakSet();
168
+ }
169
+ redact(value, mediaType) {
170
+ this.visited = /* @__PURE__ */ new WeakSet();
171
+ return this.walk(value, mediaType ? new MediaTypeContext(void 0, void 0, mediaType) : MediaTypeContext.EMPTY);
172
+ }
173
+ walk(value, ctx) {
174
+ if (value === null || value === void 0) return value;
175
+ if (typeof value === "string") return this.redactString(value, ctx);
176
+ if (typeof value !== "object") return value;
177
+ if (typeof Uint8Array !== "undefined" && value instanceof Uint8Array) return this.placeholderFor(ctx.inferMediaType());
178
+ if (this.visited.has(value)) return null;
179
+ this.visited.add(value);
180
+ if (Array.isArray(value)) return value.map((item) => this.walk(item, ctx));
181
+ const obj = value;
182
+ const out = {};
183
+ for (const k of Object.keys(obj)) out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
184
+ return out;
185
+ }
186
+ redactString(value, ctx) {
187
+ const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
188
+ const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, "") : value;
189
+ const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
190
+ const recognition = this.recognizer.recognize(recognitionValue, minLength);
191
+ switch (recognition.kind) {
192
+ case "data-url": return this.placeholderFor(recognition.mediaType);
193
+ case "raw": return this.placeholderFor(ctx.inferMediaType());
194
+ case "none": return value;
195
+ }
196
+ }
197
+ placeholderFor(mediaType) {
198
+ if (!mediaType) return "[base64 redacted]";
199
+ if (mediaType === "application/octet-stream") return "[base64 file redacted]";
200
+ return `[base64 ${mediaType} redacted]`;
201
+ }
202
+ };
203
+ //#endregion
204
+ //#region src/sanitization.ts
186
205
  const redactor = new BinaryContentRedactor();
187
206
  function redactBase64DataUrl(str, mediaType) {
188
- return redactor.redact(str, mediaType);
207
+ return redactor.redact(str, mediaType);
189
208
  }
190
209
  const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
191
210
  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
-
211
+ //#endregion
212
+ //#region src/utils.ts
213
+ const TOKEN_PROPERTY_KEYS = /* @__PURE__ */ new Set([
214
+ "$ai_input_tokens",
215
+ "$ai_output_tokens",
216
+ "$ai_cache_read_input_tokens",
217
+ "$ai_cache_creation_input_tokens",
218
+ "$ai_total_tokens",
219
+ "$ai_reasoning_tokens"
220
+ ]);
195
221
  /**
196
- * Whether the caller supplied their own token counts, which override the ones the SDK
197
- * derived from the provider response.
198
- */
222
+ * Whether the caller supplied their own token counts, which override the ones the SDK
223
+ * derived from the provider response.
224
+ */
199
225
  function hasTokenOverrides(posthogProperties) {
200
- return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
226
+ return !!posthogProperties && Object.keys(posthogProperties).some((key) => TOKEN_PROPERTY_KEYS.has(key));
201
227
  }
202
228
  function getTokensSource(posthogProperties) {
203
- return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
229
+ return hasTokenOverrides(posthogProperties) ? "passthrough" : "sdk";
204
230
  }
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.
231
+ const STRING_FORMAT = "utf8";
209
232
  new TextEncoder();
210
- new TextDecoder(STRING_FORMAT, {
211
- fatal: false
212
- });
213
-
233
+ new TextDecoder(STRING_FORMAT, { fatal: false });
214
234
  /**
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
- */
235
+ * Safely converts content to a string, preserving structure for objects/arrays.
236
+ * - If content is already a string, returns it as-is
237
+ * - If content is an object or array, stringifies it with JSON.stringify to preserve structure
238
+ * - Otherwise, converts to string with String()
239
+ *
240
+ * This prevents the "[object Object]" bug when objects are naively converted to strings.
241
+ *
242
+ * @param content - The content to convert to a string
243
+ * @returns A string representation that preserves structure for complex types
244
+ */
225
245
  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);
246
+ if (typeof content === "string") return content;
247
+ if (content !== void 0 && content !== null && typeof content === "object") try {
248
+ return JSON.stringify(content);
249
+ } catch {
250
+ return String(content);
251
+ }
252
+ return String(content);
238
253
  }
239
254
  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
- };
255
+ if (mimeType.startsWith("audio/")) return {
256
+ type: "audio",
257
+ mime_type: mimeType,
258
+ data
259
+ };
260
+ if (mimeType.startsWith("image/")) return {
261
+ type: "image",
262
+ inline_data: {
263
+ mime_type: mimeType,
264
+ data
265
+ }
266
+ };
267
+ return {
268
+ type: "document",
269
+ inline_data: {
270
+ mime_type: mimeType,
271
+ data
272
+ }
273
+ };
263
274
  };
264
275
  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 ?? ''));
276
+ const mimeType = inlineData.mimeType || inlineData.mime_type || "application/octet-stream";
277
+ let data = inlineData.data;
278
+ if (data instanceof Uint8Array) {
279
+ if (typeof Buffer !== "undefined") data = Buffer.from(data).toString("base64");
280
+ else {
281
+ let binary = "";
282
+ for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]);
283
+ data = btoa(binary);
284
+ }
285
+ }
286
+ data = isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mimeType);
287
+ return buildInlineDataBlock(mimeType, String(data ?? ""));
280
288
  };
281
289
  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;
290
+ const output = [];
291
+ if (response.candidates && Array.isArray(response.candidates)) {
292
+ for (const candidate of response.candidates) if (candidate.content && candidate.content.parts) {
293
+ const content = [];
294
+ for (const part of candidate.content.parts) if (part.text) content.push({
295
+ type: "text",
296
+ text: part.text
297
+ });
298
+ else if (part.functionCall) content.push({
299
+ type: "function",
300
+ function: {
301
+ name: part.functionCall.name,
302
+ arguments: part.functionCall.args
303
+ }
304
+ });
305
+ else if (part.inlineData) content.push(formatInlineDataBlock(part.inlineData, client));
306
+ if (content.length > 0) output.push({
307
+ role: "assistant",
308
+ content
309
+ });
310
+ } else if (candidate.text) output.push({
311
+ role: "assistant",
312
+ content: [{
313
+ type: "text",
314
+ text: candidate.text
315
+ }]
316
+ });
317
+ } else if (response.text) output.push({
318
+ role: "assistant",
319
+ content: [{
320
+ type: "text",
321
+ text: response.text
322
+ }]
323
+ });
324
+ return output;
331
325
  };
332
326
  const withPrivacyMode = (client, privacyMode, input) => {
333
- return client.privacy_mode || privacyMode ? null : input;
327
+ return client.privacy_mode || privacyMode ? null : input;
334
328
  };
335
- let AIEvent = /*#__PURE__*/function (AIEvent) {
336
- AIEvent["Generation"] = "$ai_generation";
337
- AIEvent["Embedding"] = "$ai_embedding";
338
- return AIEvent;
339
- }({});
340
329
  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;
330
+ if (obj === void 0 || obj === null) return obj;
331
+ const jsonSafe = JSON.parse(JSON.stringify(obj));
332
+ if (typeof jsonSafe === "string") return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
333
+ else if (Array.isArray(jsonSafe)) return jsonSafe.map(sanitizeValues);
334
+ else if (jsonSafe && typeof jsonSafe === "object") return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
335
+ return jsonSafe;
354
336
  }
355
-
337
+ //#endregion
338
+ //#region src/serializeError.ts
356
339
  const DEFAULT_MAX_DEPTH = 3;
357
340
  const MAX_STACK_LINES = 20;
358
341
  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;
342
+ if (depth < 0 || value === null || typeof value !== "object") return value;
343
+ if (value instanceof Error) {
344
+ const out = {
345
+ name: value.name,
346
+ message: value.message,
347
+ stack: truncateStack(value.stack)
348
+ };
349
+ for (const key of Object.keys(value)) out[key] = serializeError(value[key], depth - 1);
350
+ if (value.cause !== void 0) out.cause = serializeError(value.cause, depth - 1);
351
+ return out;
352
+ }
353
+ if (Array.isArray(value)) return value.map((item) => serializeError(item, depth - 1));
354
+ return value;
380
355
  }
381
356
  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
- }
357
+ try {
358
+ return JSON.stringify(sanitizeValues(serializeError(error)));
359
+ } catch {
360
+ if (error instanceof Error) return JSON.stringify({
361
+ name: error.name,
362
+ message: error.message
363
+ });
364
+ return JSON.stringify({ message: String(error) });
365
+ }
395
366
  }
396
367
  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');
368
+ if (!stack) return stack;
369
+ const lines = stack.split("\n");
370
+ if (lines.length <= MAX_STACK_LINES) return stack;
371
+ return [...lines.slice(0, MAX_STACK_LINES), "... (truncated)"].join("\n");
405
372
  }
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
- }
373
+ //#endregion
374
+ //#region src/gatewayWarning.ts
375
+ const POSTHOG_AI_GATEWAY_HOSTS = [
376
+ "gateway.posthog.com",
377
+ "gateway.us.posthog.com",
378
+ "gateway.eu.posthog.com",
379
+ "ai-gateway.us.posthog.com",
380
+ "ai-gateway.eu.posthog.com"
381
+ ];
382
+ const GATEWAY_DOCS_URL = "https://posthog.com/docs/ai-observability";
383
+ const extractHost = (baseURL) => {
384
+ try {
385
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
386
+ return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
387
+ } catch {
388
+ return;
389
+ }
427
390
  };
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);
391
+ const isPostHogAiGatewayUrl = (baseURL) => {
392
+ if (!baseURL) return false;
393
+ const host = extractHost(baseURL);
394
+ return host !== void 0 && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
434
395
  };
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}.`);
396
+ const warnIfPostHogAiGateway = (baseURL) => {
397
+ if (!isPostHogAiGatewayUrl(baseURL)) return;
398
+ 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
399
  };
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
-
400
+ //#endregion
401
+ //#region src/captureAiGeneration.ts
451
402
  /**
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
- */
403
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
404
+ *
405
+ * This is the canonical primitive that every `@posthog/ai` wrapper
406
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
407
+ * external code can use it directly to instrument LLM calls made through
408
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
409
+ * same events the SDK wrappers produce.
410
+ *
411
+ * When `error` is set, the event is captured as an error. If the error is an
412
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
413
+ * so callers can re-throw the original error reference safely.
414
+ */
464
415
  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
- }
416
+ try {
417
+ if (!client.capture) return;
418
+ warnIfPostHogAiGateway(options.baseURL);
419
+ const traceId = options.traceId ?? (0, uuid.v4)();
420
+ const eventType = options.eventType ?? "$ai_generation";
421
+ const privacyMode = options.privacyMode ?? false;
422
+ const usage = options.usage ?? {};
423
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
424
+ const safeInput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.input);
425
+ const safeOutput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.output);
426
+ let httpStatus = options.httpStatus;
427
+ let errorData = {};
428
+ if (options.error) {
429
+ if (httpStatus === void 0) {
430
+ if (typeof options.error === "object" && "status" in options.error && typeof options.error.status === "number") httpStatus = options.error.status;
431
+ else if (typeof options.error === "object" && "statusCode" in options.error && typeof options.error.statusCode === "number") httpStatus = options.error.statusCode;
432
+ else httpStatus = 500;
433
+ }
434
+ let exceptionId;
435
+ if (client.options?.enableExceptionAutocapture) {
436
+ exceptionId = (0, _posthog_core.uuidv7)();
437
+ client.captureException(options.error, void 0, { $ai_trace_id: traceId }, exceptionId);
438
+ if (typeof options.error === "object") options.error.__posthog_previously_captured_error = true;
439
+ }
440
+ errorData = {
441
+ $ai_is_error: true,
442
+ $ai_error: stringifyError(options.error),
443
+ $exception_event_id: exceptionId
444
+ };
445
+ }
446
+ httpStatus = httpStatus ?? 200;
447
+ const costOverrideData = {};
448
+ if (options.costOverride) {
449
+ if (usage.inputTokens !== void 0) costOverrideData.$ai_input_cost_usd = (options.costOverride.inputCost ?? 0) * usage.inputTokens;
450
+ if (usage.outputTokens !== void 0) costOverrideData.$ai_output_cost_usd = (options.costOverride.outputCost ?? 0) * usage.outputTokens;
451
+ if (Object.keys(costOverrideData).length > 0) costOverrideData.$ai_total_cost_usd = (costOverrideData.$ai_input_cost_usd ?? 0) + (costOverrideData.$ai_output_cost_usd ?? 0);
452
+ }
453
+ const tokensOverridden = hasTokenOverrides(options.properties);
454
+ const additionalTokenValues = {
455
+ ...usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {},
456
+ ...usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {},
457
+ ...usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {},
458
+ ...usage.cacheReportingExclusive !== void 0 && !tokensOverridden ? { $ai_cache_reporting_exclusive: usage.cacheReportingExclusive } : {},
459
+ ...usage.webSearchCount ? { $ai_web_search_count: usage.webSearchCount } : {},
460
+ ...usage.rawUsage ? { $ai_usage: usage.rawUsage } : {}
461
+ };
462
+ const properties = {
463
+ $ai_lib: "posthog-ai",
464
+ $ai_lib_version: version,
465
+ $ai_provider: options.providerOverride ?? options.provider,
466
+ $ai_model: options.modelOverride ?? options.model,
467
+ $ai_model_parameters: options.modelParameters ?? {},
468
+ $ai_input: safeInput,
469
+ $ai_output_choices: safeOutput,
470
+ $ai_http_status: httpStatus,
471
+ ...usage.inputTokens !== void 0 ? { $ai_input_tokens: usage.inputTokens } : {},
472
+ ...usage.outputTokens !== void 0 ? { $ai_output_tokens: usage.outputTokens } : {},
473
+ ...additionalTokenValues,
474
+ ...options.latency !== void 0 ? { $ai_latency: options.latency } : {},
475
+ ...options.timeToFirstToken !== void 0 ? { $ai_time_to_first_token: options.timeToFirstToken } : {},
476
+ $ai_trace_id: traceId,
477
+ ...options.baseURL === null ? {} : { $ai_base_url: options.baseURL ?? "" },
478
+ ...options.properties,
479
+ $ai_tokens_source: getTokensSource(options.properties),
480
+ ...options.distinctId ? {} : { $process_person_profile: false },
481
+ ...options.stopReason ? { $ai_stop_reason: options.stopReason } : {},
482
+ ...options.tools ? { $ai_tools: options.tools } : {},
483
+ ...options.completionId ? { $ai_completion_id: options.completionId } : {},
484
+ ...options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? { $ai_provider_metadata: options.providerMetadata } : {},
485
+ ...errorData,
486
+ ...costOverrideData
487
+ };
488
+ const event = {
489
+ distinctId: options.distinctId ?? traceId,
490
+ event: eventType,
491
+ properties,
492
+ groups: options.groups
493
+ };
494
+ if (options.captureImmediate) await captureAiEventImmediate(client, event);
495
+ else captureAiEvent(client, event);
496
+ } catch (error) {
497
+ try {
498
+ options.onError?.(error);
499
+ } catch {}
500
+ console.warn("[PostHog AI] Failed to capture generation telemetry:", error);
501
+ }
627
502
  };
628
-
503
+ //#endregion
504
+ //#region src/gemini/usage.ts
629
505
  /** Map Gemini usage metadata to PostHog's provider-agnostic token fields. */
630
506
  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
- };
507
+ return {
508
+ inputTokens: metadata?.promptTokenCount ?? 0,
509
+ outputTokens: metadata?.candidatesTokenCount ?? 0,
510
+ reasoningTokens: metadata?.thoughtsTokenCount ?? 0,
511
+ cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0,
512
+ ...metadata?.cachedContentTokenCount ? { cacheReportingExclusive: false } : {},
513
+ ...additionalUsage,
514
+ rawUsage: metadata
515
+ };
645
516
  }
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
-
517
+ //#endregion
518
+ //#region src/adk/plugin.ts
654
519
  /** 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
-
520
+ const MAX_PENDING_AGE_MS = 36e5;
657
521
  /**
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
-
522
+ * A Google ADK (`@google/adk`) `BasePlugin` that captures PostHog AI traces,
523
+ * agent and tool spans, and a full `$ai_generation` event for every model call.
524
+ *
525
+ * Run, agent, and tool callbacks build the trace hierarchy. Model callbacks
526
+ * record input, output, model, token usage, latency, and finish reason through
527
+ * the shared {@link captureAiGeneration} primitive so PostHog derives cost from
528
+ * the model and tokens (never hardcoded here).
529
+ *
530
+ * ADK already emits OpenTelemetry `gen_ai.*` spans; this plugin is the
531
+ * complement for users who capture LLM analytics through the PostHog SDK rather
532
+ * than an OTEL exporter.
533
+ *
534
+ * @example
535
+ * ```typescript
536
+ * import { PostHogADKPlugin } from '@posthog/ai/adk'
537
+ * import { Runner } from '@google/adk'
538
+ * import { PostHog } from 'posthog-node'
539
+ *
540
+ * const phClient = new PostHog('<POSTHOG_API_KEY>')
541
+ *
542
+ * const runner = new Runner({
543
+ * appName: 'my-app',
544
+ * agent,
545
+ * sessionService,
546
+ * plugins: [new PostHogADKPlugin({ client: phClient, distinctId: 'user@example.com' })],
547
+ * })
548
+ * ```
549
+ */
550
+ var PostHogADKPlugin = class extends _google_adk.BasePlugin {
551
+ constructor(options) {
552
+ super("posthog");
553
+ this._pending = /* @__PURE__ */ new Map();
554
+ this._traces = /* @__PURE__ */ new Map();
555
+ this._pendingAgents = /* @__PURE__ */ new Map();
556
+ this._pendingTools = /* @__PURE__ */ new Map();
557
+ this._client = options.client;
558
+ this._distinctId = options.distinctId;
559
+ this._provider = options.provider ?? "gemini";
560
+ this._privacyMode = options.privacyMode ?? false;
561
+ this._groups = options.groups;
562
+ this._properties = options.properties ?? {};
563
+ this._captureImmediate = options.captureImmediate ?? false;
564
+ this._onError = options.onError;
565
+ }
566
+ async beforeRunCallback({ invocationContext }) {
567
+ try {
568
+ this._evictStalePending();
569
+ this._traces.set(invocationContext.invocationId, {
570
+ startTime: Date.now(),
571
+ spanId: invocationContext.invocationId,
572
+ name: invocationContext.agent?.name ?? "ADK invocation",
573
+ input: invocationContext.userContent,
574
+ distinctId: this._resolveInvocationDistinctId(invocationContext),
575
+ sessionId: invocationContext.session?.id
576
+ });
577
+ } catch (error) {
578
+ this._handleError(error);
579
+ }
580
+ }
581
+ async afterRunCallback({ invocationContext }) {
582
+ try {
583
+ const trace = this._traces.get(invocationContext.invocationId);
584
+ this._traces.delete(invocationContext.invocationId);
585
+ this._clearPendingInvocation(invocationContext.invocationId);
586
+ if (!trace) return;
587
+ await this._captureLifecycleEvent("$ai_trace", trace.distinctId, {
588
+ $ai_trace_id: invocationContext.invocationId,
589
+ $ai_span_id: trace.spanId,
590
+ $ai_span_name: trace.name,
591
+ $ai_input_state: withPrivacyMode(this._client, this._privacyMode, trace.input),
592
+ $ai_latency: (Date.now() - trace.startTime) / 1e3,
593
+ ...trace.sessionId ? { $ai_session_id: trace.sessionId } : {}
594
+ });
595
+ } catch (error) {
596
+ this._handleError(error);
597
+ }
598
+ }
599
+ async beforeAgentCallback({ agent, callbackContext }) {
600
+ try {
601
+ this._evictStalePending();
602
+ this._rememberContext(callbackContext);
603
+ const key = this._pendingKey(callbackContext);
604
+ const pending = {
605
+ startTime: Date.now(),
606
+ spanId: (0, uuid.v4)(),
607
+ name: agent.name,
608
+ input: callbackContext.userContent
609
+ };
610
+ const queue = this._pendingAgents.get(key);
611
+ if (queue) queue.push(pending);
612
+ else this._pendingAgents.set(key, [pending]);
613
+ } catch (error) {
614
+ this._handleError(error);
615
+ }
616
+ }
617
+ async afterAgentCallback({ callbackContext }) {
618
+ try {
619
+ const pending = this._takePendingAgent(this._pendingKey(callbackContext));
620
+ if (pending) await this._captureLifecycleEvent("$ai_span", this._resolveDistinctId(callbackContext), {
621
+ $ai_trace_id: callbackContext.invocationId,
622
+ $ai_span_id: pending.spanId,
623
+ ...this._traces.get(callbackContext.invocationId)?.spanId ? { $ai_parent_id: this._traces.get(callbackContext.invocationId)?.spanId } : {},
624
+ $ai_span_name: pending.name,
625
+ $ai_input_state: withPrivacyMode(this._client, this._privacyMode, pending.input),
626
+ $ai_latency: (Date.now() - pending.startTime) / 1e3,
627
+ ...callbackContext.sessionId ? { $ai_session_id: callbackContext.sessionId } : {},
628
+ ...callbackContext.agentName ? { $ai_agent_name: callbackContext.agentName } : {}
629
+ });
630
+ } catch (error) {
631
+ this._handleError(error);
632
+ }
633
+ }
634
+ async beforeModelCallback({ callbackContext, llmRequest }) {
635
+ try {
636
+ this._evictStalePending();
637
+ this._rememberContext(callbackContext);
638
+ const pending = {
639
+ startTime: Date.now(),
640
+ spanId: (0, uuid.v4)(),
641
+ input: this._formatInput(llmRequest),
642
+ model: llmRequest.model,
643
+ modelParameters: extractModelParameters(llmRequest.config),
644
+ tools: extractTools(llmRequest),
645
+ streamedOutput: []
646
+ };
647
+ const key = this._pendingKey(callbackContext);
648
+ const queue = this._pending.get(key);
649
+ if (queue) queue.push(pending);
650
+ else this._pending.set(key, [pending]);
651
+ } catch (error) {
652
+ this._handleError(error);
653
+ }
654
+ }
655
+ async afterModelCallback({ callbackContext, llmResponse }) {
656
+ try {
657
+ if (llmResponse.partial) return;
658
+ const key = this._pendingKey(callbackContext);
659
+ const pending = this._peekPending(key);
660
+ if (this._isNonTerminalStreamResponse(llmResponse)) {
661
+ if (pending) pending.streamedOutput.push(...this._formatOutput(llmResponse));
662
+ return;
663
+ }
664
+ const completedPending = this._takePending(key);
665
+ const error = llmResponse.errorCode ? new Error(llmResponse.errorMessage ?? String(llmResponse.errorCode)) : void 0;
666
+ const output = error ? [] : this._formatOutput(llmResponse);
667
+ await this._capture(callbackContext, {
668
+ pending: completedPending,
669
+ output: !error && output.length === 0 && completedPending?.streamedOutput.length ? completedPending.streamedOutput : output,
670
+ model: llmResponse.modelVersion ?? completedPending?.model,
671
+ usage: llmResponse.usageMetadata,
672
+ stopReason: llmResponse.finishReason ? String(llmResponse.finishReason) : void 0,
673
+ error
674
+ });
675
+ } catch (error) {
676
+ this._handleError(error);
677
+ }
678
+ }
679
+ async onModelErrorCallback({ callbackContext, llmRequest, error }) {
680
+ try {
681
+ const pending = this._takePending(this._pendingKey(callbackContext));
682
+ await this._capture(callbackContext, {
683
+ pending: pending ?? {
684
+ startTime: Date.now(),
685
+ spanId: (0, uuid.v4)(),
686
+ input: this._formatInput(llmRequest),
687
+ model: llmRequest.model,
688
+ modelParameters: extractModelParameters(llmRequest.config),
689
+ tools: extractTools(llmRequest),
690
+ streamedOutput: []
691
+ },
692
+ output: [],
693
+ model: llmRequest.model,
694
+ usage: void 0,
695
+ error
696
+ });
697
+ } catch (captureError) {
698
+ this._handleError(captureError);
699
+ }
700
+ }
701
+ async beforeToolCallback({ tool, toolArgs, toolContext }) {
702
+ try {
703
+ this._evictStalePending();
704
+ this._rememberContext(toolContext);
705
+ const key = this._toolKey(toolContext, tool.name);
706
+ const pending = {
707
+ startTime: Date.now(),
708
+ spanId: toolContext.functionCallId ?? (0, uuid.v4)(),
709
+ name: tool.name,
710
+ input: toolArgs
711
+ };
712
+ const queue = this._pendingTools.get(key);
713
+ if (queue) queue.push(pending);
714
+ else this._pendingTools.set(key, [pending]);
715
+ } catch (error) {
716
+ this._handleError(error);
717
+ }
718
+ }
719
+ async afterToolCallback({ tool, toolContext, result }) {
720
+ try {
721
+ const pending = this._takePendingTool(this._toolKey(toolContext, tool.name));
722
+ if (pending) await this._captureToolSpan(toolContext, pending, result);
723
+ } catch (error) {
724
+ this._handleError(error);
725
+ }
726
+ }
727
+ async onToolErrorCallback({ tool, toolContext, error }) {
728
+ try {
729
+ const pending = this._takePendingTool(this._toolKey(toolContext, tool.name));
730
+ if (pending) await this._captureToolSpan(toolContext, pending, void 0, error);
731
+ } catch (captureError) {
732
+ this._handleError(captureError);
733
+ }
734
+ }
735
+ async _capture(callbackContext, args) {
736
+ const { pending, output, model, usage, stopReason, error } = args;
737
+ const latency = pending ? (Date.now() - pending.startTime) / 1e3 : void 0;
738
+ await captureAiGeneration(this._client, {
739
+ distinctId: this._resolveDistinctId(callbackContext),
740
+ traceId: callbackContext.invocationId,
741
+ model,
742
+ provider: this._provider,
743
+ baseURL: null,
744
+ input: pending?.input ?? [],
745
+ output,
746
+ latency,
747
+ modelParameters: pending?.modelParameters,
748
+ usage: mapGeminiUsage(usage),
749
+ stopReason,
750
+ tools: pending?.tools,
751
+ groups: this._groups,
752
+ privacyMode: this._privacyMode,
753
+ captureImmediate: this._captureImmediate,
754
+ onError: this._onError,
755
+ properties: {
756
+ $ai_framework: "google-adk",
757
+ $ai_span_id: pending?.spanId ?? (0, uuid.v4)(),
758
+ ...this._parentSpanId(callbackContext) ? { $ai_parent_id: this._parentSpanId(callbackContext) } : {},
759
+ ...callbackContext.sessionId ? { $ai_session_id: callbackContext.sessionId } : {},
760
+ ...callbackContext.agentName ? {
761
+ $ai_agent_name: callbackContext.agentName,
762
+ $ai_span_name: callbackContext.agentName
763
+ } : {},
764
+ ...this._properties
765
+ },
766
+ error
767
+ });
768
+ }
769
+ async _captureToolSpan(context, pending, result, error) {
770
+ await this._captureLifecycleEvent("$ai_span", this._resolveDistinctId(context), {
771
+ $ai_trace_id: context.invocationId,
772
+ $ai_span_id: pending.spanId,
773
+ ...this._parentSpanId(context) ? { $ai_parent_id: this._parentSpanId(context) } : {},
774
+ $ai_span_name: pending.name,
775
+ $ai_input_state: withPrivacyMode(this._client, this._privacyMode, pending.input),
776
+ ...result !== void 0 ? { $ai_output_state: withPrivacyMode(this._client, this._privacyMode, result) } : {},
777
+ $ai_latency: (Date.now() - pending.startTime) / 1e3,
778
+ ...context.sessionId ? { $ai_session_id: context.sessionId } : {},
779
+ ...context.agentName ? { $ai_agent_name: context.agentName } : {},
780
+ ...error ? {
781
+ $ai_is_error: true,
782
+ $ai_error: stringifyError(error)
783
+ } : {}
784
+ });
785
+ }
786
+ async _captureLifecycleEvent(event, distinctId, properties) {
787
+ const message = {
788
+ distinctId: distinctId ?? String(properties.$ai_trace_id),
789
+ event,
790
+ properties: {
791
+ $ai_lib: "posthog-ai",
792
+ $ai_lib_version: version,
793
+ $ai_framework: "google-adk",
794
+ ...properties,
795
+ ...this._properties,
796
+ ...distinctId ? {} : { $process_person_profile: false }
797
+ },
798
+ groups: this._groups
799
+ };
800
+ if (this._captureImmediate) await captureAiEventImmediate(this._client, message);
801
+ else captureAiEvent(this._client, message);
802
+ }
803
+ _resolveDistinctId(context) {
804
+ if (typeof this._distinctId === "function") {
805
+ const resolved = this._distinctId(context);
806
+ if (resolved) return String(resolved);
807
+ } else if (this._distinctId) return String(this._distinctId);
808
+ return context.userId ? String(context.userId) : void 0;
809
+ }
810
+ _resolveInvocationDistinctId(context) {
811
+ if (typeof this._distinctId === "string" && this._distinctId) return String(this._distinctId);
812
+ return context.userId ? String(context.userId) : void 0;
813
+ }
814
+ _rememberContext(context) {
815
+ const trace = this._traces.get(context.invocationId);
816
+ if (trace) {
817
+ trace.distinctId = this._resolveDistinctId(context);
818
+ trace.sessionId = context.sessionId || trace.sessionId;
819
+ }
820
+ }
821
+ _parentSpanId(context) {
822
+ return this._pendingAgents.get(this._pendingKey(context))?.[0]?.spanId ?? this._traces.get(context.invocationId)?.spanId;
823
+ }
824
+ _pendingKey(context) {
825
+ return [
826
+ context.invocationId,
827
+ context.invocationContext?.branch ?? "",
828
+ context.agentName
829
+ ].join("\0");
830
+ }
831
+ _toolKey(context, toolName) {
832
+ return [
833
+ this._pendingKey(context),
834
+ context.functionCallId ?? "",
835
+ toolName
836
+ ].join("\0");
837
+ }
838
+ _peekPending(key) {
839
+ return this._pending.get(key)?.[0];
840
+ }
841
+ _takePending(key) {
842
+ const queue = this._pending.get(key);
843
+ if (!queue || queue.length === 0) return;
844
+ const pending = queue.shift();
845
+ if (queue.length === 0) this._pending.delete(key);
846
+ return pending;
847
+ }
848
+ _takePendingAgent(key) {
849
+ const queue = this._pendingAgents.get(key);
850
+ if (!queue || queue.length === 0) return;
851
+ const pending = queue.shift();
852
+ if (queue.length === 0) this._pendingAgents.delete(key);
853
+ return pending;
854
+ }
855
+ _takePendingTool(key) {
856
+ const queue = this._pendingTools.get(key);
857
+ if (!queue || queue.length === 0) return;
858
+ const pending = queue.shift();
859
+ if (queue.length === 0) this._pendingTools.delete(key);
860
+ return pending;
861
+ }
862
+ _isNonTerminalStreamResponse(llmResponse) {
863
+ if (llmResponse.turnComplete === false) return true;
864
+ return llmResponse.partial === false && llmResponse.turnComplete !== true && llmResponse.content !== void 0 && llmResponse.finishReason === void 0 && llmResponse.errorCode === void 0;
865
+ }
866
+ _evictStalePending() {
867
+ const cutoff = Date.now() - MAX_PENDING_AGE_MS;
868
+ this._evictStaleQueueEntries(this._pending, cutoff);
869
+ this._evictStaleQueueEntries(this._pendingAgents, cutoff);
870
+ this._evictStaleQueueEntries(this._pendingTools, cutoff);
871
+ for (const [invocationId, trace] of this._traces) if (trace.startTime < cutoff) this._traces.delete(invocationId);
872
+ }
873
+ _evictStaleQueueEntries(queues, cutoff) {
874
+ for (const [key, queue] of queues) {
875
+ const active = queue.filter((entry) => entry.startTime >= cutoff);
876
+ if (active.length > 0) queues.set(key, active);
877
+ else queues.delete(key);
878
+ }
879
+ }
880
+ _clearPendingInvocation(invocationId) {
881
+ const prefix = `${invocationId}\0`;
882
+ for (const key of this._pending.keys()) if (key.startsWith(prefix)) this._pending.delete(key);
883
+ for (const key of this._pendingAgents.keys()) if (key.startsWith(prefix)) this._pendingAgents.delete(key);
884
+ for (const key of this._pendingTools.keys()) if (key.startsWith(prefix)) this._pendingTools.delete(key);
885
+ }
886
+ _formatInput(llmRequest) {
887
+ const contents = sanitizeGemini(llmRequest.contents, this._client) ?? [];
888
+ const messages = Array.isArray(contents) ? contents.map((content) => formatContent(content, this._client)) : [];
889
+ const systemInstruction = extractSystemInstruction(llmRequest);
890
+ if (systemInstruction && !messages.some((message) => message.role === "system")) return [{
891
+ role: "system",
892
+ content: systemInstruction
893
+ }, ...messages];
894
+ return messages;
895
+ }
896
+ _formatOutput(llmResponse) {
897
+ return formatResponseGemini({ candidates: llmResponse.content ? [{ content: llmResponse.content }] : [] }, this._client);
898
+ }
899
+ _handleError(error) {
900
+ try {
901
+ this._onError?.(error);
902
+ } catch {}
903
+ }
904
+ };
1179
905
  /** Map a genai content role to PostHog's convention (`model` -> `assistant`). */
1180
906
  function mapRole(role) {
1181
- if (role === 'model') {
1182
- return 'assistant';
1183
- }
1184
- return role ?? 'user';
907
+ if (role === "model") return "assistant";
908
+ return role ?? "user";
1185
909
  }
1186
910
  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
- };
911
+ const parts = Array.isArray(content?.parts) ? content.parts : [];
912
+ const blocks = [];
913
+ for (const part of parts) {
914
+ if (part == null) continue;
915
+ if (part.text) blocks.push({
916
+ type: "text",
917
+ text: String(part.text)
918
+ });
919
+ else if (part.functionCall) blocks.push({
920
+ type: "function",
921
+ id: part.functionCall.id,
922
+ function: {
923
+ name: part.functionCall.name,
924
+ arguments: part.functionCall.args ?? {}
925
+ }
926
+ });
927
+ else if (part.functionResponse) blocks.push({
928
+ type: "text",
929
+ text: toContentString(part.functionResponse.response ?? part.functionResponse)
930
+ });
931
+ else if (part.inlineData) blocks.push(formatInlineDataBlock(part.inlineData, client));
932
+ }
933
+ return {
934
+ role: mapRole(content?.role),
935
+ content: blocks
936
+ };
1220
937
  }
1221
-
1222
938
  /** Extract the system instruction text from an LlmRequest's config, if any. */
1223
939
  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;
940
+ const systemInstruction = llmRequest.config?.systemInstruction;
941
+ if (!systemInstruction) return null;
942
+ if (typeof systemInstruction === "string") return systemInstruction;
943
+ const asObject = systemInstruction;
944
+ if (typeof asObject.text === "string") return asObject.text;
945
+ const textParts = (Array.isArray(asObject.parts) ? asObject.parts : Array.isArray(systemInstruction) ? systemInstruction : []).flatMap((part) => {
946
+ if (typeof part === "string") return [part];
947
+ if (part && typeof part === "object" && typeof part.text === "string") return [part.text];
948
+ return [];
949
+ });
950
+ return textParts.length > 0 ? textParts.join("") : null;
1246
951
  }
1247
- const MODEL_PARAM_KEYS = ['temperature', 'topP', 'topK', 'maxOutputTokens', 'candidateCount', 'stopSequences', 'presencePenalty', 'frequencyPenalty', 'seed'];
952
+ const MODEL_PARAM_KEYS = [
953
+ "temperature",
954
+ "topP",
955
+ "topK",
956
+ "maxOutputTokens",
957
+ "candidateCount",
958
+ "stopSequences",
959
+ "presencePenalty",
960
+ "frequencyPenalty",
961
+ "seed"
962
+ ];
1248
963
  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;
964
+ const params = {};
965
+ if (!config || typeof config !== "object") return params;
966
+ const source = config;
967
+ for (const key of MODEL_PARAM_KEYS) if (source[key] !== void 0) params[key] = source[key];
968
+ return params;
1260
969
  }
1261
970
  function extractTools(llmRequest) {
1262
- const tools = llmRequest.config?.tools;
1263
- return Array.isArray(tools) && tools.length > 0 ? tools : null;
971
+ const tools = llmRequest.config?.tools;
972
+ return Array.isArray(tools) && tools.length > 0 ? tools : null;
1264
973
  }
1265
-
974
+ //#endregion
1266
975
  exports.PostHogADKPlugin = PostHogADKPlugin;
1267
- //# sourceMappingURL=index.cjs.map
976
+
977
+ //# sourceMappingURL=index.cjs.map