@posthog/ai 8.10.0 → 8.10.2

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