@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
package/dist/index.cjs CHANGED
@@ -1,1645 +1,1324 @@
1
- 'use strict';
2
-
3
- var uuid = require('uuid');
4
- var core = require('@posthog/core');
5
-
6
- // Type guards for safer type checking
7
- const isString = value => {
8
- return typeof value === 'string';
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let uuid = require("uuid");
3
+ let _posthog_core = require("@posthog/core");
4
+ //#region src/typeGuards.ts
5
+ const isString = (value) => {
6
+ return typeof value === "string";
9
7
  };
10
- const isObject = value => {
11
- return value !== null && typeof value === 'object' && !Array.isArray(value);
8
+ const isObject = (value) => {
9
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
10
  };
13
-
11
+ //#endregion
12
+ //#region src/captureAiEvent.ts
14
13
  /** @internal */
15
14
  function isFullAiCaptureEnabled(client) {
16
- return client?.enableFullAiCapture === true;
15
+ return client?.enableFullAiCapture === true;
17
16
  }
18
17
  /** @internal */
19
18
  function captureAiEvent(client, event) {
20
- if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
21
- client.captureAi(event);
22
- return;
23
- }
24
- client.capture(event);
19
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === "function") {
20
+ client.captureAi(event);
21
+ return;
22
+ }
23
+ client.capture(event);
25
24
  }
26
25
  /** @internal */
27
26
  async function captureAiEventImmediate(client, event) {
28
- if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
29
- await client.captureAiImmediate(event);
30
- return;
31
- }
32
- await client.captureImmediate(event);
27
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === "function") {
28
+ await client.captureAiImmediate(event);
29
+ return;
30
+ }
31
+ await client.captureImmediate(event);
33
32
  }
34
-
33
+ //#endregion
34
+ //#region src/sanitization/base64_recognizer.ts
35
35
  const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
36
36
  const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
37
- class Base64Recognizer {
38
- recognize(value, minLength) {
39
- const dataUrl = DATA_URL_PREFIX_RE.exec(value);
40
- if (dataUrl) return {
41
- kind: 'data-url',
42
- mediaType: dataUrl[1]
43
- };
44
- if (value.length < minLength) return {
45
- kind: 'none'
46
- };
47
- const confidencePrefix = value.slice(0, minLength);
48
- if (BASE64_ALPHABET_RE.test(confidencePrefix)) {
49
- return {
50
- kind: 'raw'
51
- };
52
- } else {
53
- return {
54
- kind: 'none'
55
- };
56
- }
57
- }
58
- }
59
-
60
- const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'];
61
- 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']);
62
- const STRONG_CONTEXT_TYPES = new Set(['image', 'image_url', 'input_image', 'audio', 'input_audio', 'video', 'video_url', 'file', 'input_file', 'document', 'media', 'file-data']);
63
- const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data']);
64
- const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
65
- class MediaTypeContext {
66
- constructor(parent, key, explicitMediaType) {
67
- this.parent = parent;
68
- this.key = key;
69
- this.explicitMediaType = explicitMediaType;
70
- }
71
- inferMediaType() {
72
- return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
73
- }
74
- inferFromSiblingMime() {
75
- if (this.explicitMediaType) return this.explicitMediaType;
76
- if (!this.parent) return undefined;
77
- for (const hint of MIME_HINT_KEYS) {
78
- const v = this.parent[hint];
79
- if (typeof v === 'string') return v;
80
- }
81
- return undefined;
82
- }
83
- inferFromSiblingFormat() {
84
- if (!this.parent) return undefined;
85
- const fmt = this.parent.format;
86
- if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {
87
- return `audio/${fmt.toLowerCase()}`;
88
- }
89
- return undefined;
90
- }
91
- inferFromParentType() {
92
- if (!this.parent) return undefined;
93
- const t = this.parent.type;
94
- if (typeof t !== 'string') return undefined;
95
- if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image';
96
- if (t === 'audio' || t === 'input_audio') return 'audio';
97
- if (t === 'video' || t === 'video_url') return 'video';
98
- if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream';
99
- return undefined;
100
- }
101
- inferFromKey() {
102
- if (!this.key) return undefined;
103
- const key = this.key.toLowerCase();
104
- if (key.includes('audio')) return 'audio';
105
- if (key.includes('video')) return 'video';
106
- if (key.includes('image')) return 'image';
107
- if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
108
- return undefined;
109
- }
110
- hasExplicitBinaryMediaType() {
111
- if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
112
- const mediaType = this.inferFromSiblingMime();
113
- return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/');
114
- }
115
- signalsBinary() {
116
- if (this.explicitMediaType) return true;
117
- if (this.parent) {
118
- for (const hint of MIME_HINT_KEYS) {
119
- if (typeof this.parent[hint] === 'string') return true;
120
- }
121
- const fmt = this.parent.format;
122
- if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
123
- const t = this.parent.type;
124
- if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true;
125
- }
126
- if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
127
- return false;
128
- }
129
- }
130
- MediaTypeContext.EMPTY = new MediaTypeContext(undefined, undefined);
131
-
37
+ var Base64Recognizer = class {
38
+ recognize(value, minLength) {
39
+ const dataUrl = DATA_URL_PREFIX_RE.exec(value);
40
+ if (dataUrl) return {
41
+ kind: "data-url",
42
+ mediaType: dataUrl[1]
43
+ };
44
+ if (value.length < minLength) return { kind: "none" };
45
+ const confidencePrefix = value.slice(0, minLength);
46
+ if (BASE64_ALPHABET_RE.test(confidencePrefix)) return { kind: "raw" };
47
+ else return { kind: "none" };
48
+ }
49
+ };
50
+ //#endregion
51
+ //#region src/sanitization/media_type_context.ts
52
+ const MIME_HINT_KEYS = [
53
+ "mediaType",
54
+ "media_type",
55
+ "mimeType",
56
+ "mime_type"
57
+ ];
58
+ const STRONG_CONTEXT_KEYS = /* @__PURE__ */ new Set([
59
+ "data",
60
+ "file_data",
61
+ "fileData",
62
+ "image_url",
63
+ "imageUrl",
64
+ "video_url",
65
+ "videoUrl",
66
+ "audio",
67
+ "audio_data",
68
+ "audioData",
69
+ "inline_data",
70
+ "inlineData",
71
+ "source",
72
+ "result"
73
+ ]);
74
+ const STRONG_CONTEXT_TYPES = /* @__PURE__ */ new Set([
75
+ "image",
76
+ "image_url",
77
+ "input_image",
78
+ "audio",
79
+ "input_audio",
80
+ "video",
81
+ "video_url",
82
+ "file",
83
+ "input_file",
84
+ "document",
85
+ "media",
86
+ "file-data"
87
+ ]);
88
+ const FILE_FAMILY_TYPES = /* @__PURE__ */ new Set([
89
+ "file",
90
+ "input_file",
91
+ "document",
92
+ "media",
93
+ "file-data"
94
+ ]);
95
+ const KNOWN_AUDIO_FORMATS = /* @__PURE__ */ new Set([
96
+ "wav",
97
+ "mp3",
98
+ "ogg",
99
+ "flac",
100
+ "m4a",
101
+ "aac",
102
+ "webm"
103
+ ]);
104
+ var MediaTypeContext = class MediaTypeContext {
105
+ static {
106
+ this.EMPTY = new MediaTypeContext(void 0, void 0);
107
+ }
108
+ constructor(parent, key, explicitMediaType) {
109
+ this.parent = parent;
110
+ this.key = key;
111
+ this.explicitMediaType = explicitMediaType;
112
+ }
113
+ inferMediaType() {
114
+ return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
115
+ }
116
+ inferFromSiblingMime() {
117
+ if (this.explicitMediaType) return this.explicitMediaType;
118
+ if (!this.parent) return void 0;
119
+ for (const hint of MIME_HINT_KEYS) {
120
+ const v = this.parent[hint];
121
+ if (typeof v === "string") return v;
122
+ }
123
+ }
124
+ inferFromSiblingFormat() {
125
+ if (!this.parent) return void 0;
126
+ const fmt = this.parent.format;
127
+ if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return `audio/${fmt.toLowerCase()}`;
128
+ }
129
+ inferFromParentType() {
130
+ if (!this.parent) return void 0;
131
+ const t = this.parent.type;
132
+ if (typeof t !== "string") return void 0;
133
+ if (t === "image" || t === "image_url" || t === "input_image") return "image";
134
+ if (t === "audio" || t === "input_audio") return "audio";
135
+ if (t === "video" || t === "video_url") return "video";
136
+ if (FILE_FAMILY_TYPES.has(t)) return "application/octet-stream";
137
+ }
138
+ inferFromKey() {
139
+ if (!this.key) return void 0;
140
+ const key = this.key.toLowerCase();
141
+ if (key.includes("audio")) return "audio";
142
+ if (key.includes("video")) return "video";
143
+ if (key.includes("image")) return "image";
144
+ if (key.includes("file") || key.includes("document")) return "application/octet-stream";
145
+ }
146
+ hasExplicitBinaryMediaType() {
147
+ if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
148
+ const mediaType = this.inferFromSiblingMime();
149
+ return mediaType !== void 0 && !mediaType.toLowerCase().startsWith("text/");
150
+ }
151
+ signalsBinary() {
152
+ if (this.explicitMediaType) return true;
153
+ if (this.parent) {
154
+ for (const hint of MIME_HINT_KEYS) if (typeof this.parent[hint] === "string") return true;
155
+ const fmt = this.parent.format;
156
+ if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
157
+ const t = this.parent.type;
158
+ if (typeof t === "string" && STRONG_CONTEXT_TYPES.has(t)) return true;
159
+ }
160
+ if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
161
+ return false;
162
+ }
163
+ };
164
+ //#endregion
165
+ //#region src/sanitization/binary_content_redactor.ts
132
166
  const STRONG_CONTEXT_MIN_LENGTH = 64;
133
167
  const WEAK_CONTEXT_MIN_LENGTH = 1024;
134
- class BinaryContentRedactor {
135
- constructor(recognizer = new Base64Recognizer()) {
136
- this.recognizer = recognizer;
137
- this.visited = new WeakSet();
138
- }
139
- redact(value, mediaType) {
140
- this.visited = new WeakSet();
141
- return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
142
- }
143
- walk(value, ctx) {
144
- if (value === null || value === undefined) return value;
145
- if (typeof value === 'string') return this.redactString(value, ctx);
146
- if (typeof value !== 'object') return value;
147
- // Buffer extends Uint8Array, so this branch catches both.
148
- if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
149
- return this.placeholderFor(ctx.inferMediaType());
150
- }
151
- if (this.visited.has(value)) return null;
152
- this.visited.add(value);
153
- if (Array.isArray(value)) {
154
- return value.map(item => this.walk(item, ctx));
155
- }
156
- const obj = value;
157
- const out = {};
158
- for (const k of Object.keys(obj)) {
159
- out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
160
- }
161
- return out;
162
- }
163
- redactString(value, ctx) {
164
- const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
165
- const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, '') : value;
166
- const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
167
- const recognition = this.recognizer.recognize(recognitionValue, minLength);
168
- switch (recognition.kind) {
169
- case 'data-url':
170
- return this.placeholderFor(recognition.mediaType);
171
- case 'raw':
172
- return this.placeholderFor(ctx.inferMediaType());
173
- case 'none':
174
- return value;
175
- }
176
- }
177
- placeholderFor(mediaType) {
178
- if (!mediaType) return '[base64 redacted]';
179
- if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
180
- return `[base64 ${mediaType} redacted]`;
181
- }
182
- }
183
-
168
+ var BinaryContentRedactor = class {
169
+ constructor(recognizer = new Base64Recognizer()) {
170
+ this.recognizer = recognizer;
171
+ this.visited = /* @__PURE__ */ new WeakSet();
172
+ }
173
+ redact(value, mediaType) {
174
+ this.visited = /* @__PURE__ */ new WeakSet();
175
+ return this.walk(value, mediaType ? new MediaTypeContext(void 0, void 0, mediaType) : MediaTypeContext.EMPTY);
176
+ }
177
+ walk(value, ctx) {
178
+ if (value === null || value === void 0) return value;
179
+ if (typeof value === "string") return this.redactString(value, ctx);
180
+ if (typeof value !== "object") return value;
181
+ if (typeof Uint8Array !== "undefined" && value instanceof Uint8Array) return this.placeholderFor(ctx.inferMediaType());
182
+ if (this.visited.has(value)) return null;
183
+ this.visited.add(value);
184
+ if (Array.isArray(value)) return value.map((item) => this.walk(item, ctx));
185
+ const obj = value;
186
+ const out = {};
187
+ for (const k of Object.keys(obj)) out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
188
+ return out;
189
+ }
190
+ redactString(value, ctx) {
191
+ const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
192
+ const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, "") : value;
193
+ const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
194
+ const recognition = this.recognizer.recognize(recognitionValue, minLength);
195
+ switch (recognition.kind) {
196
+ case "data-url": return this.placeholderFor(recognition.mediaType);
197
+ case "raw": return this.placeholderFor(ctx.inferMediaType());
198
+ case "none": return value;
199
+ }
200
+ }
201
+ placeholderFor(mediaType) {
202
+ if (!mediaType) return "[base64 redacted]";
203
+ if (mediaType === "application/octet-stream") return "[base64 file redacted]";
204
+ return `[base64 ${mediaType} redacted]`;
205
+ }
206
+ };
207
+ //#endregion
208
+ //#region src/sanitization.ts
184
209
  const redactor = new BinaryContentRedactor();
185
210
  function redactBase64DataUrl(str, mediaType) {
186
- return redactor.redact(str, mediaType);
211
+ return redactor.redact(str, mediaType);
187
212
  }
188
213
  const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
189
214
  const sanitizeVercel = (data, client) => sanitize(data, client);
190
-
191
- 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']);
215
+ //#endregion
216
+ //#region src/utils.ts
217
+ const TOKEN_PROPERTY_KEYS = /* @__PURE__ */ new Set([
218
+ "$ai_input_tokens",
219
+ "$ai_output_tokens",
220
+ "$ai_cache_read_input_tokens",
221
+ "$ai_cache_creation_input_tokens",
222
+ "$ai_total_tokens",
223
+ "$ai_reasoning_tokens"
224
+ ]);
192
225
  /**
193
- * Whether the caller supplied their own token counts, which override the ones the SDK
194
- * derived from the provider response.
195
- */
226
+ * Whether the caller supplied their own token counts, which override the ones the SDK
227
+ * derived from the provider response.
228
+ */
196
229
  function hasTokenOverrides(posthogProperties) {
197
- return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
230
+ return !!posthogProperties && Object.keys(posthogProperties).some((key) => TOKEN_PROPERTY_KEYS.has(key));
198
231
  }
199
232
  function getTokensSource(posthogProperties) {
200
- return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
233
+ return hasTokenOverrides(posthogProperties) ? "passthrough" : "sdk";
201
234
  }
202
- // limit large outputs by truncating to 200kb (approx 200k bytes)
203
- const MAX_OUTPUT_SIZE = 200000;
204
- const STRING_FORMAT = 'utf8';
205
- // Reused across calls to avoid per-invocation allocation; truncate() runs
206
- // hundreds of times for prompts with many parts.
235
+ const MAX_OUTPUT_SIZE = 2e5;
236
+ const STRING_FORMAT = "utf8";
207
237
  const sharedTextEncoder = new TextEncoder();
208
- const sharedTextDecoder = new TextDecoder(STRING_FORMAT, {
209
- fatal: false
210
- });
211
- const utf8ByteLength = str => sharedTextEncoder.encode(str).byteLength;
238
+ const sharedTextDecoder = new TextDecoder(STRING_FORMAT, { fatal: false });
239
+ const utf8ByteLength = (str) => sharedTextEncoder.encode(str).byteLength;
212
240
  /**
213
- * Safely converts content to a string, preserving structure for objects/arrays.
214
- * - If content is already a string, returns it as-is
215
- * - If content is an object or array, stringifies it with JSON.stringify to preserve structure
216
- * - Otherwise, converts to string with String()
217
- *
218
- * This prevents the "[object Object]" bug when objects are naively converted to strings.
219
- *
220
- * @param content - The content to convert to a string
221
- * @returns A string representation that preserves structure for complex types
222
- */
241
+ * Safely converts content to a string, preserving structure for objects/arrays.
242
+ * - If content is already a string, returns it as-is
243
+ * - If content is an object or array, stringifies it with JSON.stringify to preserve structure
244
+ * - Otherwise, converts to string with String()
245
+ *
246
+ * This prevents the "[object Object]" bug when objects are naively converted to strings.
247
+ *
248
+ * @param content - The content to convert to a string
249
+ * @returns A string representation that preserves structure for complex types
250
+ */
223
251
  function toContentString(content) {
224
- if (typeof content === 'string') {
225
- return content;
226
- }
227
- if (content !== undefined && content !== null && typeof content === 'object') {
228
- try {
229
- return JSON.stringify(content);
230
- } catch {
231
- // Fallback for circular refs, BigInt, or objects with throwing toJSON
232
- return String(content);
233
- }
234
- }
235
- return String(content);
252
+ if (typeof content === "string") return content;
253
+ if (content !== void 0 && content !== null && typeof content === "object") try {
254
+ return JSON.stringify(content);
255
+ } catch {
256
+ return String(content);
257
+ }
258
+ return String(content);
236
259
  }
237
260
  const getModelParams = (params, responseServiceTier) => {
238
- if (!params) {
239
- return {};
240
- }
241
- const modelParams = {};
242
- 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'];
243
- for (const key of paramKeys) {
244
- if (key in params && params[key] !== undefined) {
245
- modelParams[key] = params[key];
246
- }
247
- }
248
- return modelParams;
261
+ if (!params) return {};
262
+ const modelParams = {};
263
+ for (const key of [
264
+ "temperature",
265
+ "max_tokens",
266
+ "max_completion_tokens",
267
+ "top_p",
268
+ "frequency_penalty",
269
+ "presence_penalty",
270
+ "n",
271
+ "stop",
272
+ "stream",
273
+ "streaming",
274
+ "language",
275
+ "response_format",
276
+ "timestamp_granularities",
277
+ "service_tier"
278
+ ]) if (key in params && params[key] !== void 0) modelParams[key] = params[key];
279
+ if (responseServiceTier != null) modelParams.service_tier = responseServiceTier;
280
+ return modelParams;
249
281
  };
250
282
  const withPrivacyMode = (client, privacyMode, input) => {
251
- return client.privacy_mode || privacyMode ? null : input;
283
+ return client.privacy_mode || privacyMode ? null : input;
252
284
  };
253
285
  function toSafeString(input) {
254
- if (input === undefined || input === null) {
255
- return '';
256
- }
257
- if (typeof input === 'string') {
258
- return input;
259
- }
260
- try {
261
- return JSON.stringify(input);
262
- } catch {
263
- console.warn('Failed to stringify input', input);
264
- return '';
265
- }
286
+ if (input === void 0 || input === null) return "";
287
+ if (typeof input === "string") return input;
288
+ try {
289
+ return JSON.stringify(input);
290
+ } catch {
291
+ console.warn("Failed to stringify input", input);
292
+ return "";
293
+ }
266
294
  }
267
295
  const truncate = (input, client) => {
268
- const str = toSafeString(input);
269
- if (str === '') {
270
- return '';
271
- }
272
- if (isFullAiCaptureEnabled(client)) {
273
- return str;
274
- }
275
- // Check if we need to truncate and ensure STRING_FORMAT is respected
276
- const buffer = sharedTextEncoder.encode(str);
277
- if (buffer.length <= MAX_OUTPUT_SIZE) {
278
- // Ensure STRING_FORMAT is respected
279
- return sharedTextDecoder.decode(buffer);
280
- }
281
- // Truncate the buffer and ensure a valid string is returned.
282
- // fatal: false means we get U+FFFD at the end if truncation broke the encoding.
283
- const truncatedBuffer = buffer.slice(0, MAX_OUTPUT_SIZE);
284
- let truncatedStr = sharedTextDecoder.decode(truncatedBuffer);
285
- if (truncatedStr.endsWith('\uFFFD')) {
286
- truncatedStr = truncatedStr.slice(0, -1);
287
- }
288
- return `${truncatedStr}... [truncated]`;
296
+ const str = toSafeString(input);
297
+ if (str === "") return "";
298
+ if (isFullAiCaptureEnabled(client)) return str;
299
+ const buffer = sharedTextEncoder.encode(str);
300
+ if (buffer.length <= 2e5) return sharedTextDecoder.decode(buffer);
301
+ const truncatedBuffer = buffer.slice(0, MAX_OUTPUT_SIZE);
302
+ let truncatedStr = sharedTextDecoder.decode(truncatedBuffer);
303
+ if (truncatedStr.endsWith("�")) truncatedStr = truncatedStr.slice(0, -1);
304
+ return `${truncatedStr}... [truncated]`;
289
305
  };
290
306
  /**
291
- * Calculate web search count from raw API response.
292
- *
293
- * Uses a two-tier detection strategy:
294
- * Priority 1 (Exact Count): Count actual web search calls when available
295
- * Priority 2 (Binary Detection): Return 1 if web search indicators are present, 0 otherwise
296
- *
297
- * @param result - Raw API response from any provider (OpenAI, Perplexity, OpenRouter, Gemini, etc.)
298
- * @returns Number of web searches performed (exact count or binary 1/0)
299
- */
307
+ * Calculate web search count from raw API response.
308
+ *
309
+ * Uses a two-tier detection strategy:
310
+ * Priority 1 (Exact Count): Count actual web search calls when available
311
+ * Priority 2 (Binary Detection): Return 1 if web search indicators are present, 0 otherwise
312
+ *
313
+ * @param result - Raw API response from any provider (OpenAI, Perplexity, OpenRouter, Gemini, etc.)
314
+ * @returns Number of web searches performed (exact count or binary 1/0)
315
+ */
300
316
  function calculateWebSearchCount(result) {
301
- if (!result || typeof result !== 'object') {
302
- return 0;
303
- }
304
- // Priority 1: Exact Count
305
- // Check for OpenAI Responses API web_search_call items
306
- if ('output' in result && Array.isArray(result.output)) {
307
- let count = 0;
308
- for (const item of result.output) {
309
- if (typeof item === 'object' && item !== null && 'type' in item && item.type === 'web_search_call') {
310
- count++;
311
- }
312
- }
313
- if (count > 0) {
314
- return count;
315
- }
316
- }
317
- // Priority 2: Binary Detection (1 or 0)
318
- // Check for citations at root level (Perplexity)
319
- if ('citations' in result && Array.isArray(result.citations) && result.citations.length > 0) {
320
- return 1;
321
- }
322
- // Check for search_results at root level (Perplexity via OpenRouter)
323
- if ('search_results' in result && Array.isArray(result.search_results) && result.search_results.length > 0) {
324
- return 1;
325
- }
326
- // Check for usage.search_context_size (Perplexity via OpenRouter)
327
- if ('usage' in result && typeof result.usage === 'object' && result.usage !== null) {
328
- if ('search_context_size' in result.usage && result.usage.search_context_size) {
329
- return 1;
330
- }
331
- }
332
- // Check for annotations with url_citation in choices[].message or choices[].delta (OpenAI/Perplexity)
333
- if ('choices' in result && Array.isArray(result.choices)) {
334
- for (const choice of result.choices) {
335
- if (typeof choice === 'object' && choice !== null) {
336
- // Check both message (non-streaming) and delta (streaming) for annotations
337
- const content = ('message' in choice ? choice.message : null) || ('delta' in choice ? choice.delta : null);
338
- if (typeof content === 'object' && content !== null && 'annotations' in content) {
339
- const annotations = content.annotations;
340
- if (Array.isArray(annotations)) {
341
- const hasUrlCitation = annotations.some(ann => {
342
- return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
343
- });
344
- if (hasUrlCitation) {
345
- return 1;
346
- }
347
- }
348
- }
349
- }
350
- }
351
- }
352
- // Check for annotations in output[].content[] (OpenAI Responses API)
353
- if ('output' in result && Array.isArray(result.output)) {
354
- for (const item of result.output) {
355
- if (typeof item === 'object' && item !== null && 'content' in item) {
356
- const content = item.content;
357
- if (Array.isArray(content)) {
358
- for (const contentItem of content) {
359
- if (typeof contentItem === 'object' && contentItem !== null && 'annotations' in contentItem) {
360
- const annotations = contentItem.annotations;
361
- if (Array.isArray(annotations)) {
362
- const hasUrlCitation = annotations.some(ann => {
363
- return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
364
- });
365
- if (hasUrlCitation) {
366
- return 1;
367
- }
368
- }
369
- }
370
- }
371
- }
372
- }
373
- }
374
- }
375
- // Check for grounding_metadata (Gemini)
376
- if ('candidates' in result && Array.isArray(result.candidates)) {
377
- for (const candidate of result.candidates) {
378
- if (typeof candidate === 'object' && candidate !== null && 'grounding_metadata' in candidate && candidate.grounding_metadata) {
379
- return 1;
380
- }
381
- }
382
- }
383
- return 0;
317
+ if (!result || typeof result !== "object") return 0;
318
+ if ("output" in result && Array.isArray(result.output)) {
319
+ let count = 0;
320
+ for (const item of result.output) if (typeof item === "object" && item !== null && "type" in item && item.type === "web_search_call") count++;
321
+ if (count > 0) return count;
322
+ }
323
+ if ("citations" in result && Array.isArray(result.citations) && result.citations.length > 0) return 1;
324
+ if ("search_results" in result && Array.isArray(result.search_results) && result.search_results.length > 0) return 1;
325
+ if ("usage" in result && typeof result.usage === "object" && result.usage !== null) {
326
+ if ("search_context_size" in result.usage && result.usage.search_context_size) return 1;
327
+ }
328
+ if ("choices" in result && Array.isArray(result.choices)) {
329
+ for (const choice of result.choices) if (typeof choice === "object" && choice !== null) {
330
+ const content = ("message" in choice ? choice.message : null) || ("delta" in choice ? choice.delta : null);
331
+ if (typeof content === "object" && content !== null && "annotations" in content) {
332
+ const annotations = content.annotations;
333
+ if (Array.isArray(annotations)) {
334
+ if (annotations.some((ann) => {
335
+ return typeof ann === "object" && ann !== null && "type" in ann && ann.type === "url_citation";
336
+ })) return 1;
337
+ }
338
+ }
339
+ }
340
+ }
341
+ if ("output" in result && Array.isArray(result.output)) {
342
+ for (const item of result.output) if (typeof item === "object" && item !== null && "content" in item) {
343
+ const content = item.content;
344
+ if (Array.isArray(content)) {
345
+ for (const contentItem of content) if (typeof contentItem === "object" && contentItem !== null && "annotations" in contentItem) {
346
+ const annotations = contentItem.annotations;
347
+ if (Array.isArray(annotations)) {
348
+ if (annotations.some((ann) => {
349
+ return typeof ann === "object" && ann !== null && "type" in ann && ann.type === "url_citation";
350
+ })) return 1;
351
+ }
352
+ }
353
+ }
354
+ }
355
+ }
356
+ if ("candidates" in result && Array.isArray(result.candidates)) {
357
+ for (const candidate of result.candidates) if (typeof candidate === "object" && candidate !== null && "grounding_metadata" in candidate && candidate.grounding_metadata) return 1;
358
+ }
359
+ return 0;
384
360
  }
385
361
  /**
386
- * Extract available tool calls from the request parameters.
387
- * These are the tools provided to the LLM, not the tool calls in the response.
388
- */
362
+ * Extract available tool calls from the request parameters.
363
+ * These are the tools provided to the LLM, not the tool calls in the response.
364
+ */
389
365
  const extractAvailableToolCalls = (provider, params) => {
390
- {
391
- if (params.tools) {
392
- return params.tools;
393
- }
394
- return null;
395
- }
366
+ if (provider === "anthropic") {
367
+ if (params.tools) return params.tools;
368
+ return null;
369
+ } else if (provider === "gemini") {
370
+ if (params.config && params.config.tools) return params.config.tools;
371
+ return null;
372
+ } else if (provider === "openai") {
373
+ if (params.tools) return params.tools;
374
+ return null;
375
+ } else if (provider === "vercel") {
376
+ if (params.tools) return params.tools;
377
+ return null;
378
+ }
379
+ return null;
396
380
  };
397
- exports.AIEvent = void 0;
398
- (function (AIEvent) {
399
- AIEvent["Generation"] = "$ai_generation";
400
- AIEvent["Embedding"] = "$ai_embedding";
401
- })(exports.AIEvent || (exports.AIEvent = {}));
381
+ let AIEvent = /* @__PURE__ */ function(AIEvent) {
382
+ AIEvent["Generation"] = "$ai_generation";
383
+ AIEvent["Embedding"] = "$ai_embedding";
384
+ return AIEvent;
385
+ }({});
402
386
  function sanitizeValues(obj) {
403
- if (obj === undefined || obj === null) {
404
- return obj;
405
- }
406
- const jsonSafe = JSON.parse(JSON.stringify(obj));
407
- if (typeof jsonSafe === 'string') {
408
- // Sanitize lone surrogates by round-tripping through UTF-8
409
- return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
410
- } else if (Array.isArray(jsonSafe)) {
411
- return jsonSafe.map(sanitizeValues);
412
- } else if (jsonSafe && typeof jsonSafe === 'object') {
413
- return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
414
- }
415
- return jsonSafe;
387
+ if (obj === void 0 || obj === null) return obj;
388
+ const jsonSafe = JSON.parse(JSON.stringify(obj));
389
+ if (typeof jsonSafe === "string") return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
390
+ else if (Array.isArray(jsonSafe)) return jsonSafe.map(sanitizeValues);
391
+ else if (jsonSafe && typeof jsonSafe === "object") return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
392
+ return jsonSafe;
416
393
  }
417
-
418
- var version = "8.9.3";
419
-
394
+ //#endregion
395
+ //#region package.json
396
+ var version = "8.10.1";
397
+ //#endregion
398
+ //#region src/serializeError.ts
420
399
  const DEFAULT_MAX_DEPTH = 3;
421
400
  const MAX_STACK_LINES = 20;
422
401
  function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
423
- if (depth < 0 || value === null || typeof value !== 'object') {
424
- return value;
425
- }
426
- if (value instanceof Error) {
427
- const out = {
428
- name: value.name,
429
- message: value.message,
430
- stack: truncateStack(value.stack)
431
- };
432
- for (const key of Object.keys(value)) {
433
- out[key] = serializeError(value[key], depth - 1);
434
- }
435
- if (value.cause !== undefined) {
436
- out.cause = serializeError(value.cause, depth - 1);
437
- }
438
- return out;
439
- }
440
- if (Array.isArray(value)) {
441
- return value.map(item => serializeError(item, depth - 1));
442
- }
443
- return value;
402
+ if (depth < 0 || value === null || typeof value !== "object") return value;
403
+ if (value instanceof Error) {
404
+ const out = {
405
+ name: value.name,
406
+ message: value.message,
407
+ stack: truncateStack(value.stack)
408
+ };
409
+ for (const key of Object.keys(value)) out[key] = serializeError(value[key], depth - 1);
410
+ if (value.cause !== void 0) out.cause = serializeError(value.cause, depth - 1);
411
+ return out;
412
+ }
413
+ if (Array.isArray(value)) return value.map((item) => serializeError(item, depth - 1));
414
+ return value;
444
415
  }
445
416
  function stringifyError(error) {
446
- try {
447
- return JSON.stringify(sanitizeValues(serializeError(error)));
448
- } catch {
449
- if (error instanceof Error) {
450
- return JSON.stringify({
451
- name: error.name,
452
- message: error.message
453
- });
454
- }
455
- return JSON.stringify({
456
- message: String(error)
457
- });
458
- }
417
+ try {
418
+ return JSON.stringify(sanitizeValues(serializeError(error)));
419
+ } catch {
420
+ if (error instanceof Error) return JSON.stringify({
421
+ name: error.name,
422
+ message: error.message
423
+ });
424
+ return JSON.stringify({ message: String(error) });
425
+ }
459
426
  }
460
427
  function truncateStack(stack) {
461
- if (!stack) {
462
- return stack;
463
- }
464
- const lines = stack.split('\n');
465
- if (lines.length <= MAX_STACK_LINES) {
466
- return stack;
467
- }
468
- return [...lines.slice(0, MAX_STACK_LINES), '... (truncated)'].join('\n');
428
+ if (!stack) return stack;
429
+ const lines = stack.split("\n");
430
+ if (lines.length <= MAX_STACK_LINES) return stack;
431
+ return [...lines.slice(0, MAX_STACK_LINES), "... (truncated)"].join("\n");
469
432
  }
470
-
471
- // Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway
472
- // emits its own $ai_generation, so each call would be captured (and, for billable
473
- // products, billed) twice. We only warn — the wrapper's event carries data the
474
- // gateway never sees (groups, custom properties, trace hierarchy).
475
- // Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the
476
- // main repo). gateway.us.posthog.com is live today; the rest are listed ahead of
477
- // any traffic moving to them.
478
- 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'];
479
- // Swap for the dedicated AI Gateway page once it ships.
480
- const GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability';
481
- const extractHost = baseURL => {
482
- try {
483
- // Tolerate bare hosts that omit a scheme, e.g. "gateway.us.posthog.com/v1".
484
- const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
485
- return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
486
- } catch {
487
- return undefined;
488
- }
433
+ //#endregion
434
+ //#region src/gatewayWarning.ts
435
+ const POSTHOG_AI_GATEWAY_HOSTS = [
436
+ "gateway.posthog.com",
437
+ "gateway.us.posthog.com",
438
+ "gateway.eu.posthog.com",
439
+ "ai-gateway.us.posthog.com",
440
+ "ai-gateway.eu.posthog.com"
441
+ ];
442
+ const GATEWAY_DOCS_URL = "https://posthog.com/docs/ai-observability";
443
+ const extractHost = (baseURL) => {
444
+ try {
445
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
446
+ return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
447
+ } catch {
448
+ return;
449
+ }
489
450
  };
490
- const isPostHogAiGatewayUrl = baseURL => {
491
- if (!baseURL) {
492
- return false;
493
- }
494
- const host = extractHost(baseURL);
495
- return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
451
+ const isPostHogAiGatewayUrl = (baseURL) => {
452
+ if (!baseURL) return false;
453
+ const host = extractHost(baseURL);
454
+ return host !== void 0 && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
496
455
  };
497
- // Warns on every gateway call by design: the misconfiguration is impossible to
498
- // miss that way, and a doubled bill is worse than noisy logs.
499
- const warnIfPostHogAiGateway = baseURL => {
500
- if (!isPostHogAiGatewayUrl(baseURL)) {
501
- return;
502
- }
503
- 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}.`);
456
+ const warnIfPostHogAiGateway = (baseURL) => {
457
+ if (!isPostHogAiGatewayUrl(baseURL)) return;
458
+ 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}.`);
504
459
  };
505
-
460
+ //#endregion
461
+ //#region src/captureAiGeneration.ts
506
462
  /**
507
- * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
508
- *
509
- * This is the canonical primitive that every `@posthog/ai` wrapper
510
- * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
511
- * external code can use it directly to instrument LLM calls made through
512
- * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
513
- * same events the SDK wrappers produce.
514
- *
515
- * When `error` is set, the event is captured as an error. If the error is an
516
- * object, it is mutated in place to set `__posthog_previously_captured_error`
517
- * so callers can re-throw the original error reference safely.
518
- */
463
+ * Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
464
+ *
465
+ * This is the canonical primitive that every `@posthog/ai` wrapper
466
+ * (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
467
+ * external code can use it directly to instrument LLM calls made through
468
+ * arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
469
+ * same events the SDK wrappers produce.
470
+ *
471
+ * When `error` is set, the event is captured as an error. If the error is an
472
+ * object, it is mutated in place to set `__posthog_previously_captured_error`
473
+ * so callers can re-throw the original error reference safely.
474
+ */
519
475
  const captureAiGeneration = async (client, options) => {
520
- try {
521
- if (!client.capture) {
522
- return;
523
- }
524
- warnIfPostHogAiGateway(options.baseURL);
525
- const traceId = options.traceId ?? uuid.v4();
526
- const eventType = options.eventType ?? exports.AIEvent.Generation;
527
- const privacyMode = options.privacyMode ?? false;
528
- const usage = options.usage ?? {};
529
- // Check privacy before reading or traversing input/output. Besides avoiding
530
- // needless work, this ensures hostile getters/proxies cannot observe a value
531
- // that the caller explicitly requested us to redact.
532
- const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
533
- const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
534
- const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
535
- let httpStatus = options.httpStatus;
536
- let errorData = {};
537
- if (options.error) {
538
- if (httpStatus === undefined) {
539
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
540
- httpStatus = options.error.status;
541
- } else if (typeof options.error === 'object' && 'statusCode' in options.error && typeof options.error.statusCode === 'number') {
542
- httpStatus = options.error.statusCode;
543
- } else {
544
- httpStatus = 500;
545
- }
546
- }
547
- let exceptionId;
548
- if (client.options?.enableExceptionAutocapture) {
549
- exceptionId = core.uuidv7();
550
- client.captureException(options.error, undefined, {
551
- $ai_trace_id: traceId
552
- }, exceptionId);
553
- if (typeof options.error === 'object') {
554
- ;
555
- options.error.__posthog_previously_captured_error = true;
556
- }
557
- }
558
- errorData = {
559
- $ai_is_error: true,
560
- $ai_error: stringifyError(options.error),
561
- $exception_event_id: exceptionId
562
- };
563
- }
564
- httpStatus = httpStatus ?? 200;
565
- // A configured price applies only to a count the provider reported, so a call with no
566
- // reported usage sends no cost instead of asserting $0. $ai_total_cost_usd sums the sides
567
- // that were priced, which makes it the cost of the known side alone when the other side
568
- // went unreported: a lower bound on the true total, not an assertion of it.
569
- const costOverrideData = {};
570
- if (options.costOverride) {
571
- if (usage.inputTokens !== undefined) {
572
- costOverrideData.$ai_input_cost_usd = (options.costOverride.inputCost ?? 0) * usage.inputTokens;
573
- }
574
- if (usage.outputTokens !== undefined) {
575
- costOverrideData.$ai_output_cost_usd = (options.costOverride.outputCost ?? 0) * usage.outputTokens;
576
- }
577
- if (Object.keys(costOverrideData).length > 0) {
578
- costOverrideData.$ai_total_cost_usd = (costOverrideData.$ai_input_cost_usd ?? 0) + (costOverrideData.$ai_output_cost_usd ?? 0);
579
- }
580
- }
581
- // The caller's own token counts override the SDK-derived ones further down, via the
582
- // `options.properties` spread.
583
- const tokensOverridden = hasTokenOverrides(options.properties);
584
- const additionalTokenValues = {
585
- ...(usage.reasoningTokens ? {
586
- $ai_reasoning_tokens: usage.reasoningTokens
587
- } : {}),
588
- ...(usage.cacheReadInputTokens ? {
589
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
590
- } : {}),
591
- ...(usage.cacheCreationInputTokens ? {
592
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
593
- } : {}),
594
- // Checked against undefined rather than truthiness, because false is the meaningful
595
- // value here and a truthiness guard would drop it.
596
- //
597
- // Dropped entirely when the caller overrides the token counts: the flag describes how
598
- // the SDK-derived counts relate to each other, so against passthrough counts it can be
599
- // wrong in the expensive direction. Declaring inclusive over counts that are actually
600
- // exclusive makes ingestion subtract the cache pool that was never in the input. A
601
- // caller who knows their own accounting model can still pass
602
- // `$ai_cache_reporting_exclusive` themselves, and that value wins.
603
- ...(usage.cacheReportingExclusive !== undefined && !tokensOverridden ? {
604
- $ai_cache_reporting_exclusive: usage.cacheReportingExclusive
605
- } : {}),
606
- ...(usage.webSearchCount ? {
607
- $ai_web_search_count: usage.webSearchCount
608
- } : {}),
609
- ...(usage.rawUsage ? {
610
- $ai_usage: usage.rawUsage
611
- } : {})
612
- };
613
- const properties = {
614
- $ai_lib: 'posthog-ai',
615
- $ai_lib_version: version,
616
- $ai_provider: options.providerOverride ?? options.provider,
617
- $ai_model: options.modelOverride ?? options.model,
618
- $ai_model_parameters: options.modelParameters ?? {},
619
- $ai_input: safeInput,
620
- $ai_output_choices: safeOutput,
621
- $ai_http_status: httpStatus,
622
- ...(usage.inputTokens !== undefined ? {
623
- $ai_input_tokens: usage.inputTokens
624
- } : {}),
625
- ...(usage.outputTokens !== undefined ? {
626
- $ai_output_tokens: usage.outputTokens
627
- } : {}),
628
- ...additionalTokenValues,
629
- ...(options.latency !== undefined ? {
630
- $ai_latency: options.latency
631
- } : {}),
632
- ...(options.timeToFirstToken !== undefined ? {
633
- $ai_time_to_first_token: options.timeToFirstToken
634
- } : {}),
635
- $ai_trace_id: traceId,
636
- $ai_base_url: options.baseURL ?? '',
637
- ...options.properties,
638
- $ai_tokens_source: getTokensSource(options.properties),
639
- ...(options.distinctId ? {} : {
640
- $process_person_profile: false
641
- }),
642
- ...(options.stopReason ? {
643
- $ai_stop_reason: options.stopReason
644
- } : {}),
645
- ...(options.tools ? {
646
- $ai_tools: options.tools
647
- } : {}),
648
- ...(options.completionId ? {
649
- $ai_completion_id: options.completionId
650
- } : {}),
651
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
652
- $ai_provider_metadata: options.providerMetadata
653
- } : {}),
654
- ...errorData,
655
- ...costOverrideData
656
- };
657
- const event = {
658
- distinctId: options.distinctId ?? traceId,
659
- event: eventType,
660
- properties,
661
- groups: options.groups
662
- };
663
- if (options.captureImmediate) {
664
- await captureAiEventImmediate(client, event);
665
- } else {
666
- captureAiEvent(client, event);
667
- }
668
- } catch (error) {
669
- // Telemetry failures must never affect the instrumented provider call.
670
- console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
671
- }
476
+ try {
477
+ if (!client.capture) return;
478
+ warnIfPostHogAiGateway(options.baseURL);
479
+ const traceId = options.traceId ?? (0, uuid.v4)();
480
+ const eventType = options.eventType ?? "$ai_generation";
481
+ const privacyMode = options.privacyMode ?? false;
482
+ const usage = options.usage ?? {};
483
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
484
+ const safeInput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.input);
485
+ const safeOutput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.output);
486
+ let httpStatus = options.httpStatus;
487
+ let errorData = {};
488
+ if (options.error) {
489
+ if (httpStatus === void 0) {
490
+ if (typeof options.error === "object" && "status" in options.error && typeof options.error.status === "number") httpStatus = options.error.status;
491
+ else if (typeof options.error === "object" && "statusCode" in options.error && typeof options.error.statusCode === "number") httpStatus = options.error.statusCode;
492
+ else httpStatus = 500;
493
+ }
494
+ let exceptionId;
495
+ if (client.options?.enableExceptionAutocapture) {
496
+ exceptionId = (0, _posthog_core.uuidv7)();
497
+ client.captureException(options.error, void 0, { $ai_trace_id: traceId }, exceptionId);
498
+ if (typeof options.error === "object") options.error.__posthog_previously_captured_error = true;
499
+ }
500
+ errorData = {
501
+ $ai_is_error: true,
502
+ $ai_error: stringifyError(options.error),
503
+ $exception_event_id: exceptionId
504
+ };
505
+ }
506
+ httpStatus = httpStatus ?? 200;
507
+ const costOverrideData = {};
508
+ if (options.costOverride) {
509
+ if (usage.inputTokens !== void 0) costOverrideData.$ai_input_cost_usd = (options.costOverride.inputCost ?? 0) * usage.inputTokens;
510
+ if (usage.outputTokens !== void 0) costOverrideData.$ai_output_cost_usd = (options.costOverride.outputCost ?? 0) * usage.outputTokens;
511
+ if (Object.keys(costOverrideData).length > 0) costOverrideData.$ai_total_cost_usd = (costOverrideData.$ai_input_cost_usd ?? 0) + (costOverrideData.$ai_output_cost_usd ?? 0);
512
+ }
513
+ const tokensOverridden = hasTokenOverrides(options.properties);
514
+ const additionalTokenValues = {
515
+ ...usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {},
516
+ ...usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {},
517
+ ...usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {},
518
+ ...usage.cacheReportingExclusive !== void 0 && !tokensOverridden ? { $ai_cache_reporting_exclusive: usage.cacheReportingExclusive } : {},
519
+ ...usage.webSearchCount ? { $ai_web_search_count: usage.webSearchCount } : {},
520
+ ...usage.rawUsage ? { $ai_usage: usage.rawUsage } : {}
521
+ };
522
+ const properties = {
523
+ $ai_lib: "posthog-ai",
524
+ $ai_lib_version: version,
525
+ $ai_provider: options.providerOverride ?? options.provider,
526
+ $ai_model: options.modelOverride ?? options.model,
527
+ $ai_model_parameters: options.modelParameters ?? {},
528
+ $ai_input: safeInput,
529
+ $ai_output_choices: safeOutput,
530
+ $ai_http_status: httpStatus,
531
+ ...usage.inputTokens !== void 0 ? { $ai_input_tokens: usage.inputTokens } : {},
532
+ ...usage.outputTokens !== void 0 ? { $ai_output_tokens: usage.outputTokens } : {},
533
+ ...additionalTokenValues,
534
+ ...options.latency !== void 0 ? { $ai_latency: options.latency } : {},
535
+ ...options.timeToFirstToken !== void 0 ? { $ai_time_to_first_token: options.timeToFirstToken } : {},
536
+ $ai_trace_id: traceId,
537
+ ...options.baseURL === null ? {} : { $ai_base_url: options.baseURL ?? "" },
538
+ ...options.properties,
539
+ $ai_tokens_source: getTokensSource(options.properties),
540
+ ...options.distinctId ? {} : { $process_person_profile: false },
541
+ ...options.stopReason ? { $ai_stop_reason: options.stopReason } : {},
542
+ ...options.tools ? { $ai_tools: options.tools } : {},
543
+ ...options.completionId ? { $ai_completion_id: options.completionId } : {},
544
+ ...options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? { $ai_provider_metadata: options.providerMetadata } : {},
545
+ ...errorData,
546
+ ...costOverrideData
547
+ };
548
+ const event = {
549
+ distinctId: options.distinctId ?? traceId,
550
+ event: eventType,
551
+ properties,
552
+ groups: options.groups
553
+ };
554
+ if (options.captureImmediate) await captureAiEventImmediate(client, event);
555
+ else captureAiEvent(client, event);
556
+ } catch (error) {
557
+ try {
558
+ options.onError?.(error);
559
+ } catch {}
560
+ console.warn("[PostHog AI] Failed to capture generation telemetry:", error);
561
+ }
672
562
  };
673
-
674
- // Type guards
563
+ //#endregion
564
+ //#region src/vercel/middleware.ts
675
565
  function isV3Model(model) {
676
- return model.specificationVersion === 'v3';
566
+ return model.specificationVersion === "v3";
677
567
  }
678
568
  function getSpecificationVersion(model) {
679
- if (typeof model === 'object' && model !== null && 'specificationVersion' in model) {
680
- return model.specificationVersion;
681
- }
682
- return undefined;
569
+ if (typeof model === "object" && model !== null && "specificationVersion" in model) return model.specificationVersion;
683
570
  }
684
571
  const redactFileData = (data, mediaType, client) => {
685
- if (data instanceof URL) {
686
- return isFullAiCaptureEnabled(client) ? data.toString() : redactBase64DataUrl(data.toString(), data.protocol === 'data:' ? mediaType : undefined);
687
- }
688
- if (isString(data)) {
689
- return isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mediaType);
690
- }
691
- return undefined;
572
+ if (data instanceof URL) return isFullAiCaptureEnabled(client) ? data.toString() : redactBase64DataUrl(data.toString(), data.protocol === "data:" ? mediaType : void 0);
573
+ if (isString(data)) return isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mediaType);
692
574
  };
693
- const mapVercelParams = params => {
694
- return {
695
- temperature: params.temperature,
696
- max_output_tokens: params.maxOutputTokens,
697
- top_p: params.topP,
698
- frequency_penalty: params.frequencyPenalty,
699
- presence_penalty: params.presencePenalty,
700
- stop: params.stopSequences,
701
- stream: params.stream
702
- };
575
+ const mapVercelParams = (params) => {
576
+ return {
577
+ temperature: params.temperature,
578
+ max_output_tokens: params.maxOutputTokens,
579
+ top_p: params.topP,
580
+ frequency_penalty: params.frequencyPenalty,
581
+ presence_penalty: params.presencePenalty,
582
+ stop: params.stopSequences,
583
+ stream: params.stream
584
+ };
703
585
  };
704
586
  const mapVercelPrompt = (messages, client) => {
705
- // Map and truncate individual content
706
- const inputs = messages.map(message => {
707
- let content;
708
- // Handle system role which has string content
709
- if (message.role === 'system') {
710
- content = [{
711
- type: 'text',
712
- text: truncate(toContentString(message.content), client)
713
- }];
714
- } else {
715
- // Handle other roles which have array content
716
- if (Array.isArray(message.content)) {
717
- content = message.content.map(c => {
718
- if (c.type === 'text') {
719
- return {
720
- type: 'text',
721
- text: truncate(c.text, client)
722
- };
723
- } else if (c.type === 'file') {
724
- // Redact base64 data URLs and raw base64 to prevent oversized events
725
- const fileData = redactFileData(c.data, c.mediaType, client) ?? 'raw files not supported';
726
- return {
727
- type: 'file',
728
- file: fileData,
729
- mediaType: c.mediaType
730
- };
731
- } else if (c.type === 'reasoning') {
732
- return {
733
- type: 'reasoning',
734
- text: truncate(c.text, client)
735
- };
736
- } else if (c.type === 'tool-call') {
737
- return {
738
- type: 'tool-call',
739
- toolCallId: c.toolCallId,
740
- toolName: c.toolName,
741
- input: c.input
742
- };
743
- } else if (c.type === 'tool-result') {
744
- return {
745
- type: 'tool-result',
746
- toolCallId: c.toolCallId,
747
- toolName: c.toolName,
748
- output: sanitizeVercel(c.output, client),
749
- isError: c.isError
750
- };
751
- }
752
- return {
753
- type: 'text',
754
- text: ''
755
- };
756
- });
757
- } else {
758
- // Fallback for non-array content
759
- content = [{
760
- type: 'text',
761
- text: truncate(toContentString(message.content), client)
762
- }];
763
- }
764
- }
765
- return {
766
- role: message.role,
767
- content
768
- };
769
- });
770
- // Full AI capture means no truncation of any kind; the aggregate trim below exists
771
- // only to keep the default-mode payload under MAX_OUTPUT_SIZE.
772
- if (isFullAiCaptureEnabled(client)) {
773
- return inputs;
774
- }
775
- try {
776
- // Trim the inputs array until its serialized JSON size fits within MAX_OUTPUT_SIZE.
777
- // Pre-compute each message's byte size once so we can shift by accumulated budget
778
- // in a single linear pass, instead of re-stringifying the whole array per iteration.
779
- const messageSizes = inputs.map(m => utf8ByteLength(JSON.stringify(m)));
780
- // Account for the surrounding `[` `]` plus a comma between each pair of elements.
781
- let totalBytes = 2 + Math.max(0, messageSizes.length - 1);
782
- for (const size of messageSizes) {
783
- totalBytes += size;
784
- }
785
- let removedCount = 0;
786
- while (totalBytes > MAX_OUTPUT_SIZE && removedCount < messageSizes.length) {
787
- totalBytes -= messageSizes[removedCount];
788
- // Each removed message past the first also drops the comma that joined it.
789
- if (removedCount < messageSizes.length - 1) {
790
- totalBytes -= 1;
791
- }
792
- removedCount++;
793
- }
794
- if (removedCount > 0) {
795
- inputs.splice(0, removedCount);
796
- // Add one placeholder to indicate how many were removed
797
- inputs.unshift({
798
- role: 'posthog',
799
- content: `[${removedCount} message${removedCount === 1 ? '' : 's'} removed due to size limit]`
800
- });
801
- }
802
- } catch (error) {
803
- console.error('Error stringifying inputs', error);
804
- return [{
805
- role: 'posthog',
806
- content: 'An error occurred while processing your request. Please try again.'
807
- }];
808
- }
809
- return inputs;
587
+ const inputs = messages.map((message) => {
588
+ let content;
589
+ if (message.role === "system") content = [{
590
+ type: "text",
591
+ text: truncate(toContentString(message.content), client)
592
+ }];
593
+ else if (Array.isArray(message.content)) content = message.content.map((c) => {
594
+ if (c.type === "text") return {
595
+ type: "text",
596
+ text: truncate(c.text, client)
597
+ };
598
+ else if (c.type === "file") return {
599
+ type: "file",
600
+ file: redactFileData(c.data, c.mediaType, client) ?? "raw files not supported",
601
+ mediaType: c.mediaType
602
+ };
603
+ else if (c.type === "reasoning") return {
604
+ type: "reasoning",
605
+ text: truncate(c.text, client)
606
+ };
607
+ else if (c.type === "tool-call") return {
608
+ type: "tool-call",
609
+ toolCallId: c.toolCallId,
610
+ toolName: c.toolName,
611
+ input: c.input
612
+ };
613
+ else if (c.type === "tool-result") return {
614
+ type: "tool-result",
615
+ toolCallId: c.toolCallId,
616
+ toolName: c.toolName,
617
+ output: sanitizeVercel(c.output, client),
618
+ isError: c.isError
619
+ };
620
+ return {
621
+ type: "text",
622
+ text: ""
623
+ };
624
+ });
625
+ else content = [{
626
+ type: "text",
627
+ text: truncate(toContentString(message.content), client)
628
+ }];
629
+ return {
630
+ role: message.role,
631
+ content
632
+ };
633
+ });
634
+ if (isFullAiCaptureEnabled(client)) return inputs;
635
+ try {
636
+ const messageSizes = inputs.map((m) => utf8ByteLength(JSON.stringify(m)));
637
+ let totalBytes = 2 + Math.max(0, messageSizes.length - 1);
638
+ for (const size of messageSizes) totalBytes += size;
639
+ let removedCount = 0;
640
+ while (totalBytes > 2e5 && removedCount < messageSizes.length) {
641
+ totalBytes -= messageSizes[removedCount];
642
+ if (removedCount < messageSizes.length - 1) totalBytes -= 1;
643
+ removedCount++;
644
+ }
645
+ if (removedCount > 0) {
646
+ inputs.splice(0, removedCount);
647
+ inputs.unshift({
648
+ role: "posthog",
649
+ content: `[${removedCount} message${removedCount === 1 ? "" : "s"} removed due to size limit]`
650
+ });
651
+ }
652
+ } catch (error) {
653
+ console.error("Error stringifying inputs", error);
654
+ return [{
655
+ role: "posthog",
656
+ content: "An error occurred while processing your request. Please try again."
657
+ }];
658
+ }
659
+ return inputs;
810
660
  };
811
661
  const mapVercelOutput = (result, client) => {
812
- const content = result.map(item => {
813
- if (item.type === 'text') {
814
- return {
815
- type: 'text',
816
- text: truncate(item.text, client)
817
- };
818
- }
819
- if (item.type === 'tool-call') {
820
- const toolCall = item;
821
- const rawArgs = toolCall.input ?? toolCall.args ?? toolCall.arguments ?? {};
822
- return {
823
- type: 'tool-call',
824
- id: item.toolCallId,
825
- function: {
826
- name: item.toolName,
827
- arguments: typeof rawArgs === 'string' ? rawArgs : JSON.stringify(rawArgs)
828
- }
829
- };
830
- }
831
- if (item.type === 'reasoning') {
832
- return {
833
- type: 'reasoning',
834
- text: truncate(item.text, client)
835
- };
836
- }
837
- if (item.type === 'file') {
838
- // Handle files similar to input mapping - avoid large base64 data
839
- let fileData = redactFileData(item.data, item.mediaType, client) ?? `[binary ${item.mediaType} file]`;
840
- // Skipped under full AI capture: media stays untouched, so no placeholder swap either.
841
- if (!isFullAiCaptureEnabled(client) && typeof item.data === 'string' && fileData === item.data && item.data.length > 1000) {
842
- fileData = `[${item.mediaType} file - ${item.data.length} bytes]`;
843
- }
844
- return {
845
- type: 'file',
846
- name: 'generated_file',
847
- mediaType: item.mediaType,
848
- data: fileData
849
- };
850
- }
851
- if (item.type === 'source') {
852
- return {
853
- type: 'source',
854
- sourceType: item.sourceType,
855
- id: item.id,
856
- url: item.url || '',
857
- title: item.title || ''
858
- };
859
- }
860
- // Fallback for unknown types - try to extract text if possible
861
- return {
862
- type: 'text',
863
- text: truncate(JSON.stringify(item), client)
864
- };
865
- });
866
- if (content.length > 0) {
867
- return [{
868
- role: 'assistant',
869
- content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
870
- }];
871
- }
872
- // otherwise stringify and truncate
873
- try {
874
- const jsonOutput = JSON.stringify(result);
875
- return [{
876
- content: truncate(jsonOutput, client),
877
- role: 'assistant'
878
- }];
879
- } catch {
880
- console.error('Error stringifying output');
881
- return [];
882
- }
662
+ const content = result.map((item) => {
663
+ if (item.type === "text") return {
664
+ type: "text",
665
+ text: truncate(item.text, client)
666
+ };
667
+ if (item.type === "tool-call") {
668
+ const toolCall = item;
669
+ const rawArgs = toolCall.input ?? toolCall.args ?? toolCall.arguments ?? {};
670
+ return {
671
+ type: "tool-call",
672
+ id: item.toolCallId,
673
+ function: {
674
+ name: item.toolName,
675
+ arguments: typeof rawArgs === "string" ? rawArgs : JSON.stringify(rawArgs)
676
+ }
677
+ };
678
+ }
679
+ if (item.type === "reasoning") return {
680
+ type: "reasoning",
681
+ text: truncate(item.text, client)
682
+ };
683
+ if (item.type === "file") {
684
+ let fileData = redactFileData(item.data, item.mediaType, client) ?? `[binary ${item.mediaType} file]`;
685
+ if (!isFullAiCaptureEnabled(client) && typeof item.data === "string" && fileData === item.data && item.data.length > 1e3) fileData = `[${item.mediaType} file - ${item.data.length} bytes]`;
686
+ return {
687
+ type: "file",
688
+ name: "generated_file",
689
+ mediaType: item.mediaType,
690
+ data: fileData
691
+ };
692
+ }
693
+ if (item.type === "source") return {
694
+ type: "source",
695
+ sourceType: item.sourceType,
696
+ id: item.id,
697
+ url: item.url || "",
698
+ title: item.title || ""
699
+ };
700
+ return {
701
+ type: "text",
702
+ text: truncate(JSON.stringify(item), client)
703
+ };
704
+ });
705
+ if (content.length > 0) return [{
706
+ role: "assistant",
707
+ content: content.length === 1 && content[0].type === "text" ? content[0].text : content
708
+ }];
709
+ try {
710
+ const jsonOutput = JSON.stringify(result);
711
+ return [{
712
+ content: truncate(jsonOutput, client),
713
+ role: "assistant"
714
+ }];
715
+ } catch {
716
+ console.error("Error stringifying output");
717
+ return [];
718
+ }
883
719
  };
884
- const extractProvider = model => {
885
- const provider = model.provider.toLowerCase();
886
- const providerName = provider.split('.')[0];
887
- return providerName;
720
+ const extractProvider = (model) => {
721
+ return model.provider.toLowerCase().split(".")[0];
888
722
  };
889
723
  /**
890
- * Recover the base URL so gateway calls self-identify via `$ai_base_url` (dedup
891
- * keys on it). The spec exposes none, so we read the off-spec provider `config`:
892
- * `@ai-sdk/anthropic` keeps a `config.baseURL` string; `@ai-sdk/openai`/
893
- * `openai-compatible` bury it in a `config.url({ path })` closure. Unknown shapes
894
- * degrade to `''` — those providers (or a custom `fetch`) stay invisible to dedup.
895
- */
896
- const extractBaseURL = model => {
897
- try {
898
- const config = model.config;
899
- if (!isObject(config)) {
900
- return '';
901
- }
902
- if (isString(config.baseURL)) {
903
- return config.baseURL;
904
- }
905
- const urlFn = config.url;
906
- if (typeof urlFn === 'function') {
907
- const url = urlFn({
908
- path: '',
909
- modelId: model.modelId
910
- });
911
- return isString(url) ? url : '';
912
- }
913
- } catch {
914
- // Unknown config shape or url() threw.
915
- }
916
- return '';
724
+ * Recover the base URL so gateway calls self-identify via `$ai_base_url` (dedup
725
+ * keys on it). The spec exposes none, so we read the off-spec provider `config`:
726
+ * `@ai-sdk/anthropic` keeps a `config.baseURL` string; `@ai-sdk/openai`/
727
+ * `openai-compatible` bury it in a `config.url({ path })` closure. Unknown shapes
728
+ * degrade to `''` — those providers (or a custom `fetch`) stay invisible to dedup.
729
+ */
730
+ const extractBaseURL = (model) => {
731
+ try {
732
+ const config = model.config;
733
+ if (!isObject(config)) return "";
734
+ if (isString(config.baseURL)) return config.baseURL;
735
+ const urlFn = config.url;
736
+ if (typeof urlFn === "function") {
737
+ const url = urlFn({
738
+ path: "",
739
+ modelId: model.modelId
740
+ });
741
+ return isString(url) ? url : "";
742
+ }
743
+ } catch {}
744
+ return "";
917
745
  };
918
- // Extract web search count from provider metadata (works for both V2 and V3)
919
746
  const extractWebSearchCount = (providerMetadata, usage) => {
920
- // Try Anthropic-specific extraction
921
- if (providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'server_tool_use' in providerMetadata.anthropic) {
922
- const serverToolUse = providerMetadata.anthropic.server_tool_use;
923
- if (serverToolUse && typeof serverToolUse === 'object' && 'web_search_requests' in serverToolUse && typeof serverToolUse.web_search_requests === 'number') {
924
- return serverToolUse.web_search_requests;
925
- }
926
- }
927
- // Fall back to generic calculation
928
- return calculateWebSearchCount({
929
- usage,
930
- providerMetadata
931
- });
747
+ if (providerMetadata && typeof providerMetadata === "object" && "anthropic" in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === "object" && "server_tool_use" in providerMetadata.anthropic) {
748
+ const serverToolUse = providerMetadata.anthropic.server_tool_use;
749
+ if (serverToolUse && typeof serverToolUse === "object" && "web_search_requests" in serverToolUse && typeof serverToolUse.web_search_requests === "number") return serverToolUse.web_search_requests;
750
+ }
751
+ return calculateWebSearchCount({
752
+ usage,
753
+ providerMetadata
754
+ });
932
755
  };
933
- // Helper to extract numeric token value from V2 (number) or V3 (object with .total) usage formats
934
- const extractTokenCount = value => {
935
- if (typeof value === 'number') {
936
- return value;
937
- }
938
- if (value && typeof value === 'object' && 'total' in value && typeof value.total === 'number') {
939
- return value.total;
940
- }
941
- return undefined;
756
+ const extractTokenCount = (value) => {
757
+ if (typeof value === "number") return value;
758
+ if (value && typeof value === "object" && "total" in value && typeof value.total === "number") return value.total;
942
759
  };
943
760
  const extractUsageToken = (usage, topLevelKey, nestedObjectKey, nestedValueKey) => {
944
- if (topLevelKey in usage) {
945
- return usage[topLevelKey];
946
- }
947
- const nestedTokens = usage[nestedObjectKey];
948
- if (nestedTokens && typeof nestedTokens === 'object' && nestedValueKey in nestedTokens) {
949
- return nestedTokens[nestedValueKey];
950
- }
951
- return undefined;
761
+ if (topLevelKey in usage) return usage[topLevelKey];
762
+ const nestedTokens = usage[nestedObjectKey];
763
+ if (nestedTokens && typeof nestedTokens === "object" && nestedValueKey in nestedTokens) return nestedTokens[nestedValueKey];
952
764
  };
953
- // Helper to extract reasoning tokens from V2 (usage.reasoningTokens) or V3 (usage.outputTokens.reasoning)
954
- const extractReasoningTokens = usage => extractUsageToken(usage, 'reasoningTokens', 'outputTokens', 'reasoning');
955
- // Helper to extract cached input tokens from V2 (usage.cachedInputTokens) or V3 (usage.inputTokens.cacheRead)
956
- const extractCacheReadTokens = usage => extractUsageToken(usage, 'cachedInputTokens', 'inputTokens', 'cacheRead');
957
- // Helper to extract cache write tokens from V3 (usage.inputTokens.cacheWrite). Providers like
958
- // Amazon Bedrock populate this standardized field instead of providerMetadata.anthropic.
959
- const extractCacheWriteTokens = usage => {
960
- if ('inputTokens' in usage && usage.inputTokens && typeof usage.inputTokens === 'object' && 'cacheWrite' in usage.inputTokens) {
961
- return usage.inputTokens.cacheWrite;
962
- }
963
- return undefined;
765
+ const extractReasoningTokens = (usage) => extractUsageToken(usage, "reasoningTokens", "outputTokens", "reasoning");
766
+ const extractCacheReadTokens = (usage) => extractUsageToken(usage, "cachedInputTokens", "inputTokens", "cacheRead");
767
+ const extractCacheWriteTokens = (usage) => {
768
+ if ("inputTokens" in usage && usage.inputTokens && typeof usage.inputTokens === "object" && "cacheWrite" in usage.inputTokens) return usage.inputTokens.cacheWrite;
964
769
  };
965
- // Extract additional token values from provider metadata, with a V3 standardized fallback
966
- // (e.g. Amazon Bedrock exposes cache write tokens via usage.inputTokens.cacheWrite rather
967
- // than providerMetadata.anthropic.cacheCreationInputTokens). A cacheWrite of 0 is treated
968
- // as absent so we preserve the pre-fallback event shape on providers that simply omit the
969
- // field — consumers downstream saw `$ai_cache_creation_input_tokens` missing, not 0.
970
770
  const extractAdditionalTokenValues = (providerMetadata, usage) => {
971
- if (providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'cacheCreationInputTokens' in providerMetadata.anthropic) {
972
- return {
973
- cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
974
- };
975
- }
976
- if (usage && typeof usage === 'object') {
977
- const cacheWrite = extractCacheWriteTokens(usage);
978
- if (typeof cacheWrite === 'number' && cacheWrite > 0) {
979
- return {
980
- cacheCreationInputTokens: cacheWrite
981
- };
982
- }
983
- }
984
- return {};
771
+ if (providerMetadata && typeof providerMetadata === "object" && "anthropic" in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === "object" && "cacheCreationInputTokens" in providerMetadata.anthropic) return { cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens };
772
+ if (usage && typeof usage === "object") {
773
+ const cacheWrite = extractCacheWriteTokens(usage);
774
+ if (typeof cacheWrite === "number" && cacheWrite > 0) return { cacheCreationInputTokens: cacheWrite };
775
+ }
776
+ return {};
985
777
  };
986
- // Detects Anthropic Claude regardless of host (direct Anthropic, Amazon Bedrock, Google Vertex, etc.).
987
- // The server applies exclusive cache token accounting based on the model name, so any Claude model
988
- // needs its V3 input tokens adjusted to exclude cache tokens — not just those routed through a
989
- // provider whose name contains "anthropic". Accepts the resolved modelId string (not the raw model)
990
- // so it sees the same id the server does after posthogModelOverride / response.modelId fallbacks.
991
778
  const isAnthropicClaudeModel = (modelId, provider) => {
992
- if (provider.toLowerCase().includes('anthropic')) {
993
- return true;
994
- }
995
- return /claude|anthropic/i.test(modelId);
779
+ if (provider.toLowerCase().includes("anthropic")) return true;
780
+ return /claude|anthropic/i.test(modelId);
996
781
  };
997
- // For Anthropic providers in V3, inputTokens.total is the sum of all tokens (uncached + cache read + cache write).
998
- // Our cost calculation expects inputTokens to be only the uncached portion for Anthropic.
999
- // This helper subtracts cache tokens from inputTokens for Anthropic V3 models.
1000
782
  const adjustAnthropicV3CacheTokens = (model, modelId, provider, usage) => {
1001
- if (isV3Model(model) && isAnthropicClaudeModel(modelId, provider)) {
1002
- const cacheReadTokens = usage.cacheReadInputTokens || 0;
1003
- const cacheWriteTokens = usage.cacheCreationInputTokens || 0;
1004
- const cacheTokens = cacheReadTokens + cacheWriteTokens;
1005
- if (usage.inputTokens && cacheTokens > 0) {
1006
- usage.inputTokens = Math.max(usage.inputTokens - cacheTokens, 0);
1007
- }
1008
- }
783
+ if (isV3Model(model) && isAnthropicClaudeModel(modelId, provider)) {
784
+ const cacheTokens = (usage.cacheReadInputTokens || 0) + (usage.cacheCreationInputTokens || 0);
785
+ if (usage.inputTokens && cacheTokens > 0) usage.inputTokens = Math.max(usage.inputTokens - cacheTokens, 0);
786
+ }
1009
787
  };
1010
788
  /**
1011
- * Wraps a Vercel AI SDK language model (V2 or V3) with PostHog tracing.
1012
- * Automatically detects the model version and applies appropriate instrumentation.
1013
- */
789
+ * Wraps a Vercel AI SDK language model (V2 or V3) with PostHog tracing.
790
+ * Automatically detects the model version and applies appropriate instrumentation.
791
+ */
1014
792
  const wrapVercelLanguageModel = (model, phClient, options) => {
1015
- const specificationVersion = getSpecificationVersion(model);
1016
- if (specificationVersion !== 'v2' && specificationVersion !== 'v3') {
1017
- throw new Error(`[PostHog AI] withTracing supports Vercel AI SDK v5 and v6 models only. ` + `Use @ai-sdk/otel with @posthog/ai/otel for AI SDK v7 models.`);
1018
- }
1019
- const traceId = options.posthogTraceId ?? uuid.v4();
1020
- const mergedOptions = {
1021
- ...options,
1022
- posthogTraceId: traceId,
1023
- posthogDistinctId: options.posthogDistinctId,
1024
- posthogProperties: {
1025
- ...options.posthogProperties,
1026
- $ai_framework: 'vercel',
1027
- $ai_framework_version: model.specificationVersion === 'v3' ? '6' : '5'
1028
- }
1029
- };
1030
- // Shared `captureAiGeneration` options for every call site in this wrapper.
1031
- const baseOptions = {
1032
- distinctId: mergedOptions.posthogDistinctId,
1033
- traceId,
1034
- properties: mergedOptions.posthogProperties,
1035
- groups: mergedOptions.posthogGroups,
1036
- privacyMode: mergedOptions.posthogPrivacyMode,
1037
- modelOverride: mergedOptions.posthogModelOverride,
1038
- providerOverride: mergedOptions.posthogProviderOverride,
1039
- costOverride: mergedOptions.posthogCostOverride,
1040
- captureImmediate: mergedOptions.posthogCaptureImmediate
1041
- };
1042
- // Create wrapped model using Object.create to preserve the prototype chain
1043
- // This automatically inherits all properties (including getters) from the model
1044
- const wrappedModel = Object.create(model, {
1045
- doGenerate: {
1046
- value: async params => {
1047
- const startTime = Date.now();
1048
- const mergedParams = {
1049
- ...mergedOptions,
1050
- ...mapVercelParams(params)
1051
- };
1052
- const availableTools = extractAvailableToolCalls('vercel', params);
1053
- const baseURL = extractBaseURL(model);
1054
- try {
1055
- const result = await model.doGenerate(params);
1056
- const modelId = mergedOptions.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
1057
- const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
1058
- // result.content is undefined when the model returns only tool calls with no text output
1059
- const content = mapVercelOutput(result.content ?? [], phClient);
1060
- const latency = (Date.now() - startTime) / 1000;
1061
- const providerMetadata = result.providerMetadata;
1062
- const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, result.usage);
1063
- const webSearchCount = extractWebSearchCount(providerMetadata, result.usage);
1064
- // V2 usage has simple numbers, V3 has objects with .total - normalize both
1065
- const usageObj = result.usage;
1066
- // Extract raw response for providers that include detailed usage metadata
1067
- // For Gemini, candidatesTokensDetails is in result.response.body.usageMetadata
1068
- const rawUsageData = {
1069
- usage: result.usage,
1070
- providerMetadata
1071
- };
1072
- // Include response body usageMetadata if it contains detailed token breakdown (e.g., candidatesTokensDetails)
1073
- if (result.response && typeof result.response === 'object') {
1074
- const responseBody = result.response.body;
1075
- if (responseBody && typeof responseBody === 'object' && 'usageMetadata' in responseBody) {
1076
- rawUsageData.rawResponse = {
1077
- usageMetadata: responseBody.usageMetadata
1078
- };
1079
- }
1080
- }
1081
- const usage = {
1082
- inputTokens: extractTokenCount(result.usage.inputTokens),
1083
- outputTokens: extractTokenCount(result.usage.outputTokens),
1084
- reasoningTokens: extractReasoningTokens(usageObj),
1085
- cacheReadInputTokens: extractCacheReadTokens(usageObj),
1086
- webSearchCount,
1087
- ...additionalTokenValues,
1088
- rawUsage: rawUsageData
1089
- };
1090
- adjustAnthropicV3CacheTokens(model, modelId, provider, usage);
1091
- // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1092
- const rawFinishReason = result.finishReason;
1093
- const finishReasonStr = typeof rawFinishReason === 'string' ? rawFinishReason : rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason ? String(rawFinishReason.unified) : undefined;
1094
- await captureAiGeneration(phClient, {
1095
- ...baseOptions,
1096
- model: modelId,
1097
- provider: provider,
1098
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1099
- output: content,
1100
- latency,
1101
- baseURL,
1102
- modelParameters: getModelParams(mergedParams),
1103
- httpStatus: 200,
1104
- usage,
1105
- stopReason: finishReasonStr,
1106
- tools: availableTools
1107
- });
1108
- return result;
1109
- } catch (error) {
1110
- const modelId = model.modelId;
1111
- await captureAiGeneration(phClient, {
1112
- ...baseOptions,
1113
- model: modelId,
1114
- provider: model.provider,
1115
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1116
- output: [],
1117
- latency: (Date.now() - startTime) / 1000,
1118
- baseURL,
1119
- modelParameters: getModelParams(mergedParams),
1120
- usage: {},
1121
- error: error,
1122
- tools: availableTools
1123
- });
1124
- throw error;
1125
- }
1126
- },
1127
- writable: true,
1128
- configurable: true,
1129
- enumerable: false
1130
- },
1131
- doStream: {
1132
- value: async params => {
1133
- const startTime = Date.now();
1134
- let firstTokenTime;
1135
- let generatedText = '';
1136
- let reasoningText = '';
1137
- let stopReason;
1138
- let usage = {};
1139
- let providerMetadata = undefined;
1140
- const mergedParams = {
1141
- ...mergedOptions,
1142
- ...mapVercelParams(params)
1143
- };
1144
- const modelId = mergedOptions.posthogModelOverride ?? model.modelId;
1145
- const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
1146
- const availableTools = extractAvailableToolCalls('vercel', params);
1147
- const baseURL = extractBaseURL(model);
1148
- // Map to track in-progress tool calls
1149
- const toolCallsInProgress = new Map();
1150
- const captureStreamGeneration = async captureOptions => {
1151
- try {
1152
- await captureAiGeneration(phClient, captureOptions);
1153
- } catch (error) {
1154
- // Telemetry must never change the provider stream's behavior.
1155
- console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1156
- }
1157
- };
1158
- try {
1159
- const {
1160
- stream,
1161
- ...rest
1162
- } = await model.doStream(params);
1163
- const reader = stream.getReader();
1164
- let inBandError;
1165
- let hasInBandError = false;
1166
- let finalizationPromise;
1167
- const observeChunk = chunk => {
1168
- // Handle streaming patterns - compatible with both V2 and V3
1169
- if (chunk.type === 'text-delta') {
1170
- if (firstTokenTime === undefined) {
1171
- firstTokenTime = Date.now();
1172
- }
1173
- generatedText += chunk.delta;
1174
- }
1175
- if (chunk.type === 'reasoning-delta') {
1176
- if (firstTokenTime === undefined) {
1177
- firstTokenTime = Date.now();
1178
- }
1179
- reasoningText += chunk.delta;
1180
- }
1181
- // Handle tool call chunks
1182
- if (chunk.type === 'tool-input-start') {
1183
- if (firstTokenTime === undefined) {
1184
- firstTokenTime = Date.now();
1185
- }
1186
- toolCallsInProgress.set(chunk.id, {
1187
- toolCallId: chunk.id,
1188
- toolName: chunk.toolName,
1189
- input: ''
1190
- });
1191
- }
1192
- if (chunk.type === 'tool-input-delta') {
1193
- const toolCall = toolCallsInProgress.get(chunk.id);
1194
- if (toolCall) {
1195
- toolCall.input += chunk.delta;
1196
- }
1197
- }
1198
- if (chunk.type === 'tool-call') {
1199
- if (firstTokenTime === undefined) {
1200
- firstTokenTime = Date.now();
1201
- }
1202
- toolCallsInProgress.set(chunk.toolCallId, {
1203
- toolCallId: chunk.toolCallId,
1204
- toolName: chunk.toolName,
1205
- input: chunk.input
1206
- });
1207
- }
1208
- if (chunk.type === 'error') {
1209
- hasInBandError = true;
1210
- inBandError = chunk.error;
1211
- }
1212
- if (chunk.type === 'finish') {
1213
- providerMetadata = chunk.providerMetadata;
1214
- const chunkUsage = chunk.usage || {};
1215
- const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
1216
- usage = {
1217
- inputTokens: extractTokenCount(chunk.usage?.inputTokens),
1218
- outputTokens: extractTokenCount(chunk.usage?.outputTokens),
1219
- reasoningTokens: extractReasoningTokens(chunkUsage),
1220
- cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
1221
- ...additionalTokenValues
1222
- };
1223
- // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1224
- const rawFinishReason = chunk.finishReason;
1225
- if (typeof rawFinishReason === 'string') {
1226
- stopReason = rawFinishReason;
1227
- } else if (rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason) {
1228
- stopReason = String(rawFinishReason.unified);
1229
- }
1230
- }
1231
- };
1232
- const finalize = (terminalError, isError = false) => {
1233
- if (finalizationPromise) {
1234
- return finalizationPromise;
1235
- }
1236
- finalizationPromise = (async () => {
1237
- const latency = (Date.now() - startTime) / 1000;
1238
- const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1239
- const content = [];
1240
- if (reasoningText) {
1241
- content.push({
1242
- type: 'reasoning',
1243
- text: truncate(reasoningText, phClient)
1244
- });
1245
- }
1246
- if (generatedText) {
1247
- content.push({
1248
- type: 'text',
1249
- text: truncate(generatedText, phClient)
1250
- });
1251
- }
1252
- for (const toolCall of toolCallsInProgress.values()) {
1253
- if (toolCall.toolName) {
1254
- content.push({
1255
- type: 'tool-call',
1256
- id: toolCall.toolCallId,
1257
- function: {
1258
- name: toolCall.toolName,
1259
- arguments: toolCall.input
1260
- }
1261
- });
1262
- }
1263
- }
1264
- const output = content.length > 0 ? [{
1265
- role: 'assistant',
1266
- content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
1267
- }] : [];
1268
- const webSearchCount = extractWebSearchCount(providerMetadata, usage);
1269
- const finalUsage = {
1270
- ...usage,
1271
- webSearchCount,
1272
- rawUsage: {
1273
- usage,
1274
- providerMetadata
1275
- }
1276
- };
1277
- adjustAnthropicV3CacheTokens(model, modelId, provider, finalUsage);
1278
- const finishError = stopReason === 'error' ? new Error('Vercel AI SDK stream finished with an error') : undefined;
1279
- const error = isError ? terminalError ?? new Error('Vercel AI SDK stream failed') : hasInBandError ? inBandError ?? new Error('Vercel AI SDK stream emitted an error chunk') : finishError;
1280
- await captureStreamGeneration({
1281
- ...baseOptions,
1282
- model: modelId,
1283
- provider: provider,
1284
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1285
- output,
1286
- latency,
1287
- timeToFirstToken,
1288
- baseURL,
1289
- modelParameters: getModelParams(mergedParams),
1290
- httpStatus: error ? undefined : 200,
1291
- usage: finalUsage,
1292
- stopReason,
1293
- error,
1294
- tools: availableTools
1295
- });
1296
- })().catch(error => {
1297
- // Building telemetry must not change the provider stream's behavior.
1298
- console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1299
- });
1300
- return finalizationPromise;
1301
- };
1302
- const instrumentedStream = new ReadableStream({
1303
- async pull(controller) {
1304
- let result;
1305
- try {
1306
- result = await reader.read();
1307
- } catch (error) {
1308
- void finalize(error, true);
1309
- controller.error(error);
1310
- return;
1311
- }
1312
- if (result.done) {
1313
- controller.close();
1314
- void finalize();
1315
- return;
1316
- }
1317
- try {
1318
- observeChunk(result.value);
1319
- } catch {
1320
- // Instrumentation must not alter or suppress provider chunks.
1321
- }
1322
- controller.enqueue(result.value);
1323
- },
1324
- cancel(reason) {
1325
- void finalize(reason ?? new Error('Vercel AI SDK stream was cancelled'), true);
1326
- return reader.cancel(reason);
1327
- }
1328
- }, {
1329
- highWaterMark: 0
1330
- });
1331
- return {
1332
- stream: instrumentedStream,
1333
- ...rest
1334
- };
1335
- } catch (error) {
1336
- await captureStreamGeneration({
1337
- ...baseOptions,
1338
- model: modelId,
1339
- provider: provider,
1340
- input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
1341
- output: [],
1342
- latency: (Date.now() - startTime) / 1000,
1343
- baseURL,
1344
- modelParameters: getModelParams(mergedParams),
1345
- usage: {},
1346
- error: error,
1347
- tools: availableTools
1348
- });
1349
- throw error;
1350
- }
1351
- },
1352
- writable: true,
1353
- configurable: true,
1354
- enumerable: false
1355
- }
1356
- });
1357
- return wrappedModel;
793
+ const specificationVersion = getSpecificationVersion(model);
794
+ if (specificationVersion !== "v2" && specificationVersion !== "v3") throw new Error("[PostHog AI] withTracing supports Vercel AI SDK v5 and v6 models only. Use @ai-sdk/otel with @posthog/ai/otel for AI SDK v7 models.");
795
+ const traceId = options.posthogTraceId ?? (0, uuid.v4)();
796
+ const mergedOptions = {
797
+ ...options,
798
+ posthogTraceId: traceId,
799
+ posthogDistinctId: options.posthogDistinctId,
800
+ posthogProperties: {
801
+ ...options.posthogProperties,
802
+ $ai_framework: "vercel",
803
+ $ai_framework_version: model.specificationVersion === "v3" ? "6" : "5"
804
+ }
805
+ };
806
+ const baseOptions = {
807
+ distinctId: mergedOptions.posthogDistinctId,
808
+ traceId,
809
+ properties: mergedOptions.posthogProperties,
810
+ groups: mergedOptions.posthogGroups,
811
+ privacyMode: mergedOptions.posthogPrivacyMode,
812
+ modelOverride: mergedOptions.posthogModelOverride,
813
+ providerOverride: mergedOptions.posthogProviderOverride,
814
+ costOverride: mergedOptions.posthogCostOverride,
815
+ captureImmediate: mergedOptions.posthogCaptureImmediate
816
+ };
817
+ return Object.create(model, {
818
+ doGenerate: {
819
+ value: async (params) => {
820
+ const startTime = Date.now();
821
+ const mergedParams = {
822
+ ...mergedOptions,
823
+ ...mapVercelParams(params)
824
+ };
825
+ const availableTools = extractAvailableToolCalls("vercel", params);
826
+ const baseURL = extractBaseURL(model);
827
+ try {
828
+ const result = await model.doGenerate(params);
829
+ const modelId = mergedOptions.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
830
+ const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
831
+ const content = mapVercelOutput(result.content ?? [], phClient);
832
+ const latency = (Date.now() - startTime) / 1e3;
833
+ const providerMetadata = result.providerMetadata;
834
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, result.usage);
835
+ const webSearchCount = extractWebSearchCount(providerMetadata, result.usage);
836
+ const usageObj = result.usage;
837
+ const rawUsageData = {
838
+ usage: result.usage,
839
+ providerMetadata
840
+ };
841
+ if (result.response && typeof result.response === "object") {
842
+ const responseBody = result.response.body;
843
+ if (responseBody && typeof responseBody === "object" && "usageMetadata" in responseBody) rawUsageData.rawResponse = { usageMetadata: responseBody.usageMetadata };
844
+ }
845
+ const usage = {
846
+ inputTokens: extractTokenCount(result.usage.inputTokens),
847
+ outputTokens: extractTokenCount(result.usage.outputTokens),
848
+ reasoningTokens: extractReasoningTokens(usageObj),
849
+ cacheReadInputTokens: extractCacheReadTokens(usageObj),
850
+ webSearchCount,
851
+ ...additionalTokenValues,
852
+ rawUsage: rawUsageData
853
+ };
854
+ adjustAnthropicV3CacheTokens(model, modelId, provider, usage);
855
+ const rawFinishReason = result.finishReason;
856
+ const finishReasonStr = typeof rawFinishReason === "string" ? rawFinishReason : rawFinishReason && typeof rawFinishReason === "object" && "unified" in rawFinishReason ? String(rawFinishReason.unified) : void 0;
857
+ await captureAiGeneration(phClient, {
858
+ ...baseOptions,
859
+ model: modelId,
860
+ provider,
861
+ input: mergedOptions.posthogPrivacyMode ? "" : mapVercelPrompt(params.prompt, phClient),
862
+ output: content,
863
+ latency,
864
+ baseURL,
865
+ modelParameters: getModelParams(mergedParams),
866
+ httpStatus: 200,
867
+ usage,
868
+ stopReason: finishReasonStr,
869
+ tools: availableTools
870
+ });
871
+ return result;
872
+ } catch (error) {
873
+ const modelId = model.modelId;
874
+ await captureAiGeneration(phClient, {
875
+ ...baseOptions,
876
+ model: modelId,
877
+ provider: model.provider,
878
+ input: mergedOptions.posthogPrivacyMode ? "" : mapVercelPrompt(params.prompt, phClient),
879
+ output: [],
880
+ latency: (Date.now() - startTime) / 1e3,
881
+ baseURL,
882
+ modelParameters: getModelParams(mergedParams),
883
+ usage: {},
884
+ error,
885
+ tools: availableTools
886
+ });
887
+ throw error;
888
+ }
889
+ },
890
+ writable: true,
891
+ configurable: true,
892
+ enumerable: false
893
+ },
894
+ doStream: {
895
+ value: async (params) => {
896
+ const startTime = Date.now();
897
+ let firstTokenTime;
898
+ let generatedText = "";
899
+ let reasoningText = "";
900
+ let stopReason;
901
+ let usage = {};
902
+ let providerMetadata = void 0;
903
+ const mergedParams = {
904
+ ...mergedOptions,
905
+ ...mapVercelParams(params)
906
+ };
907
+ const modelId = mergedOptions.posthogModelOverride ?? model.modelId;
908
+ const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
909
+ const availableTools = extractAvailableToolCalls("vercel", params);
910
+ const baseURL = extractBaseURL(model);
911
+ const toolCallsInProgress = /* @__PURE__ */ new Map();
912
+ const captureStreamGeneration = async (captureOptions) => {
913
+ try {
914
+ await captureAiGeneration(phClient, captureOptions);
915
+ } catch (error) {
916
+ console.warn("[PostHog AI] Failed to capture Vercel stream telemetry:", error);
917
+ }
918
+ };
919
+ try {
920
+ const { stream, ...rest } = await model.doStream(params);
921
+ const reader = stream.getReader();
922
+ let inBandError;
923
+ let hasInBandError = false;
924
+ let finalizationPromise;
925
+ const observeChunk = (chunk) => {
926
+ if (chunk.type === "text-delta") {
927
+ if (firstTokenTime === void 0) firstTokenTime = Date.now();
928
+ generatedText += chunk.delta;
929
+ }
930
+ if (chunk.type === "reasoning-delta") {
931
+ if (firstTokenTime === void 0) firstTokenTime = Date.now();
932
+ reasoningText += chunk.delta;
933
+ }
934
+ if (chunk.type === "tool-input-start") {
935
+ if (firstTokenTime === void 0) firstTokenTime = Date.now();
936
+ toolCallsInProgress.set(chunk.id, {
937
+ toolCallId: chunk.id,
938
+ toolName: chunk.toolName,
939
+ input: ""
940
+ });
941
+ }
942
+ if (chunk.type === "tool-input-delta") {
943
+ const toolCall = toolCallsInProgress.get(chunk.id);
944
+ if (toolCall) toolCall.input += chunk.delta;
945
+ }
946
+ if (chunk.type === "tool-call") {
947
+ if (firstTokenTime === void 0) firstTokenTime = Date.now();
948
+ toolCallsInProgress.set(chunk.toolCallId, {
949
+ toolCallId: chunk.toolCallId,
950
+ toolName: chunk.toolName,
951
+ input: chunk.input
952
+ });
953
+ }
954
+ if (chunk.type === "error") {
955
+ hasInBandError = true;
956
+ inBandError = chunk.error;
957
+ }
958
+ if (chunk.type === "finish") {
959
+ providerMetadata = chunk.providerMetadata;
960
+ const chunkUsage = chunk.usage || {};
961
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
962
+ usage = {
963
+ inputTokens: extractTokenCount(chunk.usage?.inputTokens),
964
+ outputTokens: extractTokenCount(chunk.usage?.outputTokens),
965
+ reasoningTokens: extractReasoningTokens(chunkUsage),
966
+ cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
967
+ ...additionalTokenValues
968
+ };
969
+ const rawFinishReason = chunk.finishReason;
970
+ if (typeof rawFinishReason === "string") stopReason = rawFinishReason;
971
+ else if (rawFinishReason && typeof rawFinishReason === "object" && "unified" in rawFinishReason) stopReason = String(rawFinishReason.unified);
972
+ }
973
+ };
974
+ const finalize = (terminalError, isError = false) => {
975
+ if (finalizationPromise) return finalizationPromise;
976
+ finalizationPromise = (async () => {
977
+ const latency = (Date.now() - startTime) / 1e3;
978
+ const timeToFirstToken = firstTokenTime !== void 0 ? (firstTokenTime - startTime) / 1e3 : void 0;
979
+ const content = [];
980
+ if (reasoningText) content.push({
981
+ type: "reasoning",
982
+ text: truncate(reasoningText, phClient)
983
+ });
984
+ if (generatedText) content.push({
985
+ type: "text",
986
+ text: truncate(generatedText, phClient)
987
+ });
988
+ for (const toolCall of toolCallsInProgress.values()) if (toolCall.toolName) content.push({
989
+ type: "tool-call",
990
+ id: toolCall.toolCallId,
991
+ function: {
992
+ name: toolCall.toolName,
993
+ arguments: toolCall.input
994
+ }
995
+ });
996
+ const output = content.length > 0 ? [{
997
+ role: "assistant",
998
+ content: content.length === 1 && content[0].type === "text" ? content[0].text : content
999
+ }] : [];
1000
+ const webSearchCount = extractWebSearchCount(providerMetadata, usage);
1001
+ const finalUsage = {
1002
+ ...usage,
1003
+ webSearchCount,
1004
+ rawUsage: {
1005
+ usage,
1006
+ providerMetadata
1007
+ }
1008
+ };
1009
+ adjustAnthropicV3CacheTokens(model, modelId, provider, finalUsage);
1010
+ const error = isError ? terminalError ?? /* @__PURE__ */ new Error("Vercel AI SDK stream failed") : hasInBandError ? inBandError ?? /* @__PURE__ */ new Error("Vercel AI SDK stream emitted an error chunk") : stopReason === "error" ? /* @__PURE__ */ new Error("Vercel AI SDK stream finished with an error") : void 0;
1011
+ await captureStreamGeneration({
1012
+ ...baseOptions,
1013
+ model: modelId,
1014
+ provider,
1015
+ input: mergedOptions.posthogPrivacyMode ? "" : mapVercelPrompt(params.prompt, phClient),
1016
+ output,
1017
+ latency,
1018
+ timeToFirstToken,
1019
+ baseURL,
1020
+ modelParameters: getModelParams(mergedParams),
1021
+ httpStatus: error ? void 0 : 200,
1022
+ usage: finalUsage,
1023
+ stopReason,
1024
+ error,
1025
+ tools: availableTools
1026
+ });
1027
+ })().catch((error) => {
1028
+ console.warn("[PostHog AI] Failed to capture Vercel stream telemetry:", error);
1029
+ });
1030
+ return finalizationPromise;
1031
+ };
1032
+ return {
1033
+ stream: new ReadableStream({
1034
+ async pull(controller) {
1035
+ let result;
1036
+ try {
1037
+ result = await reader.read();
1038
+ } catch (error) {
1039
+ finalize(error, true);
1040
+ controller.error(error);
1041
+ return;
1042
+ }
1043
+ if (result.done) {
1044
+ controller.close();
1045
+ finalize();
1046
+ return;
1047
+ }
1048
+ try {
1049
+ observeChunk(result.value);
1050
+ } catch {}
1051
+ controller.enqueue(result.value);
1052
+ },
1053
+ cancel(reason) {
1054
+ finalize(reason ?? /* @__PURE__ */ new Error("Vercel AI SDK stream was cancelled"), true);
1055
+ return reader.cancel(reason);
1056
+ }
1057
+ }, { highWaterMark: 0 }),
1058
+ ...rest
1059
+ };
1060
+ } catch (error) {
1061
+ await captureStreamGeneration({
1062
+ ...baseOptions,
1063
+ model: modelId,
1064
+ provider,
1065
+ input: mergedOptions.posthogPrivacyMode ? "" : mapVercelPrompt(params.prompt, phClient),
1066
+ output: [],
1067
+ latency: (Date.now() - startTime) / 1e3,
1068
+ baseURL,
1069
+ modelParameters: getModelParams(mergedParams),
1070
+ usage: {},
1071
+ error,
1072
+ tools: availableTools
1073
+ });
1074
+ throw error;
1075
+ }
1076
+ },
1077
+ writable: true,
1078
+ configurable: true,
1079
+ enumerable: false
1080
+ }
1081
+ });
1358
1082
  };
1359
-
1360
- /// <reference lib="dom" />
1361
- const DEFAULT_CACHE_TTL_SECONDS = 300; // 5 minutes
1362
- const DEFAULT_PROMPTS_HOST = 'https://us.posthog.com';
1083
+ //#endregion
1084
+ //#region src/prompts.ts
1085
+ const DEFAULT_CACHE_TTL_SECONDS = 300;
1086
+ const DEFAULT_PROMPTS_HOST = "https://us.posthog.com";
1363
1087
  function normalizeApiKey(value) {
1364
- return typeof value === 'string' ? value.trim() : '';
1088
+ return typeof value === "string" ? value.trim() : "";
1365
1089
  }
1366
1090
  function normalizeHost(value) {
1367
- const normalizedHost = typeof value === 'string' ? value.trim() : '';
1368
- return (normalizedHost || DEFAULT_PROMPTS_HOST).replace(/\/+$/, '');
1091
+ return ((typeof value === "string" ? value.trim() : "") || DEFAULT_PROMPTS_HOST).replace(/\/+$/, "");
1369
1092
  }
1370
1093
  /** Reads config from an API response, tolerating servers that don't send it. */
1371
1094
  function extractConfig(value) {
1372
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1373
- return value;
1374
- }
1375
- return null;
1095
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) return value;
1096
+ return null;
1376
1097
  }
1377
1098
  /** Copied so a caller mutating result.config can't pollute the cache entry later reads are served from. */
1378
1099
  function cloneConfig(config) {
1379
- return config === null ? null : structuredClone(config);
1100
+ return config === null ? null : structuredClone(config);
1380
1101
  }
1381
1102
  function isPromptApiResponse(data) {
1382
- if (typeof data !== 'object' || data === null) {
1383
- return false;
1384
- }
1385
- const record = data;
1386
- return typeof record.prompt === 'string' && typeof record.name === 'string' && typeof record.version === 'number' && (record.label === undefined || typeof record.label === 'string');
1103
+ if (typeof data !== "object" || data === null) return false;
1104
+ const record = data;
1105
+ return typeof record.prompt === "string" && typeof record.name === "string" && typeof record.version === "number" && (record.label === void 0 || typeof record.label === "string");
1387
1106
  }
1388
1107
  function isPromptsWithPostHog(options) {
1389
- return 'posthog' in options;
1108
+ return "posthog" in options;
1390
1109
  }
1391
1110
  /**
1392
- * Prompts class for fetching and compiling LLM prompts from PostHog
1393
- *
1394
- * @example
1395
- * ```ts
1396
- * // With PostHog client
1397
- * const prompts = new Prompts({ posthog })
1398
- *
1399
- * // Or with direct options (no PostHog client needed)
1400
- * const prompts = new Prompts({
1401
- * personalApiKey: 'phx_xxx',
1402
- * projectApiKey: 'phc_xxx',
1403
- * host: 'https://us.posthog.com',
1404
- * })
1405
- *
1406
- * // Fetch with caching and fallback
1407
- * const result = await prompts.get('support-system-prompt', {
1408
- * cacheTtlSeconds: 300,
1409
- * fallback: 'You are a helpful assistant.',
1410
- * })
1411
- *
1412
- * // Or fetch an exact published version
1413
- * const v3 = await prompts.get('support-system-prompt', {
1414
- * version: 3,
1415
- * })
1416
- *
1417
- * // Or fetch the version a label currently points to
1418
- * const prod = await prompts.get('support-system-prompt', {
1419
- * label: 'production',
1420
- * })
1421
- *
1422
- * // Compile with variables
1423
- * const systemPrompt = prompts.compile(result.prompt, {
1424
- * company: 'Acme Corp',
1425
- * tier: 'premium',
1426
- * })
1427
- * ```
1428
- */
1429
- class Prompts {
1430
- constructor(options) {
1431
- this.cache = new Map();
1432
- this.defaultCacheTtlSeconds = options.defaultCacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS;
1433
- if (isPromptsWithPostHog(options)) {
1434
- this.personalApiKey = options.posthog.options.personalApiKey ?? '';
1435
- this.projectApiKey = options.posthog.apiKey;
1436
- this.host = options.posthog.host;
1437
- } else {
1438
- // Direct options
1439
- this.personalApiKey = normalizeApiKey(options.personalApiKey);
1440
- this.projectApiKey = normalizeApiKey(options.projectApiKey);
1441
- this.host = normalizeHost(options.host);
1442
- }
1443
- }
1444
- getPromptCache(name) {
1445
- return this.cache.get(name);
1446
- }
1447
- getOrCreatePromptCache(name) {
1448
- const cachedPromptVersions = this.cache.get(name);
1449
- if (cachedPromptVersions) {
1450
- return cachedPromptVersions;
1451
- }
1452
- const promptVersions = new Map();
1453
- this.cache.set(name, promptVersions);
1454
- return promptVersions;
1455
- }
1456
- getPromptReference(name, version, label) {
1457
- if (version !== undefined) {
1458
- return `"${name}" version ${version}`;
1459
- }
1460
- if (label !== undefined) {
1461
- return `"${name}" label "${label}"`;
1462
- }
1463
- return `"${name}"`;
1464
- }
1465
- /**
1466
- * Fetch a prompt by name from the PostHog API.
1467
- *
1468
- * Returns a `PromptResult` object carrying the prompt text alongside `source`,
1469
- * `name`, `version`, and `config` metadata. Read `result.prompt` for the
1470
- * template string and `result.config ?? {}` for model parameters or agent
1471
- * configuration stored with the version.
1472
- */
1473
- async get(name, options) {
1474
- if (options?.version !== undefined && options?.label !== undefined) {
1475
- throw new Error('[PostHog Prompts] Pass either version or label, not both.');
1476
- }
1477
- try {
1478
- return await this.getInternal(name, options);
1479
- } catch (error) {
1480
- const fallback = options?.fallback;
1481
- if (fallback !== undefined) {
1482
- const promptReference = this.getPromptReference(name, options?.version, options?.label);
1483
- console.warn(`[PostHog Prompts] Failed to fetch prompt ${promptReference}, using fallback:`, error);
1484
- return {
1485
- source: 'code_fallback',
1486
- prompt: fallback,
1487
- name: undefined,
1488
- version: undefined,
1489
- label: undefined,
1490
- config: undefined
1491
- };
1492
- }
1493
- throw error;
1494
- }
1495
- }
1496
- /**
1497
- * Internal method that handles cache + fetch logic, returning full metadata.
1498
- * Does NOT handle the string `fallback` option — callers handle that.
1499
- */
1500
- async getInternal(name, options) {
1501
- const cacheTtlSeconds = options?.cacheTtlSeconds ?? this.defaultCacheTtlSeconds;
1502
- const version = options?.version;
1503
- const label = options?.label;
1504
- const promptReference = this.getPromptReference(name, version, label);
1505
- const cacheEntryKey = version ?? label;
1506
- // Check cache first
1507
- const cached = this.getPromptCache(name)?.get(cacheEntryKey);
1508
- const now = Date.now();
1509
- if (cached) {
1510
- const isFresh = now - cached.fetchedAt < cacheTtlSeconds * 1000;
1511
- if (isFresh) {
1512
- const {
1513
- fetchedAt: _,
1514
- ...cachedResult
1515
- } = cached;
1516
- return {
1517
- source: 'cache',
1518
- ...cachedResult,
1519
- config: cloneConfig(cached.config)
1520
- };
1521
- }
1522
- }
1523
- // Try to fetch from API
1524
- try {
1525
- const fetched = await this.fetchPromptFromApi(name, version, label);
1526
- // An older PostHog server ignores the label param and returns the latest
1527
- // version with no label field — surface that instead of failing silently.
1528
- if (label !== undefined && fetched.label !== label) {
1529
- console.warn(`[PostHog Prompts] Requested label "${label}" for prompt "${name}" but the server resolved ` + `${fetched.label === undefined ? 'no label' : `"${fetched.label}"`}. It may not support prompt ` + 'labels yet and returned the latest version instead.');
1530
- }
1531
- // Update cache
1532
- this.getOrCreatePromptCache(name).set(cacheEntryKey, {
1533
- ...fetched,
1534
- fetchedAt: Date.now()
1535
- });
1536
- return {
1537
- source: 'api',
1538
- ...fetched,
1539
- config: cloneConfig(fetched.config)
1540
- };
1541
- } catch (error) {
1542
- // Return stale cache (with warning)
1543
- if (cached) {
1544
- const {
1545
- fetchedAt: _,
1546
- ...cachedResult
1547
- } = cached;
1548
- console.warn(`[PostHog Prompts] Failed to fetch prompt ${promptReference}, using stale cache:`, error);
1549
- return {
1550
- source: 'stale_cache',
1551
- ...cachedResult,
1552
- config: cloneConfig(cached.config)
1553
- };
1554
- }
1555
- throw error;
1556
- }
1557
- }
1558
- /**
1559
- * Compile a prompt template with variable substitution
1560
- *
1561
- * Variables in the format `{{variableName}}` will be replaced with values from the variables object.
1562
- * Unmatched variables are left unchanged.
1563
- *
1564
- * @param prompt - The prompt template string
1565
- * @param variables - Object containing variable values
1566
- * @returns The compiled prompt string
1567
- */
1568
- compile(prompt, variables) {
1569
- return prompt.replace(/\{\{([\w.-]+)\}\}/g, (match, variableName) => {
1570
- if (variableName in variables) {
1571
- return String(variables[variableName]);
1572
- }
1573
- return match;
1574
- });
1575
- }
1576
- /**
1577
- * Clear the cache for a specific prompt or all prompts
1578
- *
1579
- * @param name - Optional prompt name to clear. If provided, clears all cached versions for that prompt unless a version is also provided.
1580
- * @param version - Optional prompt version to clear. Requires a prompt name.
1581
- */
1582
- clearCache(name, version) {
1583
- if (version !== undefined && name === undefined) {
1584
- throw new Error("'version' requires 'name' to be provided");
1585
- }
1586
- if (name === undefined) {
1587
- this.cache.clear();
1588
- return;
1589
- }
1590
- if (version === undefined) {
1591
- this.cache.delete(name);
1592
- return;
1593
- }
1594
- const promptVersions = this.getPromptCache(name);
1595
- promptVersions?.delete(version);
1596
- if (promptVersions?.size === 0) {
1597
- this.cache.delete(name);
1598
- }
1599
- }
1600
- async fetchPromptFromApi(name, version, label) {
1601
- if (!this.personalApiKey) {
1602
- throw new Error('[PostHog Prompts] personalApiKey is required to fetch prompts. ' + 'Please provide it when initializing the Prompts instance.');
1603
- }
1604
- if (!this.projectApiKey) {
1605
- throw new Error('[PostHog Prompts] projectApiKey is required to fetch prompts. ' + 'Please provide it when initializing the Prompts instance.');
1606
- }
1607
- const encodedPromptName = encodeURIComponent(name);
1608
- const encodedProjectApiKey = encodeURIComponent(this.projectApiKey);
1609
- const versionQuery = version === undefined ? '' : `&version=${encodeURIComponent(String(version))}`;
1610
- const labelQuery = label === undefined ? '' : `&label=${encodeURIComponent(label)}`;
1611
- const promptReference = this.getPromptReference(name, version, label);
1612
- const url = `${this.host}/api/environments/@current/llm_prompts/name/${encodedPromptName}/?token=${encodedProjectApiKey}${versionQuery}${labelQuery}`;
1613
- const response = await fetch(url, {
1614
- method: 'GET',
1615
- headers: {
1616
- Authorization: `Bearer ${this.personalApiKey}`
1617
- }
1618
- });
1619
- if (!response.ok) {
1620
- if (response.status === 404) {
1621
- throw new Error(`[PostHog Prompts] Prompt ${promptReference} not found`);
1622
- }
1623
- if (response.status === 403) {
1624
- throw new Error(`[PostHog Prompts] Access denied for prompt ${promptReference}. ` + 'Check that your personalApiKey has the correct permissions and the LLM prompts feature is enabled.');
1625
- }
1626
- throw new Error(`[PostHog Prompts] Failed to fetch prompt ${promptReference}: HTTP ${response.status}`);
1627
- }
1628
- const data = await response.json();
1629
- if (!isPromptApiResponse(data)) {
1630
- throw new Error(`[PostHog Prompts] Invalid response format for prompt ${promptReference}`);
1631
- }
1632
- return {
1633
- prompt: data.prompt,
1634
- name: data.name,
1635
- version: data.version,
1636
- label: data.label,
1637
- config: extractConfig(data.config)
1638
- };
1639
- }
1640
- }
1641
-
1111
+ * Prompts class for fetching and compiling LLM prompts from PostHog
1112
+ *
1113
+ * @example
1114
+ * ```ts
1115
+ * // With PostHog client
1116
+ * const prompts = new Prompts({ posthog })
1117
+ *
1118
+ * // Or with direct options (no PostHog client needed)
1119
+ * const prompts = new Prompts({
1120
+ * personalApiKey: 'phx_xxx',
1121
+ * projectApiKey: 'phc_xxx',
1122
+ * host: 'https://us.posthog.com',
1123
+ * })
1124
+ *
1125
+ * // Fetch with caching and fallback
1126
+ * const result = await prompts.get('support-system-prompt', {
1127
+ * cacheTtlSeconds: 300,
1128
+ * fallback: 'You are a helpful assistant.',
1129
+ * })
1130
+ *
1131
+ * // Or fetch an exact published version
1132
+ * const v3 = await prompts.get('support-system-prompt', {
1133
+ * version: 3,
1134
+ * })
1135
+ *
1136
+ * // Or fetch the version a label currently points to
1137
+ * const prod = await prompts.get('support-system-prompt', {
1138
+ * label: 'production',
1139
+ * })
1140
+ *
1141
+ * // Compile with variables
1142
+ * const systemPrompt = prompts.compile(result.prompt, {
1143
+ * company: 'Acme Corp',
1144
+ * tier: 'premium',
1145
+ * })
1146
+ * ```
1147
+ */
1148
+ var Prompts = class {
1149
+ constructor(options) {
1150
+ this.cache = /* @__PURE__ */ new Map();
1151
+ this.defaultCacheTtlSeconds = options.defaultCacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS;
1152
+ if (isPromptsWithPostHog(options)) {
1153
+ this.personalApiKey = options.posthog.options.personalApiKey ?? "";
1154
+ this.projectApiKey = options.posthog.apiKey;
1155
+ this.host = options.posthog.host;
1156
+ } else {
1157
+ this.personalApiKey = normalizeApiKey(options.personalApiKey);
1158
+ this.projectApiKey = normalizeApiKey(options.projectApiKey);
1159
+ this.host = normalizeHost(options.host);
1160
+ }
1161
+ }
1162
+ getPromptCache(name) {
1163
+ return this.cache.get(name);
1164
+ }
1165
+ getOrCreatePromptCache(name) {
1166
+ const cachedPromptVersions = this.cache.get(name);
1167
+ if (cachedPromptVersions) return cachedPromptVersions;
1168
+ const promptVersions = /* @__PURE__ */ new Map();
1169
+ this.cache.set(name, promptVersions);
1170
+ return promptVersions;
1171
+ }
1172
+ getPromptReference(name, version, label) {
1173
+ if (version !== void 0) return `"${name}" version ${version}`;
1174
+ if (label !== void 0) return `"${name}" label "${label}"`;
1175
+ return `"${name}"`;
1176
+ }
1177
+ /**
1178
+ * Fetch a prompt by name from the PostHog API.
1179
+ *
1180
+ * Returns a `PromptResult` object carrying the prompt text alongside `source`,
1181
+ * `name`, `version`, and `config` metadata. Read `result.prompt` for the
1182
+ * template string and `result.config ?? {}` for model parameters or agent
1183
+ * configuration stored with the version.
1184
+ */
1185
+ async get(name, options) {
1186
+ if (options?.version !== void 0 && options?.label !== void 0) throw new Error("[PostHog Prompts] Pass either version or label, not both.");
1187
+ try {
1188
+ return await this.getInternal(name, options);
1189
+ } catch (error) {
1190
+ const fallback = options?.fallback;
1191
+ if (fallback !== void 0) {
1192
+ const promptReference = this.getPromptReference(name, options?.version, options?.label);
1193
+ console.warn(`[PostHog Prompts] Failed to fetch prompt ${promptReference}, using fallback:`, error);
1194
+ return {
1195
+ source: "code_fallback",
1196
+ prompt: fallback,
1197
+ name: void 0,
1198
+ version: void 0,
1199
+ label: void 0,
1200
+ config: void 0
1201
+ };
1202
+ }
1203
+ throw error;
1204
+ }
1205
+ }
1206
+ /**
1207
+ * Internal method that handles cache + fetch logic, returning full metadata.
1208
+ * Does NOT handle the string `fallback` option — callers handle that.
1209
+ */
1210
+ async getInternal(name, options) {
1211
+ const cacheTtlSeconds = options?.cacheTtlSeconds ?? this.defaultCacheTtlSeconds;
1212
+ const version = options?.version;
1213
+ const label = options?.label;
1214
+ const promptReference = this.getPromptReference(name, version, label);
1215
+ const cacheEntryKey = version ?? label;
1216
+ const cached = this.getPromptCache(name)?.get(cacheEntryKey);
1217
+ const now = Date.now();
1218
+ if (cached) {
1219
+ if (now - cached.fetchedAt < cacheTtlSeconds * 1e3) {
1220
+ const { fetchedAt: _, ...cachedResult } = cached;
1221
+ return {
1222
+ source: "cache",
1223
+ ...cachedResult,
1224
+ config: cloneConfig(cached.config)
1225
+ };
1226
+ }
1227
+ }
1228
+ try {
1229
+ const fetched = await this.fetchPromptFromApi(name, version, label);
1230
+ if (label !== void 0 && fetched.label !== label) console.warn(`[PostHog Prompts] Requested label "${label}" for prompt "${name}" but the server resolved ${fetched.label === void 0 ? "no label" : `"${fetched.label}"`}. It may not support prompt labels yet and returned the latest version instead.`);
1231
+ this.getOrCreatePromptCache(name).set(cacheEntryKey, {
1232
+ ...fetched,
1233
+ fetchedAt: Date.now()
1234
+ });
1235
+ return {
1236
+ source: "api",
1237
+ ...fetched,
1238
+ config: cloneConfig(fetched.config)
1239
+ };
1240
+ } catch (error) {
1241
+ if (cached) {
1242
+ const { fetchedAt: _, ...cachedResult } = cached;
1243
+ console.warn(`[PostHog Prompts] Failed to fetch prompt ${promptReference}, using stale cache:`, error);
1244
+ return {
1245
+ source: "stale_cache",
1246
+ ...cachedResult,
1247
+ config: cloneConfig(cached.config)
1248
+ };
1249
+ }
1250
+ throw error;
1251
+ }
1252
+ }
1253
+ /**
1254
+ * Compile a prompt template with variable substitution
1255
+ *
1256
+ * Variables in the format `{{variableName}}` will be replaced with values from the variables object.
1257
+ * Unmatched variables are left unchanged.
1258
+ *
1259
+ * @param prompt - The prompt template string
1260
+ * @param variables - Object containing variable values
1261
+ * @returns The compiled prompt string
1262
+ */
1263
+ compile(prompt, variables) {
1264
+ return prompt.replace(/\{\{([\w.-]+)\}\}/g, (match, variableName) => {
1265
+ if (variableName in variables) return String(variables[variableName]);
1266
+ return match;
1267
+ });
1268
+ }
1269
+ /**
1270
+ * Clear the cache for a specific prompt or all prompts
1271
+ *
1272
+ * @param name - Optional prompt name to clear. If provided, clears all cached versions for that prompt unless a version is also provided.
1273
+ * @param version - Optional prompt version to clear. Requires a prompt name.
1274
+ */
1275
+ clearCache(name, version) {
1276
+ if (version !== void 0 && name === void 0) throw new Error("'version' requires 'name' to be provided");
1277
+ if (name === void 0) {
1278
+ this.cache.clear();
1279
+ return;
1280
+ }
1281
+ if (version === void 0) {
1282
+ this.cache.delete(name);
1283
+ return;
1284
+ }
1285
+ const promptVersions = this.getPromptCache(name);
1286
+ promptVersions?.delete(version);
1287
+ if (promptVersions?.size === 0) this.cache.delete(name);
1288
+ }
1289
+ async fetchPromptFromApi(name, version, label) {
1290
+ if (!this.personalApiKey) throw new Error("[PostHog Prompts] personalApiKey is required to fetch prompts. Please provide it when initializing the Prompts instance.");
1291
+ if (!this.projectApiKey) throw new Error("[PostHog Prompts] projectApiKey is required to fetch prompts. Please provide it when initializing the Prompts instance.");
1292
+ const encodedPromptName = encodeURIComponent(name);
1293
+ const encodedProjectApiKey = encodeURIComponent(this.projectApiKey);
1294
+ const versionQuery = version === void 0 ? "" : `&version=${encodeURIComponent(String(version))}`;
1295
+ const labelQuery = label === void 0 ? "" : `&label=${encodeURIComponent(label)}`;
1296
+ const promptReference = this.getPromptReference(name, version, label);
1297
+ const url = `${this.host}/api/environments/@current/llm_prompts/name/${encodedPromptName}/?token=${encodedProjectApiKey}${versionQuery}${labelQuery}`;
1298
+ const response = await fetch(url, {
1299
+ method: "GET",
1300
+ headers: { Authorization: `Bearer ${this.personalApiKey}` }
1301
+ });
1302
+ if (!response.ok) {
1303
+ if (response.status === 404) throw new Error(`[PostHog Prompts] Prompt ${promptReference} not found`);
1304
+ if (response.status === 403) throw new Error(`[PostHog Prompts] Access denied for prompt ${promptReference}. Check that your personalApiKey has the correct permissions and the LLM prompts feature is enabled.`);
1305
+ throw new Error(`[PostHog Prompts] Failed to fetch prompt ${promptReference}: HTTP ${response.status}`);
1306
+ }
1307
+ const data = await response.json();
1308
+ if (!isPromptApiResponse(data)) throw new Error(`[PostHog Prompts] Invalid response format for prompt ${promptReference}`);
1309
+ return {
1310
+ prompt: data.prompt,
1311
+ name: data.name,
1312
+ version: data.version,
1313
+ label: data.label,
1314
+ config: extractConfig(data.config)
1315
+ };
1316
+ }
1317
+ };
1318
+ //#endregion
1319
+ exports.AIEvent = AIEvent;
1642
1320
  exports.Prompts = Prompts;
1643
1321
  exports.captureAiGeneration = captureAiGeneration;
1644
1322
  exports.withTracing = wrapVercelLanguageModel;
1645
- //# sourceMappingURL=index.cjs.map
1323
+
1324
+ //# sourceMappingURL=index.cjs.map