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