@posthog/ai 8.8.1 → 8.9.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.
@@ -0,0 +1,1271 @@
1
+ import { ToolMessage, AIMessage } from '@langchain/core/messages';
2
+ import { convertToOpenAITool } from '@langchain/core/utils/function_calling';
3
+ import { extendInteropZodObject } from '@langchain/core/utils/types';
4
+ import { createMiddleware } from 'langchain';
5
+ import { v7 } from 'uuid';
6
+ import { z } from 'zod';
7
+ import { BaseCallbackHandler } from '@langchain/core/callbacks/base';
8
+
9
+ // Type guards for safer type checking
10
+
11
+ const isObject = value => {
12
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
13
+ };
14
+
15
+ /** @internal */
16
+
17
+ /** @internal */
18
+
19
+ /** @internal */
20
+ function isFullAiCaptureEnabled(client) {
21
+ return client?.enableFullAiCapture === true;
22
+ }
23
+
24
+ /** @internal */
25
+ function captureAiEvent(client, event) {
26
+ if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
27
+ client.captureAi(event);
28
+ return;
29
+ }
30
+ client.capture(event);
31
+ }
32
+
33
+ const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
34
+ const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
35
+ class Base64Recognizer {
36
+ recognize(value, minLength) {
37
+ const dataUrl = DATA_URL_PREFIX_RE.exec(value);
38
+ if (dataUrl) return {
39
+ kind: 'data-url',
40
+ mediaType: dataUrl[1]
41
+ };
42
+ if (value.length < minLength) return {
43
+ kind: 'none'
44
+ };
45
+ const confidencePrefix = value.slice(0, minLength);
46
+ if (BASE64_ALPHABET_RE.test(confidencePrefix)) {
47
+ return {
48
+ kind: 'raw'
49
+ };
50
+ } else {
51
+ return {
52
+ kind: 'none'
53
+ };
54
+ }
55
+ }
56
+ }
57
+
58
+ const MIME_HINT_KEYS = ['mediaType', 'media_type', 'mimeType', 'mime_type'];
59
+ const STRONG_CONTEXT_KEYS = new Set(['data', 'file_data', 'fileData', 'image_url', 'imageUrl', 'video_url', 'videoUrl', 'audio', 'audio_data', 'audioData', 'inline_data', 'inlineData', 'source', 'result']);
60
+ const STRONG_CONTEXT_TYPES = new Set(['image', 'image_url', 'input_image', 'audio', 'input_audio', 'video', 'video_url', 'file', 'input_file', 'document', 'media', 'file-data']);
61
+ const FILE_FAMILY_TYPES = new Set(['file', 'input_file', 'document', 'media', 'file-data']);
62
+ const KNOWN_AUDIO_FORMATS = new Set(['wav', 'mp3', 'ogg', 'flac', 'm4a', 'aac', 'webm']);
63
+ class MediaTypeContext {
64
+ static EMPTY = new MediaTypeContext(undefined, undefined);
65
+ constructor(parent, key, explicitMediaType) {
66
+ this.parent = parent;
67
+ this.key = key;
68
+ this.explicitMediaType = explicitMediaType;
69
+ }
70
+ inferMediaType() {
71
+ return this.inferFromSiblingMime() ?? this.inferFromSiblingFormat() ?? this.inferFromParentType() ?? this.inferFromKey();
72
+ }
73
+ inferFromSiblingMime() {
74
+ if (this.explicitMediaType) return this.explicitMediaType;
75
+ if (!this.parent) return undefined;
76
+ for (const hint of MIME_HINT_KEYS) {
77
+ const v = this.parent[hint];
78
+ if (typeof v === 'string') return v;
79
+ }
80
+ return undefined;
81
+ }
82
+ inferFromSiblingFormat() {
83
+ if (!this.parent) return undefined;
84
+ const fmt = this.parent.format;
85
+ if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) {
86
+ return `audio/${fmt.toLowerCase()}`;
87
+ }
88
+ return undefined;
89
+ }
90
+ inferFromParentType() {
91
+ if (!this.parent) return undefined;
92
+ const t = this.parent.type;
93
+ if (typeof t !== 'string') return undefined;
94
+ if (t === 'image' || t === 'image_url' || t === 'input_image') return 'image';
95
+ if (t === 'audio' || t === 'input_audio') return 'audio';
96
+ if (t === 'video' || t === 'video_url') return 'video';
97
+ if (FILE_FAMILY_TYPES.has(t)) return 'application/octet-stream';
98
+ return undefined;
99
+ }
100
+ inferFromKey() {
101
+ if (!this.key) return undefined;
102
+ const key = this.key.toLowerCase();
103
+ if (key.includes('audio')) return 'audio';
104
+ if (key.includes('video')) return 'video';
105
+ if (key.includes('image')) return 'image';
106
+ if (key.includes('file') || key.includes('document')) return 'application/octet-stream';
107
+ return undefined;
108
+ }
109
+ hasExplicitBinaryMediaType() {
110
+ if (!this.explicitMediaType && (!this.parent || !this.key || !STRONG_CONTEXT_KEYS.has(this.key))) return false;
111
+ const mediaType = this.inferFromSiblingMime();
112
+ return mediaType !== undefined && !mediaType.toLowerCase().startsWith('text/');
113
+ }
114
+ signalsBinary() {
115
+ if (this.explicitMediaType) return true;
116
+ if (this.parent) {
117
+ for (const hint of MIME_HINT_KEYS) {
118
+ if (typeof this.parent[hint] === 'string') return true;
119
+ }
120
+ const fmt = this.parent.format;
121
+ if (typeof fmt === 'string' && KNOWN_AUDIO_FORMATS.has(fmt.toLowerCase())) return true;
122
+ const t = this.parent.type;
123
+ if (typeof t === 'string' && STRONG_CONTEXT_TYPES.has(t)) return true;
124
+ }
125
+ if (this.key && STRONG_CONTEXT_KEYS.has(this.key)) return true;
126
+ return false;
127
+ }
128
+ }
129
+
130
+ const STRONG_CONTEXT_MIN_LENGTH = 64;
131
+ const WEAK_CONTEXT_MIN_LENGTH = 1024;
132
+ class BinaryContentRedactor {
133
+ visited = new WeakSet();
134
+ constructor(recognizer = new Base64Recognizer()) {
135
+ this.recognizer = recognizer;
136
+ }
137
+ redact(value, mediaType) {
138
+ this.visited = new WeakSet();
139
+ return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
140
+ }
141
+ walk(value, ctx) {
142
+ if (value === null || value === undefined) return value;
143
+ if (typeof value === 'string') return this.redactString(value, ctx);
144
+ if (typeof value !== 'object') return value;
145
+
146
+ // Buffer extends Uint8Array, so this branch catches both.
147
+ if (typeof Uint8Array !== 'undefined' && value instanceof Uint8Array) {
148
+ return this.placeholderFor(ctx.inferMediaType());
149
+ }
150
+ if (this.visited.has(value)) return null;
151
+ this.visited.add(value);
152
+ if (Array.isArray(value)) {
153
+ return value.map(item => this.walk(item, ctx));
154
+ }
155
+ const obj = value;
156
+ const out = {};
157
+ for (const k of Object.keys(obj)) {
158
+ out[k] = this.walk(obj[k], new MediaTypeContext(obj, k));
159
+ }
160
+ return out;
161
+ }
162
+ redactString(value, ctx) {
163
+ const hasExplicitBinaryMediaType = ctx.hasExplicitBinaryMediaType();
164
+ const recognitionValue = hasExplicitBinaryMediaType ? value.replace(/[\r\n]/g, '') : value;
165
+ const minLength = hasExplicitBinaryMediaType ? Math.min(recognitionValue.length, STRONG_CONTEXT_MIN_LENGTH) : ctx.signalsBinary() ? STRONG_CONTEXT_MIN_LENGTH : WEAK_CONTEXT_MIN_LENGTH;
166
+ const recognition = this.recognizer.recognize(recognitionValue, minLength);
167
+ switch (recognition.kind) {
168
+ case 'data-url':
169
+ return this.placeholderFor(recognition.mediaType);
170
+ case 'raw':
171
+ return this.placeholderFor(ctx.inferMediaType());
172
+ case 'none':
173
+ return value;
174
+ }
175
+ }
176
+ placeholderFor(mediaType) {
177
+ if (!mediaType) return '[base64 redacted]';
178
+ if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
179
+ return `[base64 ${mediaType} redacted]`;
180
+ }
181
+ }
182
+
183
+ const redactor = new BinaryContentRedactor();
184
+ const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
185
+ const sanitizeLangChain = (data, client) => sanitize(data, client);
186
+
187
+ const STRING_FORMAT = 'utf8';
188
+
189
+ // Reused across calls to avoid per-invocation allocation; truncate() runs
190
+ // hundreds of times for prompts with many parts.
191
+ new TextEncoder();
192
+ new TextDecoder(STRING_FORMAT, {
193
+ fatal: false
194
+ });
195
+
196
+ /**
197
+ * Safely converts content to a string, preserving structure for objects/arrays.
198
+ * - If content is already a string, returns it as-is
199
+ * - If content is an object or array, stringifies it with JSON.stringify to preserve structure
200
+ * - Otherwise, converts to string with String()
201
+ *
202
+ * This prevents the "[object Object]" bug when objects are naively converted to strings.
203
+ *
204
+ * @param content - The content to convert to a string
205
+ * @returns A string representation that preserves structure for complex types
206
+ */
207
+ function toContentString(content) {
208
+ if (typeof content === 'string') {
209
+ return content;
210
+ }
211
+ if (content !== undefined && content !== null && typeof content === 'object') {
212
+ try {
213
+ return JSON.stringify(content);
214
+ } catch {
215
+ // Fallback for circular refs, BigInt, or objects with throwing toJSON
216
+ return String(content);
217
+ }
218
+ }
219
+ return String(content);
220
+ }
221
+ const getModelParams = (params, responseServiceTier) => {
222
+ if (!params) {
223
+ return {};
224
+ }
225
+ const modelParams = {};
226
+ const paramKeys = ['temperature', 'max_tokens', 'max_completion_tokens', 'top_p', 'frequency_penalty', 'presence_penalty', 'n', 'stop', 'stream', 'streaming', 'language', 'response_format', 'timestamp_granularities', 'service_tier'];
227
+ for (const key of paramKeys) {
228
+ if (key in params && params[key] !== undefined) {
229
+ modelParams[key] = params[key];
230
+ }
231
+ }
232
+ return modelParams;
233
+ };
234
+ const withPrivacyMode = (client, privacyMode, input) => {
235
+ return client.privacy_mode || privacyMode ? null : input;
236
+ };
237
+ function sanitizeValues(obj) {
238
+ if (obj === undefined || obj === null) {
239
+ return obj;
240
+ }
241
+ const jsonSafe = JSON.parse(JSON.stringify(obj));
242
+ if (typeof jsonSafe === 'string') {
243
+ // Sanitize lone surrogates by round-tripping through UTF-8
244
+ return new TextDecoder().decode(new TextEncoder().encode(jsonSafe));
245
+ } else if (Array.isArray(jsonSafe)) {
246
+ return jsonSafe.map(sanitizeValues);
247
+ } else if (jsonSafe && typeof jsonSafe === 'object') {
248
+ return Object.fromEntries(Object.entries(jsonSafe).map(([k, v]) => [k, sanitizeValues(v)]));
249
+ }
250
+ return jsonSafe;
251
+ }
252
+
253
+ var version = "8.9.0";
254
+
255
+ const DEFAULT_MAX_DEPTH = 3;
256
+ const MAX_STACK_LINES = 20;
257
+ function serializeError(value, depth = DEFAULT_MAX_DEPTH) {
258
+ if (depth < 0 || value === null || typeof value !== 'object') {
259
+ return value;
260
+ }
261
+ if (value instanceof Error) {
262
+ const out = {
263
+ name: value.name,
264
+ message: value.message,
265
+ stack: truncateStack(value.stack)
266
+ };
267
+ for (const key of Object.keys(value)) {
268
+ out[key] = serializeError(value[key], depth - 1);
269
+ }
270
+ if (value.cause !== undefined) {
271
+ out.cause = serializeError(value.cause, depth - 1);
272
+ }
273
+ return out;
274
+ }
275
+ if (Array.isArray(value)) {
276
+ return value.map(item => serializeError(item, depth - 1));
277
+ }
278
+ return value;
279
+ }
280
+ function stringifyError(error) {
281
+ try {
282
+ return JSON.stringify(sanitizeValues(serializeError(error)));
283
+ } catch {
284
+ if (error instanceof Error) {
285
+ return JSON.stringify({
286
+ name: error.name,
287
+ message: error.message
288
+ });
289
+ }
290
+ return JSON.stringify({
291
+ message: String(error)
292
+ });
293
+ }
294
+ }
295
+ function truncateStack(stack) {
296
+ if (!stack) {
297
+ return stack;
298
+ }
299
+ const lines = stack.split('\n');
300
+ if (lines.length <= MAX_STACK_LINES) {
301
+ return stack;
302
+ }
303
+ return [...lines.slice(0, MAX_STACK_LINES), '... (truncated)'].join('\n');
304
+ }
305
+
306
+ // Warn when a wrapper's base_url points at the PostHog AI Gateway: the gateway
307
+ // emits its own $ai_generation, so each call would be captured (and, for billable
308
+ // products, billed) twice. We only warn — the wrapper's event carries data the
309
+ // gateway never sees (groups, custom properties, trace hierarchy).
310
+
311
+ // Keep in sync with the gateway's deployed hosts (see services/llm-gateway in the
312
+ // main repo). gateway.us.posthog.com is live today; the rest are listed ahead of
313
+ // any traffic moving to them.
314
+ const POSTHOG_AI_GATEWAY_HOSTS = ['gateway.posthog.com', 'gateway.us.posthog.com', 'gateway.eu.posthog.com', 'ai-gateway.us.posthog.com', 'ai-gateway.eu.posthog.com'];
315
+
316
+ // Swap for the dedicated AI Gateway page once it ships.
317
+ const GATEWAY_DOCS_URL = 'https://posthog.com/docs/ai-observability';
318
+ const extractHost = baseURL => {
319
+ try {
320
+ // Tolerate bare hosts that omit a scheme, e.g. "gateway.us.posthog.com/v1".
321
+ const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseURL);
322
+ return new URL(hasScheme ? baseURL : `https://${baseURL}`).hostname.toLowerCase();
323
+ } catch {
324
+ return undefined;
325
+ }
326
+ };
327
+ const isPostHogAiGatewayUrl = baseURL => {
328
+ if (!baseURL) {
329
+ return false;
330
+ }
331
+ const host = extractHost(baseURL);
332
+ return host !== undefined && POSTHOG_AI_GATEWAY_HOSTS.includes(host);
333
+ };
334
+
335
+ // Warns on every gateway call by design: the misconfiguration is impossible to
336
+ // miss that way, and a doubled bill is worse than noisy logs.
337
+ const warnIfPostHogAiGateway = baseURL => {
338
+ if (!isPostHogAiGatewayUrl(baseURL)) {
339
+ return;
340
+ }
341
+ 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}.`);
342
+ };
343
+
344
+ // Mirror LangGraph's isGraphBubbleUp guard without adding LangGraph as a dependency. Every
345
+ // LangGraph control-flow exception (GraphInterrupt, NodeInterrupt, ParentCommand, GraphDrained,
346
+ // and future subclasses) exposes a prototype getter `is_bubble_up` that returns true, which the
347
+ // LangGraph runtime itself uses to distinguish control flow from real failures. The getter reads
348
+ // as undefined on ordinary Errors and works across duplicated LangGraph package copies.
349
+ const isLangGraphControlFlow = error => error.is_bubble_up === true;
350
+
351
+ /** A run may either be a Span or a Generation */
352
+
353
+ /** Storage for run metadata */
354
+
355
+ class LangChainCallbackHandler extends BaseCallbackHandler {
356
+ name = 'PosthogCallbackHandler';
357
+ runs = {};
358
+ parentTree = {};
359
+ constructor(options) {
360
+ if (!options.client) {
361
+ throw new Error('PostHog client is required');
362
+ }
363
+ super();
364
+ this.client = options.client;
365
+ this.distinctId = options.distinctId;
366
+ this.traceId = options.traceId;
367
+ this.properties = options.properties || {};
368
+ this.privacyMode = options.privacyMode || false;
369
+ this.groups = options.groups || {};
370
+ this.debug = options.debug || false;
371
+ }
372
+
373
+ // ===== CALLBACK METHODS =====
374
+
375
+ handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, _runType, runName, extra) {
376
+ this._logDebugEvent('on_chain_start', runId, parentRunId, {
377
+ inputs,
378
+ tags
379
+ });
380
+ this._setParentOfRun(runId, parentRunId);
381
+ this._setTraceOrSpanMetadata(chain, inputs, runId, parentRunId, metadata, tags, runName);
382
+ if (typeof extra?.posthogStartTime === 'number' && Number.isFinite(extra.posthogStartTime)) {
383
+ this.runs[runId].startTime = extra.posthogStartTime;
384
+ }
385
+ }
386
+ handleChainEnd(outputs, runId, parentRunId, tags, _kwargs) {
387
+ this._logAndPopTraceOrSpan('on_chain_end', runId, parentRunId, {
388
+ outputs,
389
+ tags
390
+ }, outputs);
391
+ }
392
+ handleChainError(error, runId, parentRunId, tags, _kwargs) {
393
+ this._logAndPopTraceOrSpan('on_chain_error', runId, parentRunId, {
394
+ error,
395
+ tags
396
+ }, error);
397
+ }
398
+ handleChatModelStart(serialized, messages, runId, parentRunId, extraParams, tags, metadata, runName) {
399
+ this._logDebugEvent('on_chat_model_start', runId, parentRunId, {
400
+ messages,
401
+ tags
402
+ });
403
+ this._setParentOfRun(runId, parentRunId);
404
+ // Flatten the two-dimensional messages and convert each message to a plain object
405
+ const input = messages.flat().map(m => this._convertMessageToDict(m));
406
+ this._setLLMMetadata(serialized, runId, input, metadata, extraParams, runName);
407
+ }
408
+ handleLLMStart(serialized, prompts, runId, parentRunId, extraParams, tags, metadata, runName) {
409
+ this._logDebugEvent('on_llm_start', runId, parentRunId, {
410
+ prompts,
411
+ tags
412
+ });
413
+ this._setParentOfRun(runId, parentRunId);
414
+ this._setLLMMetadata(serialized, runId, prompts, metadata, extraParams, runName);
415
+ }
416
+ handleLLMEnd(output, runId, parentRunId, tags, _extraParams) {
417
+ this._logAndPopGeneration('on_llm_end', runId, parentRunId, {
418
+ output,
419
+ tags
420
+ }, output);
421
+ }
422
+ handleLLMError(err, runId, parentRunId, tags, _extraParams) {
423
+ this._logAndPopGeneration('on_llm_error', runId, parentRunId, {
424
+ err,
425
+ tags
426
+ }, err);
427
+ }
428
+ handleToolStart(tool, input, runId, parentRunId, tags, metadata, runName) {
429
+ this._logAndSetTraceOrSpan('on_tool_start', tool, input, runId, parentRunId, {
430
+ input,
431
+ tags
432
+ }, tags, metadata, runName);
433
+ }
434
+ handleToolEnd(output, runId, parentRunId, tags) {
435
+ this._logAndPopTraceOrSpan('on_tool_end', runId, parentRunId, {
436
+ output,
437
+ tags
438
+ }, output);
439
+ }
440
+ handleToolError(err, runId, parentRunId, tags) {
441
+ this._logAndPopTraceOrSpan('on_tool_error', runId, parentRunId, {
442
+ err,
443
+ tags
444
+ }, err);
445
+ }
446
+ handleRetrieverStart(retriever, query, runId, parentRunId, tags, metadata, name) {
447
+ this._logAndSetTraceOrSpan('on_retriever_start', retriever, query, runId, parentRunId, {
448
+ query,
449
+ tags
450
+ }, tags, metadata, name);
451
+ }
452
+ handleRetrieverEnd(documents, runId, parentRunId, tags) {
453
+ this._logAndPopTraceOrSpan('on_retriever_end', runId, parentRunId, {
454
+ documents,
455
+ tags
456
+ }, documents);
457
+ }
458
+ handleRetrieverError(err, runId, parentRunId, tags) {
459
+ this._logAndPopTraceOrSpan('on_retriever_error', runId, parentRunId, {
460
+ err,
461
+ tags
462
+ }, err);
463
+ }
464
+ handleAgentAction(action, runId, parentRunId, tags) {
465
+ this._logDebugEvent('on_agent_action', runId, parentRunId, {
466
+ action,
467
+ tags
468
+ });
469
+ this._setParentOfRun(runId, parentRunId);
470
+ this._setTraceOrSpanMetadata(null, action, runId, parentRunId);
471
+ }
472
+ handleAgentEnd(action, runId, parentRunId, tags) {
473
+ this._logDebugEvent('on_agent_finish', runId, parentRunId, {
474
+ action,
475
+ tags
476
+ });
477
+ this._popRunAndCaptureTraceOrSpan(runId, parentRunId, action);
478
+ }
479
+
480
+ // ===== PRIVATE HELPERS =====
481
+
482
+ _logAndSetTraceOrSpan(eventName, serialized, input, runId, parentRunId, debugPayload, tags, metadata, runName) {
483
+ this._logDebugEvent(eventName, runId, parentRunId, debugPayload);
484
+ this._setParentOfRun(runId, parentRunId);
485
+ this._setTraceOrSpanMetadata(serialized, input, runId, parentRunId, metadata, tags, runName);
486
+ }
487
+ _logAndPopTraceOrSpan(eventName, runId, parentRunId, debugPayload, result) {
488
+ this._logDebugEvent(eventName, runId, parentRunId, debugPayload);
489
+ this._popRunAndCaptureTraceOrSpan(runId, parentRunId, result);
490
+ }
491
+ _logAndPopGeneration(eventName, runId, parentRunId, debugPayload, result) {
492
+ this._logDebugEvent(eventName, runId, parentRunId, debugPayload);
493
+ this._popRunAndCaptureGeneration(runId, parentRunId, result);
494
+ }
495
+ _setParentOfRun(runId, parentRunId) {
496
+ if (parentRunId) {
497
+ this.parentTree[runId] = parentRunId;
498
+ }
499
+ }
500
+ _popParentOfRun(runId) {
501
+ delete this.parentTree[runId];
502
+ }
503
+ _findRootRun(runId) {
504
+ let id = runId;
505
+ while (this.parentTree[id]) {
506
+ id = this.parentTree[id];
507
+ }
508
+ return id;
509
+ }
510
+ _setTraceOrSpanMetadata(serialized, input, runId, parentRunId, ...args) {
511
+ // Use default names if not provided: if this is a top-level run, we mark it as a trace, otherwise as a span.
512
+ const defaultName = parentRunId ? 'span' : 'trace';
513
+ const runName = this._getLangchainRunName(serialized, ...args) || defaultName;
514
+ this.runs[runId] = {
515
+ name: runName,
516
+ input,
517
+ startTime: Date.now()
518
+ };
519
+ }
520
+ _setLLMMetadata(serialized, runId, messages, metadata, extraParams, runName) {
521
+ const runNameFound = this._getLangchainRunName(serialized, {
522
+ extraParams,
523
+ runName
524
+ }) || 'generation';
525
+ const generation = {
526
+ name: runNameFound,
527
+ input: sanitizeLangChain(messages, this.client),
528
+ startTime: Date.now()
529
+ };
530
+ if (extraParams) {
531
+ generation.modelParams = getModelParams(extraParams.invocation_params);
532
+ if (extraParams.invocation_params && extraParams.invocation_params.tools) {
533
+ generation.tools = extraParams.invocation_params.tools;
534
+ }
535
+ }
536
+ if (metadata) {
537
+ if (metadata.ls_model_name) {
538
+ generation.model = metadata.ls_model_name;
539
+ }
540
+ if (metadata.ls_provider) {
541
+ generation.provider = metadata.ls_provider;
542
+ }
543
+ }
544
+ if (serialized && 'kwargs' in serialized && serialized.kwargs.openai_api_base) {
545
+ generation.baseUrl = serialized.kwargs.openai_api_base;
546
+ }
547
+ this.runs[runId] = generation;
548
+ }
549
+ _popRunMetadata(runId) {
550
+ const endTime = Date.now();
551
+ const run = this.runs[runId];
552
+ if (!run) {
553
+ console.warn(`No run metadata found for run ${runId}`);
554
+ return undefined;
555
+ }
556
+ run.endTime = endTime;
557
+ delete this.runs[runId];
558
+ return run;
559
+ }
560
+ _getTraceId(runId) {
561
+ return this.traceId ? String(this.traceId) : this._findRootRun(runId);
562
+ }
563
+ _getParentRunId(traceId, _runId, parentRunId) {
564
+ // Replace the parent-run if not found in our stored parent tree.
565
+ if (parentRunId && !this.parentTree[parentRunId]) {
566
+ return traceId;
567
+ }
568
+ return parentRunId;
569
+ }
570
+ _safeCapture(message) {
571
+ try {
572
+ captureAiEvent(this.client, message);
573
+ } catch {
574
+ // Telemetry delivery must never affect the LangChain callback lifecycle.
575
+ }
576
+ }
577
+ _popRunAndCaptureTraceOrSpan(runId, parentRunId, outputs) {
578
+ const traceId = this._getTraceId(runId);
579
+ const isSpan = Boolean(parentRunId || this.parentTree[runId]);
580
+ this._popParentOfRun(runId);
581
+ const run = this._popRunMetadata(runId);
582
+ if (!run) {
583
+ return;
584
+ }
585
+ if ('modelParams' in run) {
586
+ console.warn(`Run ${runId} is a generation, but attempted to be captured as a trace/span.`);
587
+ return;
588
+ }
589
+ const actualParentRunId = this._getParentRunId(traceId, runId, parentRunId);
590
+ this._captureTraceOrSpan(traceId, runId, run, outputs, isSpan, actualParentRunId);
591
+ }
592
+ _captureTraceOrSpan(traceId, runId, run, outputs, isSpan, parentRunId) {
593
+ const eventName = isSpan ? '$ai_span' : '$ai_trace';
594
+ const latency = run.endTime ? (run.endTime - run.startTime) / 1000 : 0;
595
+ const eventProperties = {
596
+ $ai_lib: 'posthog-ai',
597
+ $ai_lib_version: version,
598
+ $ai_trace_id: traceId,
599
+ $ai_input_state: withPrivacyMode(this.client, this.privacyMode, sanitizeLangChain(run.input, this.client)),
600
+ $ai_latency: latency,
601
+ $ai_span_name: run.name,
602
+ $ai_span_id: runId,
603
+ $ai_framework: 'langchain'
604
+ };
605
+ if (parentRunId) {
606
+ eventProperties['$ai_parent_id'] = parentRunId;
607
+ }
608
+ Object.assign(eventProperties, this.properties);
609
+ if (!this.distinctId) {
610
+ eventProperties['$process_person_profile'] = false;
611
+ }
612
+ if (outputs instanceof Error) {
613
+ if (isLangGraphControlFlow(outputs)) {
614
+ // GraphInterrupt carries the pending interrupts (e.g. the question posed to a human).
615
+ // Surface them under the same `__interrupt__` key LangGraph hands back to the caller,
616
+ // so an interrupted span stays distinguishable from a node that returned nothing.
617
+ const interrupts = outputs.interrupts;
618
+ if (interrupts !== undefined) {
619
+ eventProperties['$ai_output_state'] = withPrivacyMode(this.client, this.privacyMode, sanitizeLangChain({
620
+ __interrupt__: interrupts
621
+ }, this.client));
622
+ }
623
+ } else {
624
+ eventProperties['$ai_error'] = stringifyError(outputs);
625
+ eventProperties['$ai_is_error'] = true;
626
+ }
627
+ } else if (outputs !== undefined) {
628
+ eventProperties['$ai_output_state'] = withPrivacyMode(this.client, this.privacyMode, sanitizeLangChain(outputs, this.client));
629
+ }
630
+ this._safeCapture({
631
+ distinctId: this.distinctId ? this.distinctId.toString() : runId,
632
+ event: eventName,
633
+ properties: eventProperties,
634
+ groups: this.groups
635
+ });
636
+ }
637
+ _popRunAndCaptureGeneration(runId, parentRunId, response) {
638
+ const traceId = this._getTraceId(runId);
639
+ this._popParentOfRun(runId);
640
+ const run = this._popRunMetadata(runId);
641
+ if (!run || typeof run !== 'object' || !('modelParams' in run)) {
642
+ console.warn(`Run ${runId} is not a generation, but attempted to be captured as such.`);
643
+ return;
644
+ }
645
+ const actualParentRunId = this._getParentRunId(traceId, runId, parentRunId);
646
+ this._captureGeneration(traceId, runId, run, response, actualParentRunId);
647
+ }
648
+ _captureGeneration(traceId, runId, run, output, parentRunId) {
649
+ const latency = run.endTime ? (run.endTime - run.startTime) / 1000 : 0;
650
+ warnIfPostHogAiGateway(run.baseUrl);
651
+ const eventProperties = {
652
+ $ai_lib: 'posthog-ai',
653
+ $ai_lib_version: version,
654
+ $ai_trace_id: traceId,
655
+ $ai_span_id: runId,
656
+ $ai_span_name: run.name,
657
+ $ai_provider: run.provider,
658
+ $ai_model: run.model,
659
+ $ai_model_parameters: run.modelParams,
660
+ $ai_input: withPrivacyMode(this.client, this.privacyMode, run.input),
661
+ $ai_http_status: 200,
662
+ $ai_latency: latency,
663
+ $ai_base_url: run.baseUrl,
664
+ $ai_framework: 'langchain'
665
+ };
666
+ if (parentRunId) {
667
+ eventProperties['$ai_parent_id'] = parentRunId;
668
+ }
669
+ if (run.tools) {
670
+ eventProperties['$ai_tools'] = run.tools;
671
+ }
672
+ if (output instanceof Error) {
673
+ eventProperties['$ai_http_status'] = output.status || 500;
674
+ eventProperties['$ai_error'] = stringifyError(output);
675
+ eventProperties['$ai_is_error'] = true;
676
+ } else {
677
+ // Handle token usage
678
+ const [inputTokens, outputTokens, additionalTokenData] = this.parseUsage(output, run.provider, run.model);
679
+ eventProperties['$ai_input_tokens'] = inputTokens;
680
+ eventProperties['$ai_output_tokens'] = outputTokens;
681
+
682
+ // Add additional token data to properties
683
+ if (additionalTokenData.cacheReadInputTokens) {
684
+ eventProperties['$ai_cache_read_input_tokens'] = additionalTokenData.cacheReadInputTokens;
685
+ }
686
+ if (additionalTokenData.cacheWriteInputTokens) {
687
+ eventProperties['$ai_cache_creation_input_tokens'] = additionalTokenData.cacheWriteInputTokens;
688
+ }
689
+ if (additionalTokenData.cacheWrite5mInputTokens !== undefined && additionalTokenData.cacheWrite1hInputTokens !== undefined) {
690
+ eventProperties['$ai_cache_creation_5m_input_tokens'] = additionalTokenData.cacheWrite5mInputTokens;
691
+ eventProperties['$ai_cache_creation_1h_input_tokens'] = additionalTokenData.cacheWrite1hInputTokens;
692
+ }
693
+ if (additionalTokenData.reasoningTokens) {
694
+ eventProperties['$ai_reasoning_tokens'] = additionalTokenData.reasoningTokens;
695
+ }
696
+ if (additionalTokenData.webSearchCount !== undefined) {
697
+ eventProperties['$ai_web_search_count'] = additionalTokenData.webSearchCount;
698
+ }
699
+
700
+ // Extract stop reason from generation info
701
+ const stopReason = this._extractStopReason(output);
702
+ if (stopReason) {
703
+ eventProperties['$ai_stop_reason'] = stopReason;
704
+ }
705
+
706
+ // Handle generations/completions
707
+ let completions;
708
+ if (output.generations && Array.isArray(output.generations)) {
709
+ const lastGeneration = output.generations[output.generations.length - 1];
710
+ if (Array.isArray(lastGeneration) && lastGeneration.length > 0) {
711
+ // Check if this is a ChatGeneration by looking at the first item
712
+ const isChatGeneration = 'message' in lastGeneration[0] && lastGeneration[0].message;
713
+ if (isChatGeneration) {
714
+ // For ChatGeneration, convert messages to dict format
715
+ completions = lastGeneration.map(gen => {
716
+ return this._convertMessageToDict(gen.message);
717
+ });
718
+ } else {
719
+ // For non-ChatGeneration, extract raw response
720
+ completions = lastGeneration.map(gen => {
721
+ return this._extractRawResponse(gen);
722
+ });
723
+ }
724
+ }
725
+ }
726
+ if (completions) {
727
+ eventProperties['$ai_output_choices'] = withPrivacyMode(this.client, this.privacyMode, completions);
728
+ }
729
+ }
730
+ Object.assign(eventProperties, this.properties);
731
+ if (!this.distinctId) {
732
+ eventProperties['$process_person_profile'] = false;
733
+ }
734
+ this._safeCapture({
735
+ distinctId: this.distinctId ? this.distinctId.toString() : traceId,
736
+ event: '$ai_generation',
737
+ properties: eventProperties,
738
+ groups: this.groups
739
+ });
740
+ }
741
+ _logDebugEvent(eventName, runId, parentRunId, extra) {
742
+ if (this.debug) {
743
+ console.log(`Event: ${eventName}, runId: ${runId}, parentRunId: ${parentRunId}, extra:`, extra);
744
+ }
745
+ }
746
+ _getLangchainRunName(serialized, ...args) {
747
+ if (args && args.length > 0) {
748
+ for (const arg of args) {
749
+ // LangChain hands runName through as a bare string, not wrapped in an object
750
+ if (typeof arg === 'string' && arg) {
751
+ return arg;
752
+ }
753
+ if (arg && typeof arg === 'object') {
754
+ if (arg.name) {
755
+ return arg.name;
756
+ }
757
+ if (arg.runName) {
758
+ return arg.runName;
759
+ }
760
+ }
761
+ }
762
+ }
763
+ if (serialized && serialized.name) {
764
+ return serialized.name;
765
+ }
766
+ if (serialized && serialized.id) {
767
+ return Array.isArray(serialized.id) ? serialized.id[serialized.id.length - 1] : serialized.id;
768
+ }
769
+ return undefined;
770
+ }
771
+ _convertLcToolCallsToOai(toolCalls) {
772
+ return toolCalls.map(toolCall => ({
773
+ type: 'function',
774
+ id: toolCall.id,
775
+ function: {
776
+ name: toolCall.name,
777
+ arguments: JSON.stringify(toolCall.args)
778
+ }
779
+ }));
780
+ }
781
+ _extractRawResponse(generation) {
782
+ // Extract the response from the last response of the LLM call
783
+ // We return the text of the response if not empty
784
+ if (generation.text != null && generation.text.trim() !== '') {
785
+ return generation.text.trim();
786
+ } else if (generation.message) {
787
+ // Additional kwargs contains the response in case of tool usage
788
+ return generation.message.additional_kwargs || generation.message.additionalKwargs || {};
789
+ } else {
790
+ // Not tool usage, some LLM responses can be simply empty
791
+ return '';
792
+ }
793
+ }
794
+ _convertMessageToDict(message) {
795
+ let messageDict = {};
796
+ const messageType = message.getType();
797
+ switch (messageType) {
798
+ case 'human':
799
+ messageDict = {
800
+ role: 'user',
801
+ content: message.content
802
+ };
803
+ break;
804
+ case 'ai':
805
+ messageDict = {
806
+ role: 'assistant',
807
+ content: message.content
808
+ };
809
+ if (message.tool_calls) {
810
+ messageDict.tool_calls = this._convertLcToolCallsToOai(message.tool_calls);
811
+ }
812
+ break;
813
+ case 'system':
814
+ messageDict = {
815
+ role: 'system',
816
+ content: message.content
817
+ };
818
+ break;
819
+ case 'tool':
820
+ messageDict = {
821
+ role: 'tool',
822
+ content: message.content
823
+ };
824
+ break;
825
+ case 'function':
826
+ messageDict = {
827
+ role: 'function',
828
+ content: message.content
829
+ };
830
+ break;
831
+ default:
832
+ messageDict = {
833
+ role: messageType,
834
+ content: toContentString(message.content)
835
+ };
836
+ break;
837
+ }
838
+ if (message.additional_kwargs) {
839
+ messageDict = {
840
+ ...messageDict,
841
+ ...message.additional_kwargs
842
+ };
843
+ }
844
+
845
+ // Sanitize the message content to redact base64 images
846
+ return sanitizeLangChain(messageDict, this.client);
847
+ }
848
+ _extractStopReason(output) {
849
+ if (!output.generations || !Array.isArray(output.generations)) {
850
+ return undefined;
851
+ }
852
+ const lastGeneration = output.generations[output.generations.length - 1];
853
+ if (!Array.isArray(lastGeneration) || lastGeneration.length === 0) {
854
+ return undefined;
855
+ }
856
+ const gen = lastGeneration[0];
857
+ const messageResponseMetadata = gen.message?.response_metadata;
858
+ const generationResponseMetadata = gen.generationInfo?.response_metadata;
859
+ const stopReason = messageResponseMetadata?.finish_reason || messageResponseMetadata?.stop_reason || gen.generationInfo?.finish_reason || generationResponseMetadata?.stop_reason || generationResponseMetadata?.finish_reason || gen.generationInfo?.stop_reason;
860
+ return stopReason != null ? String(stopReason) : undefined;
861
+ }
862
+ _extractCacheCreationTtlBreakdown(cacheCreation, aggregateValues) {
863
+ if (!isObject(cacheCreation)) {
864
+ return undefined;
865
+ }
866
+ const {
867
+ ephemeral_5m_input_tokens: cache5m,
868
+ ephemeral_1h_input_tokens: cache1h
869
+ } = cacheCreation;
870
+ const providedValues = [cache5m, cache1h].filter(value => value != null);
871
+ if (providedValues.length === 0 || !providedValues.every(value => typeof value === 'number' && Number.isFinite(value) && value >= 0)) {
872
+ return undefined;
873
+ }
874
+ const breakdown = [typeof cache5m === 'number' ? cache5m : 0, typeof cache1h === 'number' ? cache1h : 0];
875
+ const total = breakdown[0] + breakdown[1];
876
+ const validAggregates = aggregateValues.filter(value => typeof value === 'number' && Number.isFinite(value) && value >= 0);
877
+ return total > 0 && !validAggregates.some(aggregate => aggregate !== total) ? breakdown : undefined;
878
+ }
879
+ _extractBedrockCacheCreationTtlBreakdown(cacheDetails, aggregateValues) {
880
+ if (!Array.isArray(cacheDetails)) {
881
+ return undefined;
882
+ }
883
+ let cache5m = 0;
884
+ let cache1h = 0;
885
+ for (const detail of cacheDetails) {
886
+ if (!isObject(detail)) {
887
+ continue;
888
+ }
889
+ const ttl = typeof detail.ttl === 'string' ? detail.ttl.toLowerCase() : undefined;
890
+ const inputTokens = detail.inputTokens;
891
+ if (ttl !== '5m' && ttl !== 't5m' && ttl !== '1h' && ttl !== 't1h' || typeof inputTokens !== 'number' || !Number.isFinite(inputTokens) || inputTokens < 0) {
892
+ continue;
893
+ }
894
+ if (ttl === '5m' || ttl === 't5m') {
895
+ cache5m += inputTokens;
896
+ } else {
897
+ cache1h += inputTokens;
898
+ }
899
+ }
900
+ const total = cache5m + cache1h;
901
+ const validAggregates = aggregateValues.filter(value => typeof value === 'number' && Number.isFinite(value) && value >= 0);
902
+ if (total === 0 || validAggregates.some(aggregate => aggregate !== total)) {
903
+ return undefined;
904
+ }
905
+ return [cache5m, cache1h];
906
+ }
907
+ _parseUsageModel(usage, provider, model, inputIncludesCacheTokens = true, rawUsage) {
908
+ const conversionList = [['promptTokens', 'input'], ['completionTokens', 'output'], ['input_tokens', 'input'], ['output_tokens', 'output'], ['prompt_token_count', 'input'], ['candidates_token_count', 'output'], ['inputTokenCount', 'input'], ['outputTokenCount', 'output'], ['input_token_count', 'input'], ['generated_token_count', 'output']];
909
+ const parsedUsage = conversionList.reduce((acc, [modelKey, typeKey]) => {
910
+ const value = usage[modelKey];
911
+ if (value != null) {
912
+ const finalCount = Array.isArray(value) ? value.reduce((sum, tokenCount) => sum + tokenCount, 0) : value;
913
+ acc[typeKey] = finalCount;
914
+ }
915
+ return acc;
916
+ }, {
917
+ input: 0,
918
+ output: 0
919
+ });
920
+
921
+ // Extract additional token details like cached tokens and reasoning tokens
922
+ const additionalTokenData = {};
923
+
924
+ // Check for cached tokens in various formats
925
+ if (usage.prompt_tokens_details?.cached_tokens != null) {
926
+ additionalTokenData.cacheReadInputTokens = usage.prompt_tokens_details.cached_tokens;
927
+ } else if (usage.input_token_details?.cache_read != null) {
928
+ additionalTokenData.cacheReadInputTokens = usage.input_token_details.cache_read;
929
+ } else if (usage.cachedPromptTokens != null) {
930
+ additionalTokenData.cacheReadInputTokens = usage.cachedPromptTokens;
931
+ } else if (usage.cache_read_input_tokens != null) {
932
+ additionalTokenData.cacheReadInputTokens = usage.cache_read_input_tokens;
933
+ }
934
+
935
+ // Check for cache write/creation tokens in various formats
936
+ if (usage.cache_creation_input_tokens != null) {
937
+ additionalTokenData.cacheWriteInputTokens = usage.cache_creation_input_tokens;
938
+ } else if (usage.input_token_details?.cache_creation != null) {
939
+ additionalTokenData.cacheWriteInputTokens = usage.input_token_details.cache_creation;
940
+ }
941
+ const directCacheCreationAggregates = [usage.cache_creation_input_tokens, usage.input_token_details?.cache_creation, usage.cacheWriteInputTokens, rawUsage?.cache_creation_input_tokens, rawUsage?.input_token_details?.cache_creation, rawUsage?.cacheWriteInputTokens, additionalTokenData.cacheWriteInputTokens];
942
+ const cacheCreationTtl = this._extractCacheCreationTtlBreakdown(usage.cache_creation, directCacheCreationAggregates) ?? this._extractCacheCreationTtlBreakdown(rawUsage?.cache_creation, directCacheCreationAggregates) ?? this._extractBedrockCacheCreationTtlBreakdown(usage.cacheDetails, [usage.cacheWriteInputTokens, additionalTokenData.cacheWriteInputTokens]) ?? this._extractBedrockCacheCreationTtlBreakdown(rawUsage?.cacheDetails, [rawUsage?.cacheWriteInputTokens, additionalTokenData.cacheWriteInputTokens]);
943
+ if (cacheCreationTtl) {
944
+ const [cacheWrite5mInputTokens, cacheWrite1hInputTokens] = cacheCreationTtl;
945
+ additionalTokenData.cacheWrite5mInputTokens = cacheWrite5mInputTokens;
946
+ additionalTokenData.cacheWrite1hInputTokens = cacheWrite1hInputTokens;
947
+ additionalTokenData.cacheWriteInputTokens = cacheWrite5mInputTokens + cacheWrite1hInputTokens;
948
+ }
949
+
950
+ // Check for reasoning tokens in various formats
951
+ if (usage.completion_tokens_details?.reasoning_tokens != null) {
952
+ additionalTokenData.reasoningTokens = usage.completion_tokens_details.reasoning_tokens;
953
+ } else if (usage.output_token_details?.reasoning != null) {
954
+ additionalTokenData.reasoningTokens = usage.output_token_details.reasoning;
955
+ } else if (usage.reasoningTokens != null) {
956
+ additionalTokenData.reasoningTokens = usage.reasoningTokens;
957
+ }
958
+
959
+ // Extract web search counts from various provider formats
960
+ let webSearchCount;
961
+
962
+ // Priority 1: Exact Count
963
+ // Check Anthropic format (server_tool_use.web_search_requests)
964
+ if (usage.server_tool_use?.web_search_requests !== undefined) {
965
+ webSearchCount = usage.server_tool_use.web_search_requests;
966
+ }
967
+ // Priority 2: Binary Detection (1 or 0)
968
+ // Check for citations array (Perplexity)
969
+ else if (usage.citations && Array.isArray(usage.citations) && usage.citations.length > 0) {
970
+ webSearchCount = 1;
971
+ }
972
+ // Check for search_results array (Perplexity via OpenRouter)
973
+ else if (usage.search_results && Array.isArray(usage.search_results) && usage.search_results.length > 0) {
974
+ webSearchCount = 1;
975
+ }
976
+ // Check for search_context_size (Perplexity via OpenRouter)
977
+ else if (usage.search_context_size) {
978
+ webSearchCount = 1;
979
+ }
980
+ // Check for annotations with url_citation type
981
+ else if (usage.annotations && Array.isArray(usage.annotations)) {
982
+ const hasUrlCitation = usage.annotations.some(ann => {
983
+ return ann && typeof ann === 'object' && 'type' in ann && ann.type === 'url_citation';
984
+ });
985
+ if (hasUrlCitation) {
986
+ webSearchCount = 1;
987
+ }
988
+ }
989
+ // Check Gemini format (grounding metadata - binary 0 or 1)
990
+ else if (usage.grounding_metadata?.grounding_support !== undefined || usage.grounding_metadata?.web_search_queries !== undefined) {
991
+ webSearchCount = 1;
992
+ }
993
+ if (webSearchCount !== undefined) {
994
+ additionalTokenData.webSearchCount = webSearchCount;
995
+ }
996
+
997
+ // For Anthropic providers, LangChain reports input_tokens as the sum of all input tokens.
998
+ // Our cost calculation expects them to be separate for Anthropic, so we subtract cache tokens.
999
+ // Both cache_read and cache_write tokens should be subtracted since Anthropic's raw API
1000
+ // reports input_tokens as tokens NOT read from or used to create a cache.
1001
+ // For other providers (OpenAI, etc.), input_tokens already excludes cache tokens as expected.
1002
+ // Match logic consistent with plugin-server: exact match on provider OR substring match on model
1003
+ let isAnthropic = false;
1004
+ if (provider && provider.toLowerCase() === 'anthropic') {
1005
+ isAnthropic = true;
1006
+ } else if (model && model.toLowerCase().includes('anthropic')) {
1007
+ isAnthropic = true;
1008
+ }
1009
+ if (isAnthropic && inputIncludesCacheTokens && parsedUsage.input) {
1010
+ const cacheTokens = (additionalTokenData.cacheReadInputTokens || 0) + (additionalTokenData.cacheWriteInputTokens || 0);
1011
+ if (cacheTokens > 0) {
1012
+ parsedUsage.input = Math.max(parsedUsage.input - cacheTokens, 0);
1013
+ }
1014
+ }
1015
+ return [parsedUsage.input, parsedUsage.output, additionalTokenData];
1016
+ }
1017
+ parseUsage(response, provider, model) {
1018
+ const isNonEmptyUsage = usage => isObject(usage) && Object.keys(usage).length > 0;
1019
+ const firstNonEmptyUsage = (...candidates) => candidates.find(isNonEmptyUsage);
1020
+ let normalizedGenerationUsage;
1021
+ let rawGenerationUsage;
1022
+ let fallbackGenerationUsage;
1023
+ for (const generation of response.generations ?? []) {
1024
+ for (const genChunk of generation) {
1025
+ const generationInfo = genChunk.generationInfo ?? {};
1026
+ const message = 'message' in genChunk ? genChunk.message : undefined;
1027
+ const messageUsage = message && typeof message === 'object' && 'usage_metadata' in message ? message.usage_metadata : undefined;
1028
+ normalizedGenerationUsage = firstNonEmptyUsage(normalizedGenerationUsage, messageUsage, generationInfo.usage_metadata);
1029
+ const messageResponseMetadata = message && typeof message === 'object' && 'response_metadata' in message && isObject(message.response_metadata) ? message.response_metadata : undefined;
1030
+ const generationResponseMetadata = isObject(generationInfo.response_metadata) ? generationInfo.response_metadata : undefined;
1031
+ const messageStreamMetadata = isObject(messageResponseMetadata?.metadata) ? messageResponseMetadata.metadata : undefined;
1032
+ const generationStreamMetadata = isObject(generationResponseMetadata?.metadata) ? generationResponseMetadata.metadata : undefined;
1033
+ rawGenerationUsage = firstNonEmptyUsage(rawGenerationUsage, messageResponseMetadata?.usage, messageStreamMetadata?.usage, generationResponseMetadata?.usage, generationStreamMetadata?.usage);
1034
+ fallbackGenerationUsage = firstNonEmptyUsage(fallbackGenerationUsage, messageResponseMetadata?.['amazon-bedrock-invocationMetrics'], generationResponseMetadata?.['amazon-bedrock-invocationMetrics'], generationInfo.usage_metadata);
1035
+ }
1036
+ }
1037
+ const isAnthropic = provider?.toLowerCase() === 'anthropic' || model?.toLowerCase().includes('anthropic') === true;
1038
+ if (isAnthropic && isNonEmptyUsage(normalizedGenerationUsage)) {
1039
+ return this._parseUsageModel(normalizedGenerationUsage, provider, model, true, rawGenerationUsage);
1040
+ }
1041
+ const llmUsageKeys = ['token_usage', 'usage', 'tokenUsage'];
1042
+ if (response.llmOutput != null) {
1043
+ for (const key of llmUsageKeys) {
1044
+ const llmUsage = response.llmOutput[key];
1045
+ if (!isNonEmptyUsage(llmUsage)) {
1046
+ continue;
1047
+ }
1048
+ return this._parseUsageModel(llmUsage, provider, model, key !== 'usage', llmUsage);
1049
+ }
1050
+ }
1051
+ if (isNonEmptyUsage(normalizedGenerationUsage)) {
1052
+ return this._parseUsageModel(normalizedGenerationUsage, provider, model, true, rawGenerationUsage);
1053
+ }
1054
+ if (isNonEmptyUsage(rawGenerationUsage)) {
1055
+ return this._parseUsageModel(rawGenerationUsage, provider, model, false, rawGenerationUsage);
1056
+ }
1057
+ if (isNonEmptyUsage(fallbackGenerationUsage)) {
1058
+ return this._parseUsageModel(fallbackGenerationUsage, provider, model);
1059
+ }
1060
+ return [0, 0, {}];
1061
+ }
1062
+ }
1063
+
1064
+ const postHogStateSchema = z.object({
1065
+ _posthogRunId: z.string().optional(),
1066
+ _posthogStartTime: z.number().optional(),
1067
+ _posthogInput: z.record(z.string(), z.unknown()).optional()
1068
+ });
1069
+ class LangChainMiddlewareCallbackHandler extends LangChainCallbackHandler {
1070
+ _getParentRunId(_traceId, _runId, parentRunId) {
1071
+ return parentRunId;
1072
+ }
1073
+ }
1074
+ const withoutPostHogState = state => {
1075
+ const {
1076
+ _posthogRunId: _,
1077
+ _posthogStartTime: __,
1078
+ _posthogInput: ___,
1079
+ ...rest
1080
+ } = state;
1081
+ return rest;
1082
+ };
1083
+ const getRunId = state => state._posthogRunId ?? v7();
1084
+ const stringify = value => {
1085
+ try {
1086
+ return JSON.stringify(value) ?? String(value);
1087
+ } catch {
1088
+ try {
1089
+ return String(value);
1090
+ } catch {
1091
+ return '';
1092
+ }
1093
+ }
1094
+ };
1095
+ const toError = error => error instanceof Error ? error : new Error(stringify(error));
1096
+ const safely = callback => {
1097
+ try {
1098
+ callback();
1099
+ } catch {
1100
+ // Telemetry must never affect the LangChain middleware lifecycle.
1101
+ }
1102
+ };
1103
+ const serializeModel = model => {
1104
+ if (model && typeof model === 'object' && 'toJSON' in model && typeof model.toJSON === 'function') {
1105
+ try {
1106
+ return model.toJSON();
1107
+ } catch {
1108
+ // Fall back to a minimal LangChain serialization below.
1109
+ }
1110
+ }
1111
+ return {
1112
+ lc: 1,
1113
+ type: 'constructor',
1114
+ id: ['langchain', 'chat_models', 'unknown'],
1115
+ kwargs: {}
1116
+ };
1117
+ };
1118
+ const toModelOptions = modelSettings => {
1119
+ if (!isObject(modelSettings)) {
1120
+ return {};
1121
+ }
1122
+ try {
1123
+ return {
1124
+ ...modelSettings
1125
+ };
1126
+ } catch {
1127
+ return {};
1128
+ }
1129
+ };
1130
+ const getModelMetadata = (model, modelSettings) => {
1131
+ if (model && typeof model === 'object' && 'getLsParams' in model && typeof model.getLsParams === 'function') {
1132
+ try {
1133
+ return model.getLsParams(toModelOptions(modelSettings));
1134
+ } catch {
1135
+ return undefined;
1136
+ }
1137
+ }
1138
+ return undefined;
1139
+ };
1140
+ const getModelInvocationParams = (model, modelSettings) => {
1141
+ const options = toModelOptions(modelSettings);
1142
+ if (model && typeof model === 'object' && 'invocationParams' in model && typeof model.invocationParams === 'function') {
1143
+ try {
1144
+ const params = model.invocationParams(options);
1145
+ if (isObject(params)) {
1146
+ return {
1147
+ ...options,
1148
+ ...params
1149
+ };
1150
+ }
1151
+ } catch {
1152
+ // Preserve the bind-time settings when the model cannot expose invocation parameters.
1153
+ }
1154
+ }
1155
+ return options;
1156
+ };
1157
+ const normalizeTools = tools => {
1158
+ return tools.map(tool => {
1159
+ try {
1160
+ return convertToOpenAITool(tool);
1161
+ } catch {
1162
+ return tool;
1163
+ }
1164
+ });
1165
+ };
1166
+ const toLLMResult = response => {
1167
+ if (!AIMessage.isInstance(response)) {
1168
+ return {
1169
+ generations: []
1170
+ };
1171
+ }
1172
+ const generation = {
1173
+ text: toContentString(response.content),
1174
+ message: response
1175
+ };
1176
+ return {
1177
+ generations: [[generation]]
1178
+ };
1179
+ };
1180
+
1181
+ /** Options shared with the LangChain callback integration. */
1182
+
1183
+ /**
1184
+ * Creates PostHog AI observability middleware for LangChain v1 agents.
1185
+ *
1186
+ * Use either this middleware or `LangChainCallbackHandler`, not both, to avoid
1187
+ * capturing the same model and tool calls twice.
1188
+ *
1189
+ * LangChain only invokes `afterAgent` for completed runs. A terminal agent
1190
+ * failure still captures the failed model or tool call, but not a root trace.
1191
+ */
1192
+ const createPostHogMiddleware = options => {
1193
+ const {
1194
+ stateSchema,
1195
+ ...callbackOptions
1196
+ } = options;
1197
+ const callback = new LangChainMiddlewareCallbackHandler(callbackOptions);
1198
+ const middlewareStateSchema = stateSchema ? extendInteropZodObject(stateSchema, postHogStateSchema.shape) : postHogStateSchema;
1199
+ return createMiddleware({
1200
+ name: 'PostHogMiddleware',
1201
+ stateSchema: middlewareStateSchema,
1202
+ beforeAgent: state => {
1203
+ return {
1204
+ _posthogRunId: v7(),
1205
+ _posthogStartTime: Date.now(),
1206
+ _posthogInput: withoutPostHogState(state)
1207
+ };
1208
+ },
1209
+ afterAgent: state => {
1210
+ safely(() => {
1211
+ const runId = getRunId(state);
1212
+ callback.handleChainStart({
1213
+ lc: 1,
1214
+ type: 'constructor',
1215
+ id: ['langchain', 'agents', 'PostHogMiddleware'],
1216
+ kwargs: {}
1217
+ }, state._posthogInput ?? withoutPostHogState(state), runId, undefined, undefined, undefined, undefined, 'LangChain Agent', {
1218
+ posthogStartTime: state._posthogStartTime
1219
+ });
1220
+ callback.handleChainEnd(withoutPostHogState(state), runId);
1221
+ });
1222
+ },
1223
+ wrapModelCall: async (request, handler) => {
1224
+ const runId = v7();
1225
+ const parentRunId = getRunId(request.state);
1226
+ const messages = request.systemMessage.text === '' ? request.messages : [request.systemMessage, ...request.messages];
1227
+ const invocationParams = {
1228
+ ...getModelInvocationParams(request.model, request.modelSettings),
1229
+ tools: normalizeTools(request.tools)
1230
+ };
1231
+ safely(() => callback.handleChatModelStart(serializeModel(request.model), [messages], runId, parentRunId, {
1232
+ invocation_params: invocationParams
1233
+ }, undefined, getModelMetadata(request.model, request.modelSettings)));
1234
+ try {
1235
+ const response = await handler(request);
1236
+ safely(() => callback.handleLLMEnd(toLLMResult(response), runId, parentRunId));
1237
+ return response;
1238
+ } catch (error) {
1239
+ safely(() => callback.handleLLMError(toError(error), runId));
1240
+ throw error;
1241
+ }
1242
+ },
1243
+ wrapToolCall: async (request, handler) => {
1244
+ const runId = v7();
1245
+ const parentRunId = getRunId(request.state);
1246
+ const toolName = String(request.tool?.name ?? request.toolCall.name);
1247
+ const serializedTool = {
1248
+ lc: 1,
1249
+ type: 'constructor',
1250
+ id: ['langchain', 'tools', toolName],
1251
+ kwargs: {}
1252
+ };
1253
+ safely(() => callback.handleToolStart(serializedTool, stringify(request.toolCall.args), runId, parentRunId, undefined, undefined, toolName));
1254
+ try {
1255
+ const result = await handler(request);
1256
+ if (ToolMessage.isInstance(result) && result.status === 'error') {
1257
+ safely(() => callback.handleToolError(new Error(toContentString(result.content)), runId, parentRunId));
1258
+ } else {
1259
+ safely(() => callback.handleToolEnd(result, runId, parentRunId));
1260
+ }
1261
+ return result;
1262
+ } catch (error) {
1263
+ safely(() => callback.handleToolError(toError(error), runId));
1264
+ throw error;
1265
+ }
1266
+ }
1267
+ });
1268
+ };
1269
+
1270
+ export { createPostHogMiddleware };
1271
+ //# sourceMappingURL=index.mjs.map