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