@posthog/ai 8.9.3 → 8.10.1

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 +977 -0
  2. package/dist/adk/index.cjs.map +1 -0
  3. package/dist/adk/index.d.ts +149 -0
  4. package/dist/adk/index.mjs +976 -0
  5. package/dist/adk/index.mjs.map +1 -0
  6. package/dist/anthropic/index.cjs +927 -1104
  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 -1095
  10. package/dist/anthropic/index.mjs.map +1 -1
  11. package/dist/gemini/index.cjs +867 -1112
  12. package/dist/gemini/index.cjs.map +1 -1
  13. package/dist/gemini/index.d.ts +38 -35
  14. package/dist/gemini/index.mjs +862 -1107
  15. package/dist/gemini/index.mjs.map +1 -1
  16. package/dist/index.cjs +1218 -1539
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.ts +170 -157
  19. package/dist/index.mjs +1216 -1537
  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 -2516
  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 -2511
  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 -1336
  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 -1334
  50. package/dist/vercel/index.mjs.map +1 -1
  51. package/package.json +23 -12
@@ -0,0 +1,977 @@
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.1";
7
+ //#endregion
8
+ //#region src/captureAiEvent.ts
9
+ /** @internal */
10
+ function isFullAiCaptureEnabled(client) {
11
+ return client?.enableFullAiCapture === true;
12
+ }
13
+ /** @internal */
14
+ function captureAiEvent(client, event) {
15
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === "function") {
16
+ client.captureAi(event);
17
+ return;
18
+ }
19
+ client.capture(event);
20
+ }
21
+ /** @internal */
22
+ async function captureAiEventImmediate(client, event) {
23
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === "function") {
24
+ await client.captureAiImmediate(event);
25
+ return;
26
+ }
27
+ await client.captureImmediate(event);
28
+ }
29
+ //#endregion
30
+ //#region src/sanitization/base64_recognizer.ts
31
+ const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
32
+ const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
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
162
+ const STRONG_CONTEXT_MIN_LENGTH = 64;
163
+ const WEAK_CONTEXT_MIN_LENGTH = 1024;
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
205
+ const redactor = new BinaryContentRedactor();
206
+ function redactBase64DataUrl(str, mediaType) {
207
+ return redactor.redact(str, mediaType);
208
+ }
209
+ const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
210
+ const sanitizeGemini = (data, client) => sanitize(data, client);
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
+ ]);
221
+ /**
222
+ * Whether the caller supplied their own token counts, which override the ones the SDK
223
+ * derived from the provider response.
224
+ */
225
+ function hasTokenOverrides(posthogProperties) {
226
+ return !!posthogProperties && Object.keys(posthogProperties).some((key) => TOKEN_PROPERTY_KEYS.has(key));
227
+ }
228
+ function getTokensSource(posthogProperties) {
229
+ return hasTokenOverrides(posthogProperties) ? "passthrough" : "sdk";
230
+ }
231
+ const STRING_FORMAT = "utf8";
232
+ new TextEncoder();
233
+ new TextDecoder(STRING_FORMAT, { fatal: false });
234
+ /**
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
+ */
245
+ function toContentString(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);
253
+ }
254
+ const buildInlineDataBlock = (mimeType, data) => {
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
+ };
274
+ };
275
+ const formatInlineDataBlock = (inlineData, client) => {
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 ?? ""));
288
+ };
289
+ const formatResponseGemini = (response, client) => {
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;
325
+ };
326
+ const withPrivacyMode = (client, privacyMode, input) => {
327
+ return client.privacy_mode || privacyMode ? null : input;
328
+ };
329
+ function sanitizeValues(obj) {
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;
336
+ }
337
+ //#endregion
338
+ //#region src/serializeError.ts
339
+ const DEFAULT_MAX_DEPTH = 3;
340
+ const MAX_STACK_LINES = 20;
341
+ function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
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;
355
+ }
356
+ function stringifyError(error) {
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
+ }
366
+ }
367
+ function truncateStack(stack) {
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");
372
+ }
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
+ }
390
+ };
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);
395
+ };
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}.`);
399
+ };
400
+ //#endregion
401
+ //#region src/captureAiGeneration.ts
402
+ /**
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
+ */
415
+ const captureAiGeneration = async (client, options) => {
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
+ }
502
+ };
503
+ //#endregion
504
+ //#region src/gemini/usage.ts
505
+ /** Map Gemini usage metadata to PostHog's provider-agnostic token fields. */
506
+ function mapGeminiUsage(metadata, additionalUsage = {}) {
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
+ };
516
+ }
517
+ //#endregion
518
+ //#region src/adk/plugin.ts
519
+ /** Calls older than this are treated as abandoned rather than evicting live calls by count. */
520
+ const MAX_PENDING_AGE_MS = 36e5;
521
+ /**
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
+ };
905
+ /** Map a genai content role to PostHog's convention (`model` -> `assistant`). */
906
+ function mapRole(role) {
907
+ if (role === "model") return "assistant";
908
+ return role ?? "user";
909
+ }
910
+ function formatContent(content, client) {
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
+ };
937
+ }
938
+ /** Extract the system instruction text from an LlmRequest's config, if any. */
939
+ function extractSystemInstruction(llmRequest) {
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;
951
+ }
952
+ const MODEL_PARAM_KEYS = [
953
+ "temperature",
954
+ "topP",
955
+ "topK",
956
+ "maxOutputTokens",
957
+ "candidateCount",
958
+ "stopSequences",
959
+ "presencePenalty",
960
+ "frequencyPenalty",
961
+ "seed"
962
+ ];
963
+ function extractModelParameters(config) {
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;
969
+ }
970
+ function extractTools(llmRequest) {
971
+ const tools = llmRequest.config?.tools;
972
+ return Array.isArray(tools) && tools.length > 0 ? tools : null;
973
+ }
974
+ //#endregion
975
+ exports.PostHogADKPlugin = PostHogADKPlugin;
976
+
977
+ //# sourceMappingURL=index.cjs.map