@posthog/ai 8.11.2 → 8.12.0
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/README.md +24 -0
- package/dist/adk/index.cjs +1 -1
- package/dist/adk/index.cjs.map +1 -1
- package/dist/adk/index.mjs +1 -1
- package/dist/adk/index.mjs.map +1 -1
- package/dist/anthropic/index.cjs +1 -1
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.mjs +1 -1
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/claude-agent-sdk/index.cjs +1142 -0
- package/dist/claude-agent-sdk/index.cjs.map +1 -0
- package/dist/claude-agent-sdk/index.d.ts +148 -0
- package/dist/claude-agent-sdk/index.mjs +1139 -0
- package/dist/claude-agent-sdk/index.mjs.map +1 -0
- package/dist/gemini/index.cjs +1 -1
- package/dist/gemini/index.cjs.map +1 -1
- package/dist/gemini/index.mjs +1 -1
- package/dist/gemini/index.mjs.map +1 -1
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +4 -4
- package/dist/index.mjs.map +1 -1
- package/dist/langchain/index.cjs +1 -1
- package/dist/langchain/index.cjs.map +1 -1
- package/dist/langchain/index.mjs +1 -1
- package/dist/langchain/index.mjs.map +1 -1
- package/dist/langchain/middleware/index.cjs +1 -1
- package/dist/langchain/middleware/index.cjs.map +1 -1
- package/dist/langchain/middleware/index.mjs +1 -1
- package/dist/langchain/middleware/index.mjs.map +1 -1
- package/dist/openai/index.cjs +1 -1
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.mjs +1 -1
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openai-agents/index.cjs +4 -4
- package/dist/openai-agents/index.cjs.map +1 -1
- package/dist/openai-agents/index.mjs +4 -4
- package/dist/openai-agents/index.mjs.map +1 -1
- package/dist/vercel/index.cjs +4 -4
- package/dist/vercel/index.cjs.map +1 -1
- package/dist/vercel/index.mjs +4 -4
- package/dist/vercel/index.mjs.map +1 -1
- package/package.json +13 -3
|
@@ -0,0 +1,1142 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let _anthropic_ai_claude_agent_sdk = require("@anthropic-ai/claude-agent-sdk");
|
|
3
|
+
let uuid = require("uuid");
|
|
4
|
+
let _posthog_core = require("@posthog/core");
|
|
5
|
+
//#region package.json
|
|
6
|
+
var version = "8.12.0";
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/captureAiEvent.ts
|
|
9
|
+
/** @internal */
|
|
10
|
+
function isFullAiCaptureEnabled(client) {
|
|
11
|
+
return client?.enableFullAiCapture === true;
|
|
12
|
+
}
|
|
13
|
+
/** @internal */
|
|
14
|
+
function captureAiEvent(client, event) {
|
|
15
|
+
if (isFullAiCaptureEnabled(client) && typeof client.captureAi === "function") {
|
|
16
|
+
client.captureAi(event);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
client.capture(event);
|
|
20
|
+
}
|
|
21
|
+
/** @internal */
|
|
22
|
+
async function captureAiEventImmediate(client, event) {
|
|
23
|
+
if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === "function") {
|
|
24
|
+
await client.captureAiImmediate(event);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
await client.captureImmediate(event);
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/sanitization/base64_recognizer.ts
|
|
31
|
+
const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
|
|
32
|
+
const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
|
|
33
|
+
var Base64Recognizer = class {
|
|
34
|
+
recognize(value, minLength) {
|
|
35
|
+
const dataUrl = DATA_URL_PREFIX_RE.exec(value);
|
|
36
|
+
if (dataUrl) return {
|
|
37
|
+
kind: "data-url",
|
|
38
|
+
mediaType: dataUrl[1]
|
|
39
|
+
};
|
|
40
|
+
if (value.length < minLength) return { kind: "none" };
|
|
41
|
+
const confidencePrefix = value.slice(0, minLength);
|
|
42
|
+
if (BASE64_ALPHABET_RE.test(confidencePrefix)) return { kind: "raw" };
|
|
43
|
+
else return { kind: "none" };
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/sanitization/media_type_context.ts
|
|
48
|
+
const MIME_HINT_KEYS = [
|
|
49
|
+
"mediaType",
|
|
50
|
+
"media_type",
|
|
51
|
+
"mimeType",
|
|
52
|
+
"mime_type"
|
|
53
|
+
];
|
|
54
|
+
const STRONG_CONTEXT_KEYS = /* @__PURE__ */ new Set([
|
|
55
|
+
"data",
|
|
56
|
+
"file_data",
|
|
57
|
+
"fileData",
|
|
58
|
+
"image_url",
|
|
59
|
+
"imageUrl",
|
|
60
|
+
"video_url",
|
|
61
|
+
"videoUrl",
|
|
62
|
+
"audio",
|
|
63
|
+
"audio_data",
|
|
64
|
+
"audioData",
|
|
65
|
+
"inline_data",
|
|
66
|
+
"inlineData",
|
|
67
|
+
"source",
|
|
68
|
+
"result"
|
|
69
|
+
]);
|
|
70
|
+
const STRONG_CONTEXT_TYPES = /* @__PURE__ */ new Set([
|
|
71
|
+
"image",
|
|
72
|
+
"image_url",
|
|
73
|
+
"input_image",
|
|
74
|
+
"audio",
|
|
75
|
+
"input_audio",
|
|
76
|
+
"video",
|
|
77
|
+
"video_url",
|
|
78
|
+
"file",
|
|
79
|
+
"input_file",
|
|
80
|
+
"document",
|
|
81
|
+
"media",
|
|
82
|
+
"file-data"
|
|
83
|
+
]);
|
|
84
|
+
const FILE_FAMILY_TYPES = /* @__PURE__ */ new Set([
|
|
85
|
+
"file",
|
|
86
|
+
"input_file",
|
|
87
|
+
"document",
|
|
88
|
+
"media",
|
|
89
|
+
"file-data"
|
|
90
|
+
]);
|
|
91
|
+
const KNOWN_AUDIO_FORMATS = /* @__PURE__ */ new Set([
|
|
92
|
+
"wav",
|
|
93
|
+
"mp3",
|
|
94
|
+
"ogg",
|
|
95
|
+
"flac",
|
|
96
|
+
"m4a",
|
|
97
|
+
"aac",
|
|
98
|
+
"webm"
|
|
99
|
+
]);
|
|
100
|
+
var MediaTypeContext = class MediaTypeContext {
|
|
101
|
+
static {
|
|
102
|
+
this.EMPTY = new MediaTypeContext(void 0, void 0);
|
|
103
|
+
}
|
|
104
|
+
constructor(parent, key, explicitMediaType) {
|
|
105
|
+
this.parent = parent;
|
|
106
|
+
this.key = key;
|
|
107
|
+
this.explicitMediaType = explicitMediaType;
|
|
108
|
+
}
|
|
109
|
+
inferMediaType() {
|
|
110
|
+
return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
|
|
111
|
+
}
|
|
112
|
+
inferFromSiblingMime() {
|
|
113
|
+
if (this.explicitMediaType) return this.explicitMediaType;
|
|
114
|
+
if (!this.parent) return void 0;
|
|
115
|
+
for (const hint of MIME_HINT_KEYS) {
|
|
116
|
+
const v = this.parent[hint];
|
|
117
|
+
if (typeof v === "string") return v;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
inferFromSiblingFormat() {
|
|
121
|
+
if (!this.parent) return void 0;
|
|
122
|
+
const fmt = this.parent.format;
|
|
123
|
+
if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return `audio/${fmt.toLowerCase()}`;
|
|
124
|
+
}
|
|
125
|
+
inferFromParentType() {
|
|
126
|
+
if (!this.parent) return void 0;
|
|
127
|
+
const t = this.parent.type;
|
|
128
|
+
if (typeof t !== "string") return void 0;
|
|
129
|
+
if (t === "image" || t === "image_url" || t === "input_image") return "image";
|
|
130
|
+
if (t === "audio" || t === "input_audio") return "audio";
|
|
131
|
+
if (t === "video" || t === "video_url") return "video";
|
|
132
|
+
if (FILE_FAMILY_TYPES.has(t)) return "application/octet-stream";
|
|
133
|
+
}
|
|
134
|
+
inferFromKey() {
|
|
135
|
+
if (!this.key) return void 0;
|
|
136
|
+
const key = this.key.toLowerCase();
|
|
137
|
+
if (key.includes("audio")) return "audio";
|
|
138
|
+
if (key.includes("video")) return "video";
|
|
139
|
+
if (key.includes("image")) return "image";
|
|
140
|
+
if (key.includes("file") || key.includes("document")) return "application/octet-stream";
|
|
141
|
+
}
|
|
142
|
+
hasExplicitBinaryMediaType() {
|
|
143
|
+
if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
|
|
144
|
+
const mediaType = this.inferFromSiblingMime();
|
|
145
|
+
return mediaType !== void 0 && !mediaType.toLowerCase().startsWith("text/");
|
|
146
|
+
}
|
|
147
|
+
signalsBinary() {
|
|
148
|
+
if (this.explicitMediaType) return true;
|
|
149
|
+
if (this.parent) {
|
|
150
|
+
for (const hint of MIME_HINT_KEYS) if (typeof this.parent[hint] === "string") return true;
|
|
151
|
+
const fmt = this.parent.format;
|
|
152
|
+
if (typeof fmt === "string" && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
|
|
153
|
+
const t = this.parent.type;
|
|
154
|
+
if (typeof t === "string" && STRONG_CONTEXT_TYPES.has(t)) return true;
|
|
155
|
+
}
|
|
156
|
+
if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/sanitization/binary_content_redactor.ts
|
|
162
|
+
const STRONG_CONTEXT_MIN_LENGTH = 64;
|
|
163
|
+
const WEAK_CONTEXT_MIN_LENGTH = 1024;
|
|
164
|
+
var BinaryContentRedactor = class {
|
|
165
|
+
constructor(recognizer = new Base64Recognizer()) {
|
|
166
|
+
this.recognizer = recognizer;
|
|
167
|
+
this.visited = /* @__PURE__ */ new WeakSet();
|
|
168
|
+
}
|
|
169
|
+
redact(value, mediaType) {
|
|
170
|
+
this.visited = /* @__PURE__ */ new WeakSet();
|
|
171
|
+
return this.walk(value, mediaType ? new MediaTypeContext(void 0, void 0, mediaType) : MediaTypeContext.EMPTY);
|
|
172
|
+
}
|
|
173
|
+
walk(value, ctx) {
|
|
174
|
+
if (value === null || value === void 0) return value;
|
|
175
|
+
if (typeof value === "string") return this.redactString(value, ctx);
|
|
176
|
+
if (typeof value !== "object") return value;
|
|
177
|
+
if (typeof Uint8Array !== "undefined" && value instanceof Uint8Array) return this.placeholderFor(ctx.inferMediaType());
|
|
178
|
+
if (this.visited.has(value)) return null;
|
|
179
|
+
this.visited.add(value);
|
|
180
|
+
if (Array.isArray(value)) return value.map((item) => this.walk(item, ctx));
|
|
181
|
+
const obj = value;
|
|
182
|
+
const out = {};
|
|
183
|
+
for (const k of Object.keys(obj)) out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
redactString(value, ctx) {
|
|
187
|
+
const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
|
|
188
|
+
const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, "") : value;
|
|
189
|
+
const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
|
|
190
|
+
const recognition = this.recognizer.recognize(recognitionValue, minLength);
|
|
191
|
+
switch (recognition.kind) {
|
|
192
|
+
case "data-url": return this.placeholderFor(recognition.mediaType);
|
|
193
|
+
case "raw": return this.placeholderFor(ctx.inferMediaType());
|
|
194
|
+
case "none": return value;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
placeholderFor(mediaType) {
|
|
198
|
+
if (!mediaType) return "[base64 redacted]";
|
|
199
|
+
if (mediaType === "application/octet-stream") return "[base64 file redacted]";
|
|
200
|
+
return `[base64 ${mediaType} redacted]`;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/sanitization.ts
|
|
205
|
+
const redactor = new BinaryContentRedactor();
|
|
206
|
+
const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
|
|
207
|
+
const sanitizeAnthropic = (data, client) => sanitize(data, client);
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region src/utils.ts
|
|
210
|
+
const TOKEN_PROPERTY_KEYS = /* @__PURE__ */ new Set([
|
|
211
|
+
"$ai_input_tokens",
|
|
212
|
+
"$ai_output_tokens",
|
|
213
|
+
"$ai_cache_read_input_tokens",
|
|
214
|
+
"$ai_cache_creation_input_tokens",
|
|
215
|
+
"$ai_total_tokens",
|
|
216
|
+
"$ai_reasoning_tokens"
|
|
217
|
+
]);
|
|
218
|
+
/**
|
|
219
|
+
* Whether the caller supplied their own token counts, which override the ones the SDK
|
|
220
|
+
* derived from the provider response.
|
|
221
|
+
*/
|
|
222
|
+
function hasTokenOverrides(posthogProperties) {
|
|
223
|
+
return !!posthogProperties && Object.keys(posthogProperties).some((key) => TOKEN_PROPERTY_KEYS.has(key));
|
|
224
|
+
}
|
|
225
|
+
function getTokensSource(posthogProperties) {
|
|
226
|
+
return hasTokenOverrides(posthogProperties) ? "passthrough" : "sdk";
|
|
227
|
+
}
|
|
228
|
+
const MAX_OUTPUT_SIZE = 2e5;
|
|
229
|
+
const STRING_FORMAT = "utf8";
|
|
230
|
+
const sharedTextEncoder = new TextEncoder();
|
|
231
|
+
const sharedTextDecoder = new TextDecoder(STRING_FORMAT, { fatal: false });
|
|
232
|
+
/**
|
|
233
|
+
* Safely converts content to a string, preserving structure for objects/arrays.
|
|
234
|
+
* - If content is already a string, returns it as-is
|
|
235
|
+
* - If content is an object or array, stringifies it with JSON.stringify to preserve structure
|
|
236
|
+
* - Otherwise, converts to string with String()
|
|
237
|
+
*
|
|
238
|
+
* This prevents the "[object Object]" bug when objects are naively converted to strings.
|
|
239
|
+
*
|
|
240
|
+
* @param content - The content to convert to a string
|
|
241
|
+
* @returns A string representation that preserves structure for complex types
|
|
242
|
+
*/
|
|
243
|
+
function toContentString(content) {
|
|
244
|
+
if (typeof content === "string") return content;
|
|
245
|
+
if (content !== void 0 && content !== null && typeof content === "object") try {
|
|
246
|
+
return JSON.stringify(content);
|
|
247
|
+
} catch {
|
|
248
|
+
try {
|
|
249
|
+
return String(content);
|
|
250
|
+
} catch {
|
|
251
|
+
return "";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return String(content);
|
|
255
|
+
}
|
|
256
|
+
const withPrivacyMode = (client, privacyMode, input) => {
|
|
257
|
+
return client.privacy_mode || privacyMode ? null : input;
|
|
258
|
+
};
|
|
259
|
+
function toSafeString(input) {
|
|
260
|
+
if (input === void 0 || input === null) return "";
|
|
261
|
+
if (typeof input === "string") return input;
|
|
262
|
+
try {
|
|
263
|
+
return JSON.stringify(input);
|
|
264
|
+
} catch {
|
|
265
|
+
console.warn("Failed to stringify input", input);
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const truncate = (input, client, maxBytes = MAX_OUTPUT_SIZE) => {
|
|
270
|
+
const str = toSafeString(input);
|
|
271
|
+
if (str === "") return "";
|
|
272
|
+
if (isFullAiCaptureEnabled(client)) return str;
|
|
273
|
+
const buffer = sharedTextEncoder.encode(str);
|
|
274
|
+
if (buffer.length <= maxBytes) return sharedTextDecoder.decode(buffer);
|
|
275
|
+
const truncatedBuffer = buffer.slice(0, maxBytes);
|
|
276
|
+
let truncatedStr = sharedTextDecoder.decode(truncatedBuffer);
|
|
277
|
+
if (truncatedStr.endsWith("�")) truncatedStr = truncatedStr.slice(0, -1);
|
|
278
|
+
return `${truncatedStr}... [truncated]`;
|
|
279
|
+
};
|
|
280
|
+
function sanitizeValues(obj) {
|
|
281
|
+
if (obj === void 0 || obj === null) return obj;
|
|
282
|
+
const jsonSafe = JSON.parse(JSON.stringify(obj));
|
|
283
|
+
if (typeof jsonSafe === "string") return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
|
|
284
|
+
else if (Array.isArray(jsonSafe)) return jsonSafe.map(sanitizeValues);
|
|
285
|
+
else if (jsonSafe && typeof jsonSafe === "object") return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
|
|
286
|
+
return jsonSafe;
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/serializeError.ts
|
|
290
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
291
|
+
const MAX_STACK_LINES = 20;
|
|
292
|
+
function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
|
|
293
|
+
if (depth < 0 || value === null || typeof value !== "object") return value;
|
|
294
|
+
if (value instanceof Error) {
|
|
295
|
+
const out = {
|
|
296
|
+
name: value.name,
|
|
297
|
+
message: value.message,
|
|
298
|
+
stack: truncateStack(value.stack)
|
|
299
|
+
};
|
|
300
|
+
for (const key of Object.keys(value)) out[key] = serializeError(value[key], depth - 1);
|
|
301
|
+
if (value.cause !== void 0) out.cause = serializeError(value.cause, depth - 1);
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(value)) return value.map((item) => serializeError(item, depth - 1));
|
|
305
|
+
return value;
|
|
306
|
+
}
|
|
307
|
+
function stringifyError(error) {
|
|
308
|
+
try {
|
|
309
|
+
return JSON.stringify(sanitizeValues(serializeError(error)));
|
|
310
|
+
} catch {
|
|
311
|
+
if (error instanceof Error) return JSON.stringify({
|
|
312
|
+
name: error.name,
|
|
313
|
+
message: error.message
|
|
314
|
+
});
|
|
315
|
+
return JSON.stringify({ message: String(error) });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function truncateStack(stack) {
|
|
319
|
+
if (!stack) return stack;
|
|
320
|
+
const lines = stack.split("\n");
|
|
321
|
+
if (lines.length <= MAX_STACK_LINES) return stack;
|
|
322
|
+
return [...lines.slice(0, MAX_STACK_LINES), "... (truncated)"].join("\n");
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/gatewayWarning.ts
|
|
326
|
+
const POSTHOG_AI_GATEWAY_HOSTS = [
|
|
327
|
+
"gateway.posthog.com",
|
|
328
|
+
"gateway.us.posthog.com",
|
|
329
|
+
"gateway.eu.posthog.com",
|
|
330
|
+
"ai-gateway.us.posthog.com",
|
|
331
|
+
"ai-gateway.eu.posthog.com"
|
|
332
|
+
];
|
|
333
|
+
const GATEWAY_DOCS_URL = "https://posthog.com/docs/ai-observability";
|
|
334
|
+
const extractHost = (baseURL) => {
|
|
335
|
+
try {
|
|
336
|
+
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
|
|
337
|
+
return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
|
|
338
|
+
} catch {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const isPostHogAiGatewayUrl = (baseURL) => {
|
|
343
|
+
if (!baseURL) return false;
|
|
344
|
+
const host = extractHost(baseURL);
|
|
345
|
+
return host !== void 0 && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
|
|
346
|
+
};
|
|
347
|
+
const warnIfPostHogAiGateway = (baseURL) => {
|
|
348
|
+
if (!isPostHogAiGatewayUrl(baseURL)) return;
|
|
349
|
+
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}.`);
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/captureAiGeneration.ts
|
|
353
|
+
/**
|
|
354
|
+
* Capture an `$ai_generation` (or `$ai_embedding`) event to PostHog.
|
|
355
|
+
*
|
|
356
|
+
* This is the canonical primitive that every `@posthog/ai` wrapper
|
|
357
|
+
* (`withTracing`, `OpenAI`, `Anthropic`, `GoogleGenAI`, …) funnels through, so
|
|
358
|
+
* external code can use it directly to instrument LLM calls made through
|
|
359
|
+
* arbitrary clients (Cloudflare Workers AI, custom HTTP, etc.) and get the
|
|
360
|
+
* same events the SDK wrappers produce.
|
|
361
|
+
*
|
|
362
|
+
* When `error` is set, the event is captured as an error. If the error is an
|
|
363
|
+
* object, it is mutated in place to set `__posthog_previously_captured_error`
|
|
364
|
+
* so callers can re-throw the original error reference safely.
|
|
365
|
+
*/
|
|
366
|
+
const captureAiGeneration = async (client, options) => {
|
|
367
|
+
try {
|
|
368
|
+
if (!client.capture) return;
|
|
369
|
+
warnIfPostHogAiGateway(options.baseURL);
|
|
370
|
+
const traceId = options.traceId ?? (0, uuid.v4)();
|
|
371
|
+
const eventType = options.eventType ?? "$ai_generation";
|
|
372
|
+
const privacyMode = options.privacyMode ?? false;
|
|
373
|
+
const usage = options.usage ?? {};
|
|
374
|
+
const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
|
|
375
|
+
const safeInput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.input);
|
|
376
|
+
const safeOutput = shouldRedact ? null : (0, _posthog_core.toJsonSafeValue)(options.output);
|
|
377
|
+
let httpStatus = options.httpStatus;
|
|
378
|
+
let errorData = {};
|
|
379
|
+
if (options.error) {
|
|
380
|
+
if (httpStatus === void 0) {
|
|
381
|
+
if (typeof options.error === "object" && "status" in options.error && typeof options.error.status === "number") httpStatus = options.error.status;
|
|
382
|
+
else if (typeof options.error === "object" && "statusCode" in options.error && typeof options.error.statusCode === "number") httpStatus = options.error.statusCode;
|
|
383
|
+
else httpStatus = 500;
|
|
384
|
+
}
|
|
385
|
+
let exceptionId;
|
|
386
|
+
if (client.options?.enableExceptionAutocapture) {
|
|
387
|
+
exceptionId = (0, _posthog_core.uuidv7)();
|
|
388
|
+
client.captureException(options.error, void 0, { $ai_trace_id: traceId }, exceptionId);
|
|
389
|
+
if (typeof options.error === "object") options.error.__posthog_previously_captured_error = true;
|
|
390
|
+
}
|
|
391
|
+
errorData = {
|
|
392
|
+
$ai_is_error: true,
|
|
393
|
+
$ai_error: stringifyError(options.error),
|
|
394
|
+
$exception_event_id: exceptionId
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
httpStatus = httpStatus ?? 200;
|
|
398
|
+
const costOverrideData = {};
|
|
399
|
+
if (options.costOverride) {
|
|
400
|
+
if (usage.inputTokens !== void 0) costOverrideData.$ai_input_cost_usd = (options.costOverride.inputCost ?? 0) * usage.inputTokens;
|
|
401
|
+
if (usage.outputTokens !== void 0) costOverrideData.$ai_output_cost_usd = (options.costOverride.outputCost ?? 0) * usage.outputTokens;
|
|
402
|
+
if (Object.keys(costOverrideData).length > 0) costOverrideData.$ai_total_cost_usd = (costOverrideData.$ai_input_cost_usd ?? 0) + (costOverrideData.$ai_output_cost_usd ?? 0);
|
|
403
|
+
}
|
|
404
|
+
const tokensOverridden = hasTokenOverrides(options.properties);
|
|
405
|
+
const additionalTokenValues = {
|
|
406
|
+
...usage.reasoningTokens ? { $ai_reasoning_tokens: usage.reasoningTokens } : {},
|
|
407
|
+
...usage.cacheReadInputTokens ? { $ai_cache_read_input_tokens: usage.cacheReadInputTokens } : {},
|
|
408
|
+
...usage.cacheCreationInputTokens ? { $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens } : {},
|
|
409
|
+
...usage.cacheReportingExclusive !== void 0 && !tokensOverridden ? { $ai_cache_reporting_exclusive: usage.cacheReportingExclusive } : {},
|
|
410
|
+
...usage.webSearchCount ? { $ai_web_search_count: usage.webSearchCount } : {},
|
|
411
|
+
...usage.rawUsage ? { $ai_usage: usage.rawUsage } : {}
|
|
412
|
+
};
|
|
413
|
+
const properties = {
|
|
414
|
+
$ai_lib: "posthog-ai",
|
|
415
|
+
$ai_lib_version: version,
|
|
416
|
+
$ai_provider: options.providerOverride ?? options.provider,
|
|
417
|
+
$ai_model: options.modelOverride ?? options.model,
|
|
418
|
+
$ai_model_parameters: options.modelParameters ?? {},
|
|
419
|
+
...options.servedServiceTier != null ? { $ai_service_tier: options.servedServiceTier } : {},
|
|
420
|
+
$ai_input: safeInput,
|
|
421
|
+
$ai_output_choices: safeOutput,
|
|
422
|
+
$ai_http_status: httpStatus,
|
|
423
|
+
...usage.inputTokens !== void 0 ? { $ai_input_tokens: usage.inputTokens } : {},
|
|
424
|
+
...usage.outputTokens !== void 0 ? { $ai_output_tokens: usage.outputTokens } : {},
|
|
425
|
+
...additionalTokenValues,
|
|
426
|
+
...options.latency !== void 0 ? { $ai_latency: options.latency } : {},
|
|
427
|
+
...options.timeToFirstToken !== void 0 ? { $ai_time_to_first_token: options.timeToFirstToken } : {},
|
|
428
|
+
$ai_trace_id: traceId,
|
|
429
|
+
...options.baseURL === null ? {} : { $ai_base_url: options.baseURL ?? "" },
|
|
430
|
+
...options.properties,
|
|
431
|
+
$ai_tokens_source: getTokensSource(options.properties),
|
|
432
|
+
...options.distinctId ? {} : { $process_person_profile: false },
|
|
433
|
+
...options.stopReason ? { $ai_stop_reason: options.stopReason } : {},
|
|
434
|
+
...options.tools ? { $ai_tools: options.tools } : {},
|
|
435
|
+
...options.completionId ? { $ai_completion_id: options.completionId } : {},
|
|
436
|
+
...options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? { $ai_provider_metadata: options.providerMetadata } : {},
|
|
437
|
+
...errorData,
|
|
438
|
+
...costOverrideData
|
|
439
|
+
};
|
|
440
|
+
const event = {
|
|
441
|
+
distinctId: options.distinctId ?? traceId,
|
|
442
|
+
event: eventType,
|
|
443
|
+
properties,
|
|
444
|
+
groups: options.groups
|
|
445
|
+
};
|
|
446
|
+
if (options.captureImmediate) await captureAiEventImmediate(client, event);
|
|
447
|
+
else captureAiEvent(client, event);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
try {
|
|
450
|
+
options.onError?.(error);
|
|
451
|
+
} catch {}
|
|
452
|
+
console.warn("[PostHog AI] Failed to capture generation telemetry:", error);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
//#endregion
|
|
456
|
+
//#region src/claude-agent-sdk/formatting.ts
|
|
457
|
+
/**
|
|
458
|
+
* Tool results in an agent run carry whole files and command output, and the
|
|
459
|
+
* same result is replayed as input on every later turn, so each string is
|
|
460
|
+
* capped before it reaches the event.
|
|
461
|
+
*/
|
|
462
|
+
const TOOL_RESULT_MAX_STRING_LENGTH = 5e3;
|
|
463
|
+
function capStrings(value, max) {
|
|
464
|
+
if (typeof value === "string") return truncate(value, void 0, max);
|
|
465
|
+
if (Array.isArray(value)) return value.map((item) => capStrings(item, max));
|
|
466
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, capStrings(item, max)]));
|
|
467
|
+
return value;
|
|
468
|
+
}
|
|
469
|
+
function formatContent(content, client, maxBytes = MAX_OUTPUT_SIZE) {
|
|
470
|
+
const redacted = sanitizeAnthropic((0, _posthog_core.toJsonSafeValue)(content), client);
|
|
471
|
+
return isFullAiCaptureEnabled(client) ? redacted : capStrings(redacted, maxBytes);
|
|
472
|
+
}
|
|
473
|
+
/** Read the system prompt out of the SDK options, whatever shape it takes. */
|
|
474
|
+
function extractSystemPrompt(options) {
|
|
475
|
+
const systemPrompt = options?.systemPrompt;
|
|
476
|
+
if (typeof systemPrompt === "string") return systemPrompt;
|
|
477
|
+
if (Array.isArray(systemPrompt)) return systemPrompt.filter((part) => part !== _anthropic_ai_claude_agent_sdk.SYSTEM_PROMPT_DYNAMIC_BOUNDARY).join("");
|
|
478
|
+
return systemPrompt?.append;
|
|
479
|
+
}
|
|
480
|
+
/** Convert Anthropic assistant content blocks into PostHog content items. */
|
|
481
|
+
function formatAssistantBlocks(blocks, client) {
|
|
482
|
+
if (!Array.isArray(blocks)) return [];
|
|
483
|
+
const content = [];
|
|
484
|
+
for (const block of formatContent(blocks, client)) {
|
|
485
|
+
if (block == null) continue;
|
|
486
|
+
if (typeof block.thinking === "string") content.push({
|
|
487
|
+
type: "reasoning",
|
|
488
|
+
text: block.thinking
|
|
489
|
+
});
|
|
490
|
+
else if (block.type === "tool_use") content.push({
|
|
491
|
+
type: "function",
|
|
492
|
+
id: block.id,
|
|
493
|
+
function: {
|
|
494
|
+
name: block.name,
|
|
495
|
+
arguments: block.input ?? {}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
else if (typeof block.text === "string") content.push({
|
|
499
|
+
type: "text",
|
|
500
|
+
text: block.text
|
|
501
|
+
});
|
|
502
|
+
else content.push({
|
|
503
|
+
type: "text",
|
|
504
|
+
text: toContentString(block)
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return content;
|
|
508
|
+
}
|
|
509
|
+
/** Convert an Anthropic user message body into PostHog content items. */
|
|
510
|
+
function formatUserContent(content, client) {
|
|
511
|
+
if (typeof content === "string") return content;
|
|
512
|
+
if (!Array.isArray(content)) return [];
|
|
513
|
+
const formatted = [];
|
|
514
|
+
for (const block of content) {
|
|
515
|
+
if (block == null) continue;
|
|
516
|
+
if (block.type === "tool_result") formatted.push({
|
|
517
|
+
type: "tool_result",
|
|
518
|
+
tool_use_id: block.tool_use_id,
|
|
519
|
+
content: formatContent(block.content, client, TOOL_RESULT_MAX_STRING_LENGTH),
|
|
520
|
+
...typeof block.is_error === "boolean" ? { is_error: block.is_error } : {}
|
|
521
|
+
});
|
|
522
|
+
else if (typeof block.text === "string") formatted.push({
|
|
523
|
+
type: "text",
|
|
524
|
+
text: block.text
|
|
525
|
+
});
|
|
526
|
+
else formatted.push(formatContent(block, client));
|
|
527
|
+
}
|
|
528
|
+
return formatted;
|
|
529
|
+
}
|
|
530
|
+
//#endregion
|
|
531
|
+
//#region src/claude-agent-sdk/processor.ts
|
|
532
|
+
const FRAMEWORK = "claude-agent-sdk";
|
|
533
|
+
const PROVIDER = "anthropic";
|
|
534
|
+
/**
|
|
535
|
+
* Reconstructs per-generation metrics from the Anthropic streaming protocol the
|
|
536
|
+
* Agent SDK relays: every `message_start` to `message_stop` cycle is one model
|
|
537
|
+
* call.
|
|
538
|
+
*/
|
|
539
|
+
var GenerationTracker = class {
|
|
540
|
+
constructor() {
|
|
541
|
+
this._completed = [];
|
|
542
|
+
this._sawStreamEvents = false;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Queue the input of the next model call. The first call answers the prompt,
|
|
546
|
+
* and every later one answers the tool results of the call before it.
|
|
547
|
+
*/
|
|
548
|
+
setPendingInput(input) {
|
|
549
|
+
this._pendingInput = input;
|
|
550
|
+
}
|
|
551
|
+
appendPendingInput(input) {
|
|
552
|
+
this._pendingInput = [...this._pendingInput ?? [], ...input];
|
|
553
|
+
}
|
|
554
|
+
/** The queued input, still waiting for its model call. */
|
|
555
|
+
get pendingInput() {
|
|
556
|
+
return this._pendingInput;
|
|
557
|
+
}
|
|
558
|
+
processStreamEvent(event, client, ttftMs) {
|
|
559
|
+
this._sawStreamEvents = true;
|
|
560
|
+
if (event.type === "message_start") {
|
|
561
|
+
this.retryCurrent(/* @__PURE__ */ new Error("Model stream restarted before completing"));
|
|
562
|
+
const usage = event.message.usage;
|
|
563
|
+
this._current = {
|
|
564
|
+
spanId: (0, uuid.v4)(),
|
|
565
|
+
input: this._pendingInput,
|
|
566
|
+
startTime: performance.now() - (ttftMs ?? 0),
|
|
567
|
+
timeToFirstToken: ttftMs != null ? ttftMs / 1e3 : void 0,
|
|
568
|
+
model: event.message.model,
|
|
569
|
+
usage: readUsage(usage),
|
|
570
|
+
streamedOutput: []
|
|
571
|
+
};
|
|
572
|
+
this._pendingInput = void 0;
|
|
573
|
+
} else if (event.type === "content_block_start" && this._current?.streamedOutput) this._current.streamedOutput[event.index] = formatAssistantBlocks([event.content_block], client)[0];
|
|
574
|
+
else if (event.type === "content_block_delta") {
|
|
575
|
+
const block = this._current?.streamedOutput?.[event.index];
|
|
576
|
+
if (event.delta.type === "text_delta" && block?.type === "text") block.text += event.delta.text;
|
|
577
|
+
else if (event.delta.type === "thinking_delta" && block?.type === "reasoning") block.text += event.delta.thinking;
|
|
578
|
+
else if (event.delta.type === "input_json_delta" && block?.type === "function") {
|
|
579
|
+
const previous = block.function.arguments;
|
|
580
|
+
block.function.arguments = (typeof previous === "string" ? previous : "") + event.delta.partial_json;
|
|
581
|
+
}
|
|
582
|
+
} else if (event.type === "message_delta" && this._current) {
|
|
583
|
+
const usage = event.usage;
|
|
584
|
+
this._current.usage = {
|
|
585
|
+
...this._current.usage,
|
|
586
|
+
...readUsage(usage),
|
|
587
|
+
rawUsage: {
|
|
588
|
+
...this._current.usage.rawUsage,
|
|
589
|
+
...usage
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
if (event.delta?.stop_reason != null) this._current.stopReason = event.delta.stop_reason;
|
|
593
|
+
} else if (event.type === "message_stop") this.finishCurrent();
|
|
594
|
+
}
|
|
595
|
+
finishCurrent() {
|
|
596
|
+
if (this._current) {
|
|
597
|
+
this._current.endTime = performance.now();
|
|
598
|
+
this._completed.push(this._current);
|
|
599
|
+
this._lastModel = this._current.model;
|
|
600
|
+
this._current = void 0;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
retryCurrent(error) {
|
|
604
|
+
if (this._current) {
|
|
605
|
+
this._pendingInput = this._current.input;
|
|
606
|
+
this._current.error = error;
|
|
607
|
+
this.finishCurrent();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
setModel(model) {
|
|
611
|
+
if (model) this._lastModel = model;
|
|
612
|
+
}
|
|
613
|
+
popCompleted() {
|
|
614
|
+
return this._completed.shift();
|
|
615
|
+
}
|
|
616
|
+
/** Span ID of the generation in progress, before its `message_stop`. */
|
|
617
|
+
get currentSpanId() {
|
|
618
|
+
return this._current?.spanId;
|
|
619
|
+
}
|
|
620
|
+
get lastModel() {
|
|
621
|
+
return this._lastModel;
|
|
622
|
+
}
|
|
623
|
+
get sawStreamEvents() {
|
|
624
|
+
return this._sawStreamEvents;
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
/**
|
|
628
|
+
* Wraps `query()` from `@anthropic-ai/claude-agent-sdk` to capture PostHog LLM
|
|
629
|
+
* analytics.
|
|
630
|
+
*
|
|
631
|
+
* The Agent SDK runs Claude Code itself, so no request passes through an
|
|
632
|
+
* Anthropic client the `@posthog/ai` Anthropic wrapper could patch. This
|
|
633
|
+
* processor reads the SDK's own message stream instead and captures:
|
|
634
|
+
*
|
|
635
|
+
* - `$ai_generation` for each main-agent model call, reconstructed from the streamed
|
|
636
|
+
* Anthropic events;
|
|
637
|
+
* - `$ai_span` for every tool use, parented to the generation that asked for it;
|
|
638
|
+
* - `$ai_trace` for every turn, carrying its latency and reported cost.
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```typescript
|
|
642
|
+
* import { PostHogClaudeAgentProcessor } from '@posthog/ai/claude-agent-sdk'
|
|
643
|
+
* import { PostHog } from 'posthog-node'
|
|
644
|
+
*
|
|
645
|
+
* const phClient = new PostHog('<POSTHOG_API_KEY>')
|
|
646
|
+
* const processor = new PostHogClaudeAgentProcessor({ client: phClient, distinctId: 'user@example.com' })
|
|
647
|
+
*
|
|
648
|
+
* for await (const message of processor.query({ prompt: 'Explain this repo' })) {
|
|
649
|
+
* console.log(message)
|
|
650
|
+
* }
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
var PostHogClaudeAgentProcessor = class {
|
|
654
|
+
constructor(options) {
|
|
655
|
+
this._client = options.client;
|
|
656
|
+
this._traceOptions = options;
|
|
657
|
+
this._captureImmediate = options.captureImmediate ?? false;
|
|
658
|
+
this._onError = options.onError;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Drop-in replacement for `query()` from the Claude Agent SDK. Hidden partial
|
|
662
|
+
* messages pass their reply correlation to the first assistant message.
|
|
663
|
+
* Other messages and the control methods (`interrupt`,
|
|
664
|
+
* `setPermissionMode`, …) retain the SDK behavior.
|
|
665
|
+
*/
|
|
666
|
+
query({ prompt, options, posthog }) {
|
|
667
|
+
const trace = {
|
|
668
|
+
...this._traceOptions,
|
|
669
|
+
...posthog,
|
|
670
|
+
privacyMode: this._traceOptions.privacyMode === true || posthog?.privacyMode === true,
|
|
671
|
+
properties: {
|
|
672
|
+
...this._traceOptions.properties,
|
|
673
|
+
...posthog?.properties
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
for (const key of [
|
|
677
|
+
"$ai_trace_id",
|
|
678
|
+
"$ai_span_id",
|
|
679
|
+
"$ai_parent_id",
|
|
680
|
+
"$ai_session_id"
|
|
681
|
+
]) delete trace.properties?.[key];
|
|
682
|
+
const forwardStreamEvents = options?.includePartialMessages === true;
|
|
683
|
+
const state = this._createState(prompt, options, trace);
|
|
684
|
+
const inner = (0, _anthropic_ai_claude_agent_sdk.query)({
|
|
685
|
+
prompt: typeof prompt === "string" ? prompt : this._observePrompt(prompt, state),
|
|
686
|
+
options: {
|
|
687
|
+
...options,
|
|
688
|
+
includePartialMessages: true
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
const instrumented = this._instrument(inner, state, trace, forwardStreamEvents);
|
|
692
|
+
const iteration = {
|
|
693
|
+
next: instrumented.next.bind(instrumented),
|
|
694
|
+
streamInput: (stream) => inner.streamInput(this._observePrompt(stream, state)),
|
|
695
|
+
return: async (value) => {
|
|
696
|
+
try {
|
|
697
|
+
return await inner.return(value);
|
|
698
|
+
} finally {
|
|
699
|
+
await instrumented.return();
|
|
700
|
+
}
|
|
701
|
+
},
|
|
702
|
+
throw: async (error) => {
|
|
703
|
+
state.failure = error;
|
|
704
|
+
try {
|
|
705
|
+
return await inner.throw(error);
|
|
706
|
+
} finally {
|
|
707
|
+
await instrumented.return();
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
close: () => {
|
|
711
|
+
try {
|
|
712
|
+
inner.close();
|
|
713
|
+
} finally {
|
|
714
|
+
instrumented.return().catch((error) => this._handleError(error));
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
[Symbol.asyncDispose]: async () => {
|
|
718
|
+
try {
|
|
719
|
+
const dispose = Reflect.get(inner, Symbol.asyncDispose);
|
|
720
|
+
if (typeof dispose === "function") await dispose.call(inner);
|
|
721
|
+
else await inner.return();
|
|
722
|
+
} finally {
|
|
723
|
+
await instrumented.return();
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
[Symbol.asyncIterator]: () => wrapper
|
|
727
|
+
};
|
|
728
|
+
const wrapper = new Proxy(inner, { get(target, property) {
|
|
729
|
+
if (Object.hasOwn(iteration, property)) return iteration[property];
|
|
730
|
+
const value = Reflect.get(target, property, target);
|
|
731
|
+
return typeof value === "function" && property !== "constructor" ? value.bind(target) : value;
|
|
732
|
+
} });
|
|
733
|
+
return wrapper;
|
|
734
|
+
}
|
|
735
|
+
async *_instrument(inner, state, trace, forwardStreamEvents) {
|
|
736
|
+
try {
|
|
737
|
+
for await (const message of inner) {
|
|
738
|
+
try {
|
|
739
|
+
await this._handleMessage(message, state, trace);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
this._handleError(error);
|
|
742
|
+
}
|
|
743
|
+
if (message.type !== "stream_event" || forwardStreamEvents) {
|
|
744
|
+
if (!forwardStreamEvents && message.type === "assistant" && !message.parent_tool_use_id && state.userMessageUuid) {
|
|
745
|
+
const userMessageUuid = state.userMessageUuid;
|
|
746
|
+
state.userMessageUuid = void 0;
|
|
747
|
+
yield {
|
|
748
|
+
...message,
|
|
749
|
+
user_message_uuid: message.user_message_uuid ?? userMessageUuid
|
|
750
|
+
};
|
|
751
|
+
} else yield message;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
} catch (error) {
|
|
755
|
+
state.failure = error;
|
|
756
|
+
throw error;
|
|
757
|
+
} finally {
|
|
758
|
+
const failure = state.failure;
|
|
759
|
+
if (state.turnActive || state.turnCaptured || failure !== void 0 && !state.hasCapturedTrace) {
|
|
760
|
+
try {
|
|
761
|
+
state.tracker.finishCurrent();
|
|
762
|
+
await this._captureCompletedGenerations(state, trace, failure);
|
|
763
|
+
if (!state.tracker.sawStreamEvents && state.pendingOutput.length > 0) {
|
|
764
|
+
state.generationIndex += 1;
|
|
765
|
+
await this._captureGeneration({
|
|
766
|
+
spanId: (0, uuid.v4)(),
|
|
767
|
+
input: state.tracker.pendingInput,
|
|
768
|
+
startTime: state.turnStart,
|
|
769
|
+
endTime: performance.now(),
|
|
770
|
+
usage: {},
|
|
771
|
+
error: failure
|
|
772
|
+
}, state, trace);
|
|
773
|
+
}
|
|
774
|
+
} catch (error) {
|
|
775
|
+
this._handleError(error);
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
await this._captureTrace(state, trace, void 0, failure);
|
|
779
|
+
} catch (error) {
|
|
780
|
+
this._handleError(error);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
_createState(prompt, options, trace) {
|
|
786
|
+
const initialInput = [];
|
|
787
|
+
const systemPrompt = extractSystemPrompt(options);
|
|
788
|
+
if (systemPrompt) initialInput.push({
|
|
789
|
+
role: "system",
|
|
790
|
+
content: systemPrompt
|
|
791
|
+
});
|
|
792
|
+
const tracker = new GenerationTracker();
|
|
793
|
+
tracker.setPendingInput(initialInput.length > 0 ? initialInput : void 0);
|
|
794
|
+
return {
|
|
795
|
+
tracker,
|
|
796
|
+
systemPrompt,
|
|
797
|
+
pendingCaptures: [],
|
|
798
|
+
pendingTools: /* @__PURE__ */ new Map(),
|
|
799
|
+
traceId: trace.traceId ?? (0, uuid.v4)(),
|
|
800
|
+
turnStart: performance.now(),
|
|
801
|
+
turnCaptured: false,
|
|
802
|
+
turnActive: false,
|
|
803
|
+
hasCapturedTrace: false,
|
|
804
|
+
pendingPrompts: typeof prompt === "string" ? [{
|
|
805
|
+
type: "user",
|
|
806
|
+
message: {
|
|
807
|
+
role: "user",
|
|
808
|
+
content: prompt
|
|
809
|
+
},
|
|
810
|
+
parent_tool_use_id: null
|
|
811
|
+
}] : [],
|
|
812
|
+
generationIndex: 0,
|
|
813
|
+
pendingOutput: [],
|
|
814
|
+
totalCost: 0
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
async *_observePrompt(prompt, state) {
|
|
818
|
+
for await (const message of prompt) {
|
|
819
|
+
state.pendingPrompts.push(message);
|
|
820
|
+
yield message;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
_beginTurn(state, userMessageUuid) {
|
|
824
|
+
if (state.turnActive) return;
|
|
825
|
+
state.turnActive = true;
|
|
826
|
+
let end = userMessageUuid ? state.pendingPrompts.findIndex((message) => message.uuid === userMessageUuid) : -1;
|
|
827
|
+
if (end < 0) {
|
|
828
|
+
const candidates = state.pendingPrompts.filter((message) => message.shouldQuery !== false);
|
|
829
|
+
if (candidates.length !== 1 || userMessageUuid && candidates[0].uuid) return;
|
|
830
|
+
end = state.pendingPrompts.indexOf(candidates[0]);
|
|
831
|
+
}
|
|
832
|
+
if (end < 0) return;
|
|
833
|
+
let start = end;
|
|
834
|
+
while (start > 0 && state.pendingPrompts[start - 1].shouldQuery === false) start -= 1;
|
|
835
|
+
for (const message of state.pendingPrompts.splice(start, end - start + 1)) state.tracker.appendPendingInput([{
|
|
836
|
+
role: "user",
|
|
837
|
+
content: formatUserContent(message.message.content, this._client)
|
|
838
|
+
}]);
|
|
839
|
+
}
|
|
840
|
+
async _handleMessage(message, state, trace) {
|
|
841
|
+
if ("session_id" in message && message.session_id) state.sessionId = message.session_id;
|
|
842
|
+
if (message.type === "conversation_reset") state.totalCost = 0;
|
|
843
|
+
else if (message.type === "system" && message.subtype === "api_retry") {
|
|
844
|
+
state.tracker.retryCurrent(Object.assign(new Error(message.error), { status: message.error_status }));
|
|
845
|
+
await this._captureCompletedGenerations(state, trace);
|
|
846
|
+
} else if (message.type === "stream_event") {
|
|
847
|
+
if (message.parent_tool_use_id) return;
|
|
848
|
+
if (message.user_message_uuid) state.userMessageUuid = message.user_message_uuid;
|
|
849
|
+
if (message.event.type === "message_start") this._beginTurn(state, message.user_message_uuid);
|
|
850
|
+
state.tracker.processStreamEvent(message.event, this._client, message.ttft_ms);
|
|
851
|
+
await this._captureCompletedGenerations(state, trace);
|
|
852
|
+
} else if (message.type === "assistant") {
|
|
853
|
+
if (!message.parent_tool_use_id) this._beginTurn(state, message.user_message_uuid);
|
|
854
|
+
this._handleAssistantMessage(message, state, trace);
|
|
855
|
+
} else if (message.type === "user") {
|
|
856
|
+
if ("isReplay" in message && message.isReplay) return;
|
|
857
|
+
if (Array.isArray(message.message.content)) {
|
|
858
|
+
for (const block of message.message.content) if (block.type === "tool_result") await this._finishToolSpan(block.tool_use_id, state, trace, block);
|
|
859
|
+
}
|
|
860
|
+
if (message.parent_tool_use_id) return;
|
|
861
|
+
const content = formatUserContent(message.message?.content, this._client);
|
|
862
|
+
if (typeof content === "string" || Array.isArray(content) && content.length > 0) state.tracker.appendPendingInput([{
|
|
863
|
+
role: "user",
|
|
864
|
+
content
|
|
865
|
+
}]);
|
|
866
|
+
} else if (message.type === "result") try {
|
|
867
|
+
this._beginTurn(state, message.user_message_uuid);
|
|
868
|
+
state.tracker.finishCurrent();
|
|
869
|
+
await this._captureCompletedGenerations(state, trace, resultError(message), message);
|
|
870
|
+
if (message.total_cost_usd != null) {
|
|
871
|
+
state.turnCost = message.total_cost_usd - (message.total_cost_usd >= state.totalCost ? state.totalCost : 0);
|
|
872
|
+
state.totalCost = message.total_cost_usd;
|
|
873
|
+
}
|
|
874
|
+
if (!state.tracker.sawStreamEvents) await this._captureGenerationFromResult(message, state, trace);
|
|
875
|
+
} finally {
|
|
876
|
+
await this._captureTrace(state, trace, message);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async _captureCompletedGenerations(state, trace, failure, result) {
|
|
880
|
+
let generation = state.tracker.popCompleted();
|
|
881
|
+
while (generation) {
|
|
882
|
+
generation.error ??= failure;
|
|
883
|
+
state.generationIndex += 1;
|
|
884
|
+
state.lastGenerationSpanId = generation.spanId;
|
|
885
|
+
state.turnCaptured = true;
|
|
886
|
+
await this._captureGeneration(generation, state, trace, {}, result);
|
|
887
|
+
state.pendingOutput = [];
|
|
888
|
+
generation = state.tracker.popCompleted();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
_handleAssistantMessage(message, state, trace) {
|
|
892
|
+
if (!message.parent_tool_use_id) state.tracker.setModel(message.message?.model);
|
|
893
|
+
const parentSpanId = message.parent_tool_use_id ?? state.tracker.currentSpanId ?? state.lastGenerationSpanId;
|
|
894
|
+
const blocks = Array.isArray(message.message?.content) ? message.message.content : [];
|
|
895
|
+
for (const block of blocks) if (block?.type === "tool_use") {
|
|
896
|
+
state.turnCaptured = true;
|
|
897
|
+
this._startToolSpan(block, state, trace, parentSpanId);
|
|
898
|
+
}
|
|
899
|
+
if (!message.parent_tool_use_id) state.pendingOutput.push(...formatAssistantBlocks(message.message?.content, this._client));
|
|
900
|
+
}
|
|
901
|
+
async _captureGeneration(generation, state, trace, extraProperties = {}, result) {
|
|
902
|
+
const options = {
|
|
903
|
+
traceId: state.traceId,
|
|
904
|
+
model: generation.model ?? state.tracker.lastModel,
|
|
905
|
+
provider: PROVIDER,
|
|
906
|
+
baseURL: null,
|
|
907
|
+
httpStatus: result?.subtype === "success" ? result.api_error_status ?? void 0 : void 0,
|
|
908
|
+
error: generation.error,
|
|
909
|
+
input: generation.input ?? [],
|
|
910
|
+
output: formatContent(formatOutput([...state.pendingOutput, ...generation.streamedOutput?.slice(state.pendingOutput.length).filter(Boolean) ?? []]), this._client),
|
|
911
|
+
latency: generation.endTime != null ? (generation.endTime - generation.startTime) / 1e3 : void 0,
|
|
912
|
+
timeToFirstToken: generation.timeToFirstToken,
|
|
913
|
+
usage: generation.usage,
|
|
914
|
+
stopReason: generation.stopReason,
|
|
915
|
+
groups: trace.groups,
|
|
916
|
+
privacyMode: trace.privacyMode ?? false,
|
|
917
|
+
captureImmediate: this._captureImmediate,
|
|
918
|
+
onError: this._onError,
|
|
919
|
+
properties: {
|
|
920
|
+
$ai_framework: FRAMEWORK,
|
|
921
|
+
$ai_span_id: generation.spanId,
|
|
922
|
+
$ai_span_name: `generation_${state.generationIndex}`,
|
|
923
|
+
...state.sessionId ? { $ai_session_id: state.sessionId } : {},
|
|
924
|
+
...extraProperties,
|
|
925
|
+
...trace.properties
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
await this._withDistinctId(state, trace, (distinctId) => captureAiGeneration(this._client, {
|
|
929
|
+
...options,
|
|
930
|
+
distinctId
|
|
931
|
+
}));
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Without partial messages there is no per-call metric, so the aggregate the
|
|
935
|
+
* result message reports becomes one generation.
|
|
936
|
+
*/
|
|
937
|
+
async _captureGenerationFromResult(result, state, trace) {
|
|
938
|
+
state.generationIndex += 1;
|
|
939
|
+
const endTime = performance.now();
|
|
940
|
+
const generation = {
|
|
941
|
+
spanId: (0, uuid.v4)(),
|
|
942
|
+
input: state.tracker.pendingInput ?? [],
|
|
943
|
+
startTime: endTime - (result.duration_api_ms ?? 0),
|
|
944
|
+
endTime,
|
|
945
|
+
usage: readUsage(result.usage),
|
|
946
|
+
stopReason: result.stop_reason ?? void 0,
|
|
947
|
+
error: resultError(result)
|
|
948
|
+
};
|
|
949
|
+
await this._captureGeneration(generation, state, trace, { ...state.turnCost != null ? { $ai_total_cost_usd: state.turnCost } : {} }, result);
|
|
950
|
+
}
|
|
951
|
+
_startToolSpan(block, state, trace, parentSpanId) {
|
|
952
|
+
const properties = {
|
|
953
|
+
$ai_span_id: block.id ?? (0, uuid.v4)(),
|
|
954
|
+
...parentSpanId ? { $ai_parent_id: parentSpanId } : {},
|
|
955
|
+
$ai_span_name: block.name,
|
|
956
|
+
$ai_span_type: "tool",
|
|
957
|
+
$ai_input_state: withPrivacyMode(this._client, trace.privacyMode ?? false, formatContent(block.input ?? {}, this._client))
|
|
958
|
+
};
|
|
959
|
+
state.pendingTools.set(properties.$ai_span_id, {
|
|
960
|
+
startTime: performance.now(),
|
|
961
|
+
properties
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
async _finishToolSpan(id, state, trace, result) {
|
|
965
|
+
const tool = state.pendingTools.get(id);
|
|
966
|
+
if (!tool) return;
|
|
967
|
+
state.pendingTools.delete(id);
|
|
968
|
+
const properties = {
|
|
969
|
+
...tool.properties,
|
|
970
|
+
$ai_latency: (performance.now() - tool.startTime) / 1e3,
|
|
971
|
+
...result ? {
|
|
972
|
+
$ai_output_state: withPrivacyMode(this._client, trace.privacyMode ?? false, formatContent(result.content, this._client)),
|
|
973
|
+
$ai_is_error: result.is_error === true
|
|
974
|
+
} : {}
|
|
975
|
+
};
|
|
976
|
+
await this._withDistinctId(state, trace, (distinctId) => this._captureLifecycleEvent("$ai_span", distinctId, state, trace, properties));
|
|
977
|
+
}
|
|
978
|
+
async _withDistinctId(state, trace, capture) {
|
|
979
|
+
if (typeof trace.distinctId === "function") state.pendingCaptures.push(capture);
|
|
980
|
+
else await capture(resolveDistinctId(trace.distinctId));
|
|
981
|
+
}
|
|
982
|
+
async _captureTrace(state, trace, result, failure) {
|
|
983
|
+
try {
|
|
984
|
+
const latency = result?.duration_ms != null ? result.duration_ms / 1e3 : (performance.now() - state.turnStart) / 1e3;
|
|
985
|
+
const isError = failure !== void 0 || result?.is_error === true;
|
|
986
|
+
const error = failure ?? (result && resultError(result));
|
|
987
|
+
for (const id of state.pendingTools.keys()) await this._finishToolSpan(id, state, trace);
|
|
988
|
+
let distinctId;
|
|
989
|
+
try {
|
|
990
|
+
distinctId = resolveDistinctId(trace.distinctId, result);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
this._handleError(error);
|
|
993
|
+
}
|
|
994
|
+
for (const capture of state.pendingCaptures.splice(0)) try {
|
|
995
|
+
await capture(distinctId);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
this._handleError(error);
|
|
998
|
+
}
|
|
999
|
+
await this._captureLifecycleEvent("$ai_trace", distinctId, state, trace, {
|
|
1000
|
+
$ai_trace_name: "claude_agent_sdk_query",
|
|
1001
|
+
$ai_latency: latency,
|
|
1002
|
+
...state.turnCost != null ? { $ai_total_cost_usd: state.turnCost } : {},
|
|
1003
|
+
...isError ? { $ai_is_error: true } : {},
|
|
1004
|
+
...error !== void 0 ? { $ai_error: stringifyError(error) } : {},
|
|
1005
|
+
...result?.subtype === "success" && result.api_error_status != null ? { $ai_http_status: result.api_error_status } : {}
|
|
1006
|
+
});
|
|
1007
|
+
} finally {
|
|
1008
|
+
state.hasCapturedTrace = true;
|
|
1009
|
+
state.traceId = trace.traceId ?? (0, uuid.v4)();
|
|
1010
|
+
state.turnStart = performance.now();
|
|
1011
|
+
state.turnCaptured = false;
|
|
1012
|
+
state.turnActive = false;
|
|
1013
|
+
state.userMessageUuid = void 0;
|
|
1014
|
+
state.failure = void 0;
|
|
1015
|
+
state.generationIndex = 0;
|
|
1016
|
+
state.lastGenerationSpanId = void 0;
|
|
1017
|
+
state.turnCost = void 0;
|
|
1018
|
+
state.pendingCaptures = [];
|
|
1019
|
+
state.pendingTools.clear();
|
|
1020
|
+
if (result?.queued_turn_count === 0) state.pendingPrompts = state.pendingPrompts.filter((message) => message.shouldQuery === false);
|
|
1021
|
+
state.tracker = new GenerationTracker();
|
|
1022
|
+
if (state.systemPrompt) state.tracker.setPendingInput([{
|
|
1023
|
+
role: "system",
|
|
1024
|
+
content: state.systemPrompt
|
|
1025
|
+
}]);
|
|
1026
|
+
state.pendingOutput = [];
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
async _captureLifecycleEvent(event, distinctId, state, trace, properties) {
|
|
1030
|
+
const message = {
|
|
1031
|
+
distinctId: distinctId ?? state.traceId,
|
|
1032
|
+
event,
|
|
1033
|
+
properties: {
|
|
1034
|
+
$ai_lib: "posthog-ai",
|
|
1035
|
+
$ai_lib_version: version,
|
|
1036
|
+
$ai_framework: FRAMEWORK,
|
|
1037
|
+
$ai_provider: PROVIDER,
|
|
1038
|
+
$ai_trace_id: state.traceId,
|
|
1039
|
+
...state.sessionId ? { $ai_session_id: state.sessionId } : {},
|
|
1040
|
+
...properties,
|
|
1041
|
+
...trace.properties,
|
|
1042
|
+
...distinctId ? {} : { $process_person_profile: false }
|
|
1043
|
+
},
|
|
1044
|
+
groups: trace.groups
|
|
1045
|
+
};
|
|
1046
|
+
try {
|
|
1047
|
+
if (this._captureImmediate) await captureAiEventImmediate(this._client, message);
|
|
1048
|
+
else captureAiEvent(this._client, message);
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
this._handleError(error);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
_handleError(error) {
|
|
1054
|
+
try {
|
|
1055
|
+
this._onError?.(error);
|
|
1056
|
+
} catch {}
|
|
1057
|
+
}
|
|
1058
|
+
};
|
|
1059
|
+
function formatOutput(content) {
|
|
1060
|
+
return content.length > 0 ? [{
|
|
1061
|
+
role: "assistant",
|
|
1062
|
+
content
|
|
1063
|
+
}] : [];
|
|
1064
|
+
}
|
|
1065
|
+
function resultError(result) {
|
|
1066
|
+
if (!result.is_error) return;
|
|
1067
|
+
return (result.subtype === "success" ? result.result : result.errors.join("\n")) || result.subtype;
|
|
1068
|
+
}
|
|
1069
|
+
function readUsage(usage) {
|
|
1070
|
+
return {
|
|
1071
|
+
...usage && Object.keys(usage).length > 0 ? { rawUsage: { ...usage } } : {},
|
|
1072
|
+
...usage?.input_tokens != null ? { inputTokens: usage.input_tokens } : {},
|
|
1073
|
+
...usage?.output_tokens != null ? { outputTokens: usage.output_tokens } : {},
|
|
1074
|
+
...usage?.cache_read_input_tokens != null ? { cacheReadInputTokens: usage.cache_read_input_tokens } : {},
|
|
1075
|
+
...usage?.cache_creation_input_tokens != null ? { cacheCreationInputTokens: usage.cache_creation_input_tokens } : {},
|
|
1076
|
+
...usage?.server_tool_use?.web_search_requests != null ? { webSearchCount: usage.server_tool_use.web_search_requests } : {}
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
function resolveDistinctId(resolver, result) {
|
|
1080
|
+
if (typeof resolver === "function") {
|
|
1081
|
+
if (!result) return;
|
|
1082
|
+
const resolved = resolver(result);
|
|
1083
|
+
return resolved ? String(resolved) : void 0;
|
|
1084
|
+
}
|
|
1085
|
+
return resolver ? String(resolver) : void 0;
|
|
1086
|
+
}
|
|
1087
|
+
//#endregion
|
|
1088
|
+
//#region src/claude-agent-sdk/index.ts
|
|
1089
|
+
/**
|
|
1090
|
+
* Create a PostHog-instrumented `query()` for the Claude Agent SDK.
|
|
1091
|
+
*
|
|
1092
|
+
* @param options - Configuration options
|
|
1093
|
+
* @returns A processor whose `query()` method replaces `query()` from the SDK
|
|
1094
|
+
*
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```typescript
|
|
1097
|
+
* import { instrument } from '@posthog/ai/claude-agent-sdk'
|
|
1098
|
+
* import { PostHog } from 'posthog-node'
|
|
1099
|
+
*
|
|
1100
|
+
* const phClient = new PostHog('<POSTHOG_API_KEY>')
|
|
1101
|
+
* const claude = instrument({ client: phClient, distinctId: 'user@example.com' })
|
|
1102
|
+
*
|
|
1103
|
+
* for await (const message of claude.query({ prompt: 'Explain this repo' })) {
|
|
1104
|
+
* console.log(message)
|
|
1105
|
+
* }
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
1108
|
+
function instrument(options) {
|
|
1109
|
+
return new PostHogClaudeAgentProcessor(options);
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Drop-in replacement for `query()` from the Claude Agent SDK, for a single
|
|
1113
|
+
* query. Use {@link instrument} to share one configuration across queries.
|
|
1114
|
+
*
|
|
1115
|
+
* @example
|
|
1116
|
+
* ```typescript
|
|
1117
|
+
* import { query } from '@posthog/ai/claude-agent-sdk'
|
|
1118
|
+
* import { PostHog } from 'posthog-node'
|
|
1119
|
+
*
|
|
1120
|
+
* const phClient = new PostHog('<POSTHOG_API_KEY>')
|
|
1121
|
+
*
|
|
1122
|
+
* for await (const message of query({
|
|
1123
|
+
* prompt: 'Explain this repo',
|
|
1124
|
+
* posthog: { client: phClient, distinctId: 'user@example.com' },
|
|
1125
|
+
* })) {
|
|
1126
|
+
* console.log(message)
|
|
1127
|
+
* }
|
|
1128
|
+
* ```
|
|
1129
|
+
*/
|
|
1130
|
+
function query(params) {
|
|
1131
|
+
const { prompt, options, posthog } = params;
|
|
1132
|
+
return new PostHogClaudeAgentProcessor(posthog).query({
|
|
1133
|
+
prompt,
|
|
1134
|
+
options
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
exports.PostHogClaudeAgentProcessor = PostHogClaudeAgentProcessor;
|
|
1139
|
+
exports.instrument = instrument;
|
|
1140
|
+
exports.query = query;
|
|
1141
|
+
|
|
1142
|
+
//# sourceMappingURL=index.cjs.map
|