@posthog/ai 8.6.1 → 8.6.3

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.
@@ -397,7 +397,7 @@ function sanitizeValues(obj) {
397
397
  return jsonSafe;
398
398
  }
399
399
 
400
- var version = "8.6.1";
400
+ var version = "8.6.3";
401
401
 
402
402
  const DEFAULT_MAX_DEPTH = 3;
403
403
  const MAX_STACK_LINES = 20;
@@ -508,120 +508,131 @@ const warnIfPostHogAiGateway = baseURL => {
508
508
  * so callers can re-throw the original error reference safely.
509
509
  */
510
510
  const captureAiGeneration = async (client, options) => {
511
- if (!client.capture) {
512
- return;
513
- }
514
- warnIfPostHogAiGateway(options.baseURL);
515
- const traceId = options.traceId ?? uuid.v4();
516
- const eventType = options.eventType ?? AIEvent.Generation;
517
- const privacyMode = options.privacyMode ?? false;
518
- const usage = options.usage ?? {};
519
- const safeInput = sanitizeValues(options.input);
520
- const safeOutput = sanitizeValues(options.output);
521
- let httpStatus = options.httpStatus;
522
- let errorData = {};
523
- if (options.error) {
524
- if (httpStatus === undefined) {
525
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
526
- httpStatus = options.error.status;
527
- } else {
528
- httpStatus = 500;
529
- }
511
+ try {
512
+ if (!client.capture) {
513
+ return;
530
514
  }
531
- let exceptionId;
532
- if (client.options?.enableExceptionAutocapture) {
533
- exceptionId = core.uuidv7();
534
- client.captureException(options.error, undefined, {
535
- $ai_trace_id: traceId
536
- }, exceptionId);
537
- if (typeof options.error === 'object') {
538
- options.error.__posthog_previously_captured_error = true;
515
+ warnIfPostHogAiGateway(options.baseURL);
516
+ const traceId = options.traceId ?? uuid.v4();
517
+ const eventType = options.eventType ?? AIEvent.Generation;
518
+ const privacyMode = options.privacyMode ?? false;
519
+ const usage = options.usage ?? {};
520
+
521
+ // Check privacy before reading or traversing input/output. Besides avoiding
522
+ // needless work, this ensures hostile getters/proxies cannot observe a value
523
+ // that the caller explicitly requested us to redact.
524
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
525
+ const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
526
+ const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
527
+ let httpStatus = options.httpStatus;
528
+ let errorData = {};
529
+ if (options.error) {
530
+ if (httpStatus === undefined) {
531
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
532
+ httpStatus = options.error.status;
533
+ } else {
534
+ httpStatus = 500;
535
+ }
539
536
  }
537
+ let exceptionId;
538
+ if (client.options?.enableExceptionAutocapture) {
539
+ exceptionId = core.uuidv7();
540
+ client.captureException(options.error, undefined, {
541
+ $ai_trace_id: traceId
542
+ }, exceptionId);
543
+ if (typeof options.error === 'object') {
544
+ ;
545
+ options.error.__posthog_previously_captured_error = true;
546
+ }
547
+ }
548
+ errorData = {
549
+ $ai_is_error: true,
550
+ $ai_error: stringifyError(options.error),
551
+ $exception_event_id: exceptionId
552
+ };
540
553
  }
541
- errorData = {
542
- $ai_is_error: true,
543
- $ai_error: stringifyError(options.error),
544
- $exception_event_id: exceptionId
554
+ httpStatus = httpStatus ?? 200;
555
+ let costOverrideData = {};
556
+ if (options.costOverride) {
557
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
558
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
559
+ costOverrideData = {
560
+ $ai_input_cost_usd: inputCostUSD,
561
+ $ai_output_cost_usd: outputCostUSD,
562
+ $ai_total_cost_usd: inputCostUSD + outputCostUSD
563
+ };
564
+ }
565
+ const additionalTokenValues = {
566
+ ...(usage.reasoningTokens ? {
567
+ $ai_reasoning_tokens: usage.reasoningTokens
568
+ } : {}),
569
+ ...(usage.cacheReadInputTokens ? {
570
+ $ai_cache_read_input_tokens: usage.cacheReadInputTokens
571
+ } : {}),
572
+ ...(usage.cacheCreationInputTokens ? {
573
+ $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
574
+ } : {}),
575
+ ...(usage.webSearchCount ? {
576
+ $ai_web_search_count: usage.webSearchCount
577
+ } : {}),
578
+ ...(usage.rawUsage ? {
579
+ $ai_usage: usage.rawUsage
580
+ } : {})
545
581
  };
546
- }
547
- httpStatus = httpStatus ?? 200;
548
- let costOverrideData = {};
549
- if (options.costOverride) {
550
- const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
551
- const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
552
- costOverrideData = {
553
- $ai_input_cost_usd: inputCostUSD,
554
- $ai_output_cost_usd: outputCostUSD,
555
- $ai_total_cost_usd: inputCostUSD + outputCostUSD
582
+ const properties = {
583
+ $ai_lib: 'posthog-ai',
584
+ $ai_lib_version: version,
585
+ $ai_provider: options.providerOverride ?? options.provider,
586
+ $ai_model: options.modelOverride ?? options.model,
587
+ $ai_model_parameters: options.modelParameters ?? {},
588
+ $ai_input: safeInput,
589
+ $ai_output_choices: safeOutput,
590
+ $ai_http_status: httpStatus,
591
+ $ai_input_tokens: usage.inputTokens ?? 0,
592
+ ...(usage.outputTokens !== undefined ? {
593
+ $ai_output_tokens: usage.outputTokens
594
+ } : {}),
595
+ ...additionalTokenValues,
596
+ $ai_latency: options.latency ?? 0,
597
+ ...(options.timeToFirstToken !== undefined ? {
598
+ $ai_time_to_first_token: options.timeToFirstToken
599
+ } : {}),
600
+ $ai_trace_id: traceId,
601
+ $ai_base_url: options.baseURL ?? '',
602
+ ...options.properties,
603
+ $ai_tokens_source: getTokensSource(options.properties),
604
+ ...(options.distinctId ? {} : {
605
+ $process_person_profile: false
606
+ }),
607
+ ...(options.stopReason ? {
608
+ $ai_stop_reason: options.stopReason
609
+ } : {}),
610
+ ...(options.tools ? {
611
+ $ai_tools: options.tools
612
+ } : {}),
613
+ ...(options.completionId ? {
614
+ $ai_completion_id: options.completionId
615
+ } : {}),
616
+ ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
617
+ $ai_provider_metadata: options.providerMetadata
618
+ } : {}),
619
+ ...errorData,
620
+ ...costOverrideData
556
621
  };
557
- }
558
- const additionalTokenValues = {
559
- ...(usage.reasoningTokens ? {
560
- $ai_reasoning_tokens: usage.reasoningTokens
561
- } : {}),
562
- ...(usage.cacheReadInputTokens ? {
563
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
564
- } : {}),
565
- ...(usage.cacheCreationInputTokens ? {
566
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
567
- } : {}),
568
- ...(usage.webSearchCount ? {
569
- $ai_web_search_count: usage.webSearchCount
570
- } : {}),
571
- ...(usage.rawUsage ? {
572
- $ai_usage: usage.rawUsage
573
- } : {})
574
- };
575
- const properties = {
576
- $ai_lib: 'posthog-ai',
577
- $ai_lib_version: version,
578
- $ai_provider: options.providerOverride ?? options.provider,
579
- $ai_model: options.modelOverride ?? options.model,
580
- $ai_model_parameters: options.modelParameters ?? {},
581
- $ai_input: withPrivacyMode(client, privacyMode, safeInput),
582
- $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
583
- $ai_http_status: httpStatus,
584
- $ai_input_tokens: usage.inputTokens ?? 0,
585
- ...(usage.outputTokens !== undefined ? {
586
- $ai_output_tokens: usage.outputTokens
587
- } : {}),
588
- ...additionalTokenValues,
589
- $ai_latency: options.latency ?? 0,
590
- ...(options.timeToFirstToken !== undefined ? {
591
- $ai_time_to_first_token: options.timeToFirstToken
592
- } : {}),
593
- $ai_trace_id: traceId,
594
- $ai_base_url: options.baseURL ?? '',
595
- ...options.properties,
596
- $ai_tokens_source: getTokensSource(options.properties),
597
- ...(options.distinctId ? {} : {
598
- $process_person_profile: false
599
- }),
600
- ...(options.stopReason ? {
601
- $ai_stop_reason: options.stopReason
602
- } : {}),
603
- ...(options.tools ? {
604
- $ai_tools: options.tools
605
- } : {}),
606
- ...(options.completionId ? {
607
- $ai_completion_id: options.completionId
608
- } : {}),
609
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
610
- $ai_provider_metadata: options.providerMetadata
611
- } : {}),
612
- ...errorData,
613
- ...costOverrideData
614
- };
615
- const event = {
616
- distinctId: options.distinctId ?? traceId,
617
- event: eventType,
618
- properties,
619
- groups: options.groups
620
- };
621
- if (options.captureImmediate) {
622
- await client.captureImmediate(event);
623
- } else {
624
- client.capture(event);
622
+ const event = {
623
+ distinctId: options.distinctId ?? traceId,
624
+ event: eventType,
625
+ properties,
626
+ groups: options.groups
627
+ };
628
+ if (options.captureImmediate) {
629
+ await client.captureImmediate(event);
630
+ } else {
631
+ client.capture(event);
632
+ }
633
+ } catch (error) {
634
+ // Telemetry failures must never affect the instrumented provider call.
635
+ console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
625
636
  }
626
637
  };
627
638
 
@@ -1121,86 +1132,97 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1121
1132
 
1122
1133
  // Map to track in-progress tool calls
1123
1134
  const toolCallsInProgress = new Map();
1135
+ const captureStreamGeneration = async captureOptions => {
1136
+ try {
1137
+ await captureAiGeneration(phClient, captureOptions);
1138
+ } catch (error) {
1139
+ // Telemetry must never change the provider stream's behavior.
1140
+ console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1141
+ }
1142
+ };
1124
1143
  try {
1125
1144
  const {
1126
1145
  stream,
1127
1146
  ...rest
1128
1147
  } = await model.doStream(params);
1129
- const transformStream = new TransformStream({
1130
- transform(chunk, controller) {
1131
- // Handle streaming patterns - compatible with both V2 and V3
1132
- if (chunk.type === 'text-delta') {
1133
- if (firstTokenTime === undefined) {
1134
- firstTokenTime = Date.now();
1135
- }
1136
- generatedText += chunk.delta;
1148
+ const reader = stream.getReader();
1149
+ let inBandError;
1150
+ let hasInBandError = false;
1151
+ let finalizationPromise;
1152
+ const observeChunk = chunk => {
1153
+ // Handle streaming patterns - compatible with both V2 and V3
1154
+ if (chunk.type === 'text-delta') {
1155
+ if (firstTokenTime === undefined) {
1156
+ firstTokenTime = Date.now();
1137
1157
  }
1138
- if (chunk.type === 'reasoning-delta') {
1139
- if (firstTokenTime === undefined) {
1140
- firstTokenTime = Date.now();
1141
- }
1142
- reasoningText += chunk.delta;
1158
+ generatedText += chunk.delta;
1159
+ }
1160
+ if (chunk.type === 'reasoning-delta') {
1161
+ if (firstTokenTime === undefined) {
1162
+ firstTokenTime = Date.now();
1143
1163
  }
1164
+ reasoningText += chunk.delta;
1165
+ }
1144
1166
 
1145
- // Handle tool call chunks
1146
- if (chunk.type === 'tool-input-start') {
1147
- if (firstTokenTime === undefined) {
1148
- firstTokenTime = Date.now();
1149
- }
1150
- // Initialize a new tool call
1151
- toolCallsInProgress.set(chunk.id, {
1152
- toolCallId: chunk.id,
1153
- toolName: chunk.toolName,
1154
- input: ''
1155
- });
1156
- }
1157
- if (chunk.type === 'tool-input-delta') {
1158
- // Accumulate tool call arguments
1159
- const toolCall = toolCallsInProgress.get(chunk.id);
1160
- if (toolCall) {
1161
- toolCall.input += chunk.delta;
1162
- }
1167
+ // Handle tool call chunks
1168
+ if (chunk.type === 'tool-input-start') {
1169
+ if (firstTokenTime === undefined) {
1170
+ firstTokenTime = Date.now();
1163
1171
  }
1164
- if (chunk.type === 'tool-input-end') {
1165
- // Tool call is complete, keep it in the map for final processing
1172
+ toolCallsInProgress.set(chunk.id, {
1173
+ toolCallId: chunk.id,
1174
+ toolName: chunk.toolName,
1175
+ input: ''
1176
+ });
1177
+ }
1178
+ if (chunk.type === 'tool-input-delta') {
1179
+ const toolCall = toolCallsInProgress.get(chunk.id);
1180
+ if (toolCall) {
1181
+ toolCall.input += chunk.delta;
1166
1182
  }
1167
- if (chunk.type === 'tool-call') {
1168
- if (firstTokenTime === undefined) {
1169
- firstTokenTime = Date.now();
1170
- }
1171
- // Direct tool call chunk (complete tool call)
1172
- toolCallsInProgress.set(chunk.toolCallId, {
1173
- toolCallId: chunk.toolCallId,
1174
- toolName: chunk.toolName,
1175
- input: chunk.input
1176
- });
1183
+ }
1184
+ if (chunk.type === 'tool-call') {
1185
+ if (firstTokenTime === undefined) {
1186
+ firstTokenTime = Date.now();
1177
1187
  }
1178
- if (chunk.type === 'finish') {
1179
- providerMetadata = chunk.providerMetadata;
1180
- const chunkUsage = chunk.usage || {};
1181
- const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
1182
- usage = {
1183
- inputTokens: extractTokenCount(chunk.usage?.inputTokens),
1184
- outputTokens: extractTokenCount(chunk.usage?.outputTokens),
1185
- reasoningTokens: extractReasoningTokens(chunkUsage),
1186
- cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
1187
- ...additionalTokenValues
1188
- };
1188
+ toolCallsInProgress.set(chunk.toolCallId, {
1189
+ toolCallId: chunk.toolCallId,
1190
+ toolName: chunk.toolName,
1191
+ input: chunk.input
1192
+ });
1193
+ }
1194
+ if (chunk.type === 'error') {
1195
+ hasInBandError = true;
1196
+ inBandError = chunk.error;
1197
+ }
1198
+ if (chunk.type === 'finish') {
1199
+ providerMetadata = chunk.providerMetadata;
1200
+ const chunkUsage = chunk.usage || {};
1201
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
1202
+ usage = {
1203
+ inputTokens: extractTokenCount(chunk.usage?.inputTokens),
1204
+ outputTokens: extractTokenCount(chunk.usage?.outputTokens),
1205
+ reasoningTokens: extractReasoningTokens(chunkUsage),
1206
+ cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
1207
+ ...additionalTokenValues
1208
+ };
1189
1209
 
1190
- // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1191
- const rawFinishReason = chunk.finishReason;
1192
- if (typeof rawFinishReason === 'string') {
1193
- stopReason = rawFinishReason;
1194
- } else if (rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason) {
1195
- stopReason = String(rawFinishReason.unified);
1196
- }
1210
+ // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1211
+ const rawFinishReason = chunk.finishReason;
1212
+ if (typeof rawFinishReason === 'string') {
1213
+ stopReason = rawFinishReason;
1214
+ } else if (rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason) {
1215
+ stopReason = String(rawFinishReason.unified);
1197
1216
  }
1198
- controller.enqueue(chunk);
1199
- },
1200
- flush: async () => {
1217
+ }
1218
+ };
1219
+ const finalize = (terminalError, isError = false) => {
1220
+ if (finalizationPromise) {
1221
+ return finalizationPromise;
1222
+ }
1223
+ finalizationPromise = (async () => {
1201
1224
  const latency = (Date.now() - startTime) / 1000;
1202
1225
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1203
- // Build content array similar to mapVercelOutput structure
1204
1226
  const content = [];
1205
1227
  if (reasoningText) {
1206
1228
  content.push({
@@ -1214,8 +1236,6 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1214
1236
  text: truncate(generatedText)
1215
1237
  });
1216
1238
  }
1217
-
1218
- // Add completed tool calls to content
1219
1239
  for (const toolCall of toolCallsInProgress.values()) {
1220
1240
  if (toolCall.toolName) {
1221
1241
  content.push({
@@ -1228,15 +1248,11 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1228
1248
  });
1229
1249
  }
1230
1250
  }
1231
-
1232
- // Structure output like mapVercelOutput does
1233
1251
  const output = content.length > 0 ? [{
1234
1252
  role: 'assistant',
1235
1253
  content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
1236
1254
  }] : [];
1237
1255
  const webSearchCount = extractWebSearchCount(providerMetadata, usage);
1238
-
1239
- // Update usage with web search count and raw metadata
1240
1256
  const finalUsage = {
1241
1257
  ...usage,
1242
1258
  webSearchCount,
@@ -1246,29 +1262,65 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1246
1262
  }
1247
1263
  };
1248
1264
  adjustAnthropicV3CacheTokens(model, modelId, provider, finalUsage);
1249
- await captureAiGeneration(phClient, {
1265
+ const finishError = stopReason === 'error' ? new Error('Vercel AI SDK stream finished with an error') : undefined;
1266
+ const error = isError ? terminalError ?? new Error('Vercel AI SDK stream failed') : hasInBandError ? inBandError ?? new Error('Vercel AI SDK stream emitted an error chunk') : finishError;
1267
+ await captureStreamGeneration({
1250
1268
  ...baseOptions,
1251
1269
  model: modelId,
1252
1270
  provider: provider,
1253
1271
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1254
- output: output,
1272
+ output,
1255
1273
  latency,
1256
1274
  timeToFirstToken,
1257
1275
  baseURL,
1258
1276
  modelParameters: getModelParams(mergedParams),
1259
- httpStatus: 200,
1277
+ httpStatus: error ? undefined : 200,
1260
1278
  usage: finalUsage,
1261
1279
  stopReason,
1280
+ error,
1262
1281
  tools: availableTools
1263
1282
  });
1283
+ })().catch(error => {
1284
+ // Building telemetry must not change the provider stream's behavior.
1285
+ console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1286
+ });
1287
+ return finalizationPromise;
1288
+ };
1289
+ const instrumentedStream = new ReadableStream({
1290
+ async pull(controller) {
1291
+ let result;
1292
+ try {
1293
+ result = await reader.read();
1294
+ } catch (error) {
1295
+ void finalize(error, true);
1296
+ controller.error(error);
1297
+ return;
1298
+ }
1299
+ if (result.done) {
1300
+ controller.close();
1301
+ void finalize();
1302
+ return;
1303
+ }
1304
+ try {
1305
+ observeChunk(result.value);
1306
+ } catch {
1307
+ // Instrumentation must not alter or suppress provider chunks.
1308
+ }
1309
+ controller.enqueue(result.value);
1310
+ },
1311
+ cancel(reason) {
1312
+ void finalize(reason ?? new Error('Vercel AI SDK stream was cancelled'), true);
1313
+ return reader.cancel(reason);
1264
1314
  }
1315
+ }, {
1316
+ highWaterMark: 0
1265
1317
  });
1266
1318
  return {
1267
- stream: stream.pipeThrough(transformStream),
1319
+ stream: instrumentedStream,
1268
1320
  ...rest
1269
1321
  };
1270
1322
  } catch (error) {
1271
- await captureAiGeneration(phClient, {
1323
+ await captureStreamGeneration({
1272
1324
  ...baseOptions,
1273
1325
  model: modelId,
1274
1326
  provider: provider,