@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.
package/dist/index.cjs CHANGED
@@ -380,7 +380,7 @@ function sanitizeValues(obj) {
380
380
  return jsonSafe;
381
381
  }
382
382
 
383
- var version = "8.6.1";
383
+ var version = "8.6.3";
384
384
 
385
385
  const DEFAULT_MAX_DEPTH = 3;
386
386
  const MAX_STACK_LINES = 20;
@@ -482,120 +482,130 @@ const warnIfPostHogAiGateway = baseURL => {
482
482
  * so callers can re-throw the original error reference safely.
483
483
  */
484
484
  const captureAiGeneration = async (client, options) => {
485
- if (!client.capture) {
486
- return;
487
- }
488
- warnIfPostHogAiGateway(options.baseURL);
489
- const traceId = options.traceId ?? uuid.v4();
490
- const eventType = options.eventType ?? exports.AIEvent.Generation;
491
- const privacyMode = options.privacyMode ?? false;
492
- const usage = options.usage ?? {};
493
- const safeInput = sanitizeValues(options.input);
494
- const safeOutput = sanitizeValues(options.output);
495
- let httpStatus = options.httpStatus;
496
- let errorData = {};
497
- if (options.error) {
498
- if (httpStatus === undefined) {
499
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
500
- httpStatus = options.error.status;
501
- } else {
502
- httpStatus = 500;
503
- }
485
+ try {
486
+ if (!client.capture) {
487
+ return;
504
488
  }
505
- let exceptionId;
506
- if (client.options?.enableExceptionAutocapture) {
507
- exceptionId = core.uuidv7();
508
- client.captureException(options.error, undefined, {
509
- $ai_trace_id: traceId
510
- }, exceptionId);
511
- if (typeof options.error === 'object') {
512
- options.error.__posthog_previously_captured_error = true;
489
+ warnIfPostHogAiGateway(options.baseURL);
490
+ const traceId = options.traceId ?? uuid.v4();
491
+ const eventType = options.eventType ?? exports.AIEvent.Generation;
492
+ const privacyMode = options.privacyMode ?? false;
493
+ const usage = options.usage ?? {};
494
+ // Check privacy before reading or traversing input/output. Besides avoiding
495
+ // needless work, this ensures hostile getters/proxies cannot observe a value
496
+ // that the caller explicitly requested us to redact.
497
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
498
+ const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
499
+ const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
500
+ let httpStatus = options.httpStatus;
501
+ let errorData = {};
502
+ if (options.error) {
503
+ if (httpStatus === undefined) {
504
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
505
+ httpStatus = options.error.status;
506
+ } else {
507
+ httpStatus = 500;
508
+ }
513
509
  }
510
+ let exceptionId;
511
+ if (client.options?.enableExceptionAutocapture) {
512
+ exceptionId = core.uuidv7();
513
+ client.captureException(options.error, undefined, {
514
+ $ai_trace_id: traceId
515
+ }, exceptionId);
516
+ if (typeof options.error === 'object') {
517
+ ;
518
+ options.error.__posthog_previously_captured_error = true;
519
+ }
520
+ }
521
+ errorData = {
522
+ $ai_is_error: true,
523
+ $ai_error: stringifyError(options.error),
524
+ $exception_event_id: exceptionId
525
+ };
514
526
  }
515
- errorData = {
516
- $ai_is_error: true,
517
- $ai_error: stringifyError(options.error),
518
- $exception_event_id: exceptionId
527
+ httpStatus = httpStatus ?? 200;
528
+ let costOverrideData = {};
529
+ if (options.costOverride) {
530
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
531
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
532
+ costOverrideData = {
533
+ $ai_input_cost_usd: inputCostUSD,
534
+ $ai_output_cost_usd: outputCostUSD,
535
+ $ai_total_cost_usd: inputCostUSD + outputCostUSD
536
+ };
537
+ }
538
+ const additionalTokenValues = {
539
+ ...(usage.reasoningTokens ? {
540
+ $ai_reasoning_tokens: usage.reasoningTokens
541
+ } : {}),
542
+ ...(usage.cacheReadInputTokens ? {
543
+ $ai_cache_read_input_tokens: usage.cacheReadInputTokens
544
+ } : {}),
545
+ ...(usage.cacheCreationInputTokens ? {
546
+ $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
547
+ } : {}),
548
+ ...(usage.webSearchCount ? {
549
+ $ai_web_search_count: usage.webSearchCount
550
+ } : {}),
551
+ ...(usage.rawUsage ? {
552
+ $ai_usage: usage.rawUsage
553
+ } : {})
519
554
  };
520
- }
521
- httpStatus = httpStatus ?? 200;
522
- let costOverrideData = {};
523
- if (options.costOverride) {
524
- const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
525
- const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
526
- costOverrideData = {
527
- $ai_input_cost_usd: inputCostUSD,
528
- $ai_output_cost_usd: outputCostUSD,
529
- $ai_total_cost_usd: inputCostUSD + outputCostUSD
555
+ const properties = {
556
+ $ai_lib: 'posthog-ai',
557
+ $ai_lib_version: version,
558
+ $ai_provider: options.providerOverride ?? options.provider,
559
+ $ai_model: options.modelOverride ?? options.model,
560
+ $ai_model_parameters: options.modelParameters ?? {},
561
+ $ai_input: safeInput,
562
+ $ai_output_choices: safeOutput,
563
+ $ai_http_status: httpStatus,
564
+ $ai_input_tokens: usage.inputTokens ?? 0,
565
+ ...(usage.outputTokens !== undefined ? {
566
+ $ai_output_tokens: usage.outputTokens
567
+ } : {}),
568
+ ...additionalTokenValues,
569
+ $ai_latency: options.latency ?? 0,
570
+ ...(options.timeToFirstToken !== undefined ? {
571
+ $ai_time_to_first_token: options.timeToFirstToken
572
+ } : {}),
573
+ $ai_trace_id: traceId,
574
+ $ai_base_url: options.baseURL ?? '',
575
+ ...options.properties,
576
+ $ai_tokens_source: getTokensSource(options.properties),
577
+ ...(options.distinctId ? {} : {
578
+ $process_person_profile: false
579
+ }),
580
+ ...(options.stopReason ? {
581
+ $ai_stop_reason: options.stopReason
582
+ } : {}),
583
+ ...(options.tools ? {
584
+ $ai_tools: options.tools
585
+ } : {}),
586
+ ...(options.completionId ? {
587
+ $ai_completion_id: options.completionId
588
+ } : {}),
589
+ ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
590
+ $ai_provider_metadata: options.providerMetadata
591
+ } : {}),
592
+ ...errorData,
593
+ ...costOverrideData
530
594
  };
531
- }
532
- const additionalTokenValues = {
533
- ...(usage.reasoningTokens ? {
534
- $ai_reasoning_tokens: usage.reasoningTokens
535
- } : {}),
536
- ...(usage.cacheReadInputTokens ? {
537
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
538
- } : {}),
539
- ...(usage.cacheCreationInputTokens ? {
540
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
541
- } : {}),
542
- ...(usage.webSearchCount ? {
543
- $ai_web_search_count: usage.webSearchCount
544
- } : {}),
545
- ...(usage.rawUsage ? {
546
- $ai_usage: usage.rawUsage
547
- } : {})
548
- };
549
- const properties = {
550
- $ai_lib: 'posthog-ai',
551
- $ai_lib_version: version,
552
- $ai_provider: options.providerOverride ?? options.provider,
553
- $ai_model: options.modelOverride ?? options.model,
554
- $ai_model_parameters: options.modelParameters ?? {},
555
- $ai_input: withPrivacyMode(client, privacyMode, safeInput),
556
- $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
557
- $ai_http_status: httpStatus,
558
- $ai_input_tokens: usage.inputTokens ?? 0,
559
- ...(usage.outputTokens !== undefined ? {
560
- $ai_output_tokens: usage.outputTokens
561
- } : {}),
562
- ...additionalTokenValues,
563
- $ai_latency: options.latency ?? 0,
564
- ...(options.timeToFirstToken !== undefined ? {
565
- $ai_time_to_first_token: options.timeToFirstToken
566
- } : {}),
567
- $ai_trace_id: traceId,
568
- $ai_base_url: options.baseURL ?? '',
569
- ...options.properties,
570
- $ai_tokens_source: getTokensSource(options.properties),
571
- ...(options.distinctId ? {} : {
572
- $process_person_profile: false
573
- }),
574
- ...(options.stopReason ? {
575
- $ai_stop_reason: options.stopReason
576
- } : {}),
577
- ...(options.tools ? {
578
- $ai_tools: options.tools
579
- } : {}),
580
- ...(options.completionId ? {
581
- $ai_completion_id: options.completionId
582
- } : {}),
583
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
584
- $ai_provider_metadata: options.providerMetadata
585
- } : {}),
586
- ...errorData,
587
- ...costOverrideData
588
- };
589
- const event = {
590
- distinctId: options.distinctId ?? traceId,
591
- event: eventType,
592
- properties,
593
- groups: options.groups
594
- };
595
- if (options.captureImmediate) {
596
- await client.captureImmediate(event);
597
- } else {
598
- client.capture(event);
595
+ const event = {
596
+ distinctId: options.distinctId ?? traceId,
597
+ event: eventType,
598
+ properties,
599
+ groups: options.groups
600
+ };
601
+ if (options.captureImmediate) {
602
+ await client.captureImmediate(event);
603
+ } else {
604
+ client.capture(event);
605
+ }
606
+ } catch (error) {
607
+ // Telemetry failures must never affect the instrumented provider call.
608
+ console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
599
609
  }
600
610
  };
601
611
 
@@ -1070,84 +1080,95 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1070
1080
  const baseURL = extractBaseURL(model);
1071
1081
  // Map to track in-progress tool calls
1072
1082
  const toolCallsInProgress = new Map();
1083
+ const captureStreamGeneration = async captureOptions => {
1084
+ try {
1085
+ await captureAiGeneration(phClient, captureOptions);
1086
+ } catch (error) {
1087
+ // Telemetry must never change the provider stream's behavior.
1088
+ console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1089
+ }
1090
+ };
1073
1091
  try {
1074
1092
  const {
1075
1093
  stream,
1076
1094
  ...rest
1077
1095
  } = await model.doStream(params);
1078
- const transformStream = new TransformStream({
1079
- transform(chunk, controller) {
1080
- // Handle streaming patterns - compatible with both V2 and V3
1081
- if (chunk.type === 'text-delta') {
1082
- if (firstTokenTime === undefined) {
1083
- firstTokenTime = Date.now();
1084
- }
1085
- generatedText += chunk.delta;
1086
- }
1087
- if (chunk.type === 'reasoning-delta') {
1088
- if (firstTokenTime === undefined) {
1089
- firstTokenTime = Date.now();
1090
- }
1091
- reasoningText += chunk.delta;
1096
+ const reader = stream.getReader();
1097
+ let inBandError;
1098
+ let hasInBandError = false;
1099
+ let finalizationPromise;
1100
+ const observeChunk = chunk => {
1101
+ // Handle streaming patterns - compatible with both V2 and V3
1102
+ if (chunk.type === 'text-delta') {
1103
+ if (firstTokenTime === undefined) {
1104
+ firstTokenTime = Date.now();
1092
1105
  }
1093
- // Handle tool call chunks
1094
- if (chunk.type === 'tool-input-start') {
1095
- if (firstTokenTime === undefined) {
1096
- firstTokenTime = Date.now();
1097
- }
1098
- // Initialize a new tool call
1099
- toolCallsInProgress.set(chunk.id, {
1100
- toolCallId: chunk.id,
1101
- toolName: chunk.toolName,
1102
- input: ''
1103
- });
1106
+ generatedText += chunk.delta;
1107
+ }
1108
+ if (chunk.type === 'reasoning-delta') {
1109
+ if (firstTokenTime === undefined) {
1110
+ firstTokenTime = Date.now();
1104
1111
  }
1105
- if (chunk.type === 'tool-input-delta') {
1106
- // Accumulate tool call arguments
1107
- const toolCall = toolCallsInProgress.get(chunk.id);
1108
- if (toolCall) {
1109
- toolCall.input += chunk.delta;
1110
- }
1112
+ reasoningText += chunk.delta;
1113
+ }
1114
+ // Handle tool call chunks
1115
+ if (chunk.type === 'tool-input-start') {
1116
+ if (firstTokenTime === undefined) {
1117
+ firstTokenTime = Date.now();
1111
1118
  }
1112
- if (chunk.type === 'tool-input-end') {
1113
- // Tool call is complete, keep it in the map for final processing
1119
+ toolCallsInProgress.set(chunk.id, {
1120
+ toolCallId: chunk.id,
1121
+ toolName: chunk.toolName,
1122
+ input: ''
1123
+ });
1124
+ }
1125
+ if (chunk.type === 'tool-input-delta') {
1126
+ const toolCall = toolCallsInProgress.get(chunk.id);
1127
+ if (toolCall) {
1128
+ toolCall.input += chunk.delta;
1114
1129
  }
1115
- if (chunk.type === 'tool-call') {
1116
- if (firstTokenTime === undefined) {
1117
- firstTokenTime = Date.now();
1118
- }
1119
- // Direct tool call chunk (complete tool call)
1120
- toolCallsInProgress.set(chunk.toolCallId, {
1121
- toolCallId: chunk.toolCallId,
1122
- toolName: chunk.toolName,
1123
- input: chunk.input
1124
- });
1130
+ }
1131
+ if (chunk.type === 'tool-call') {
1132
+ if (firstTokenTime === undefined) {
1133
+ firstTokenTime = Date.now();
1125
1134
  }
1126
- if (chunk.type === 'finish') {
1127
- providerMetadata = chunk.providerMetadata;
1128
- const chunkUsage = chunk.usage || {};
1129
- const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
1130
- usage = {
1131
- inputTokens: extractTokenCount(chunk.usage?.inputTokens),
1132
- outputTokens: extractTokenCount(chunk.usage?.outputTokens),
1133
- reasoningTokens: extractReasoningTokens(chunkUsage),
1134
- cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
1135
- ...additionalTokenValues
1136
- };
1137
- // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1138
- const rawFinishReason = chunk.finishReason;
1139
- if (typeof rawFinishReason === 'string') {
1140
- stopReason = rawFinishReason;
1141
- } else if (rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason) {
1142
- stopReason = String(rawFinishReason.unified);
1143
- }
1135
+ toolCallsInProgress.set(chunk.toolCallId, {
1136
+ toolCallId: chunk.toolCallId,
1137
+ toolName: chunk.toolName,
1138
+ input: chunk.input
1139
+ });
1140
+ }
1141
+ if (chunk.type === 'error') {
1142
+ hasInBandError = true;
1143
+ inBandError = chunk.error;
1144
+ }
1145
+ if (chunk.type === 'finish') {
1146
+ providerMetadata = chunk.providerMetadata;
1147
+ const chunkUsage = chunk.usage || {};
1148
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
1149
+ usage = {
1150
+ inputTokens: extractTokenCount(chunk.usage?.inputTokens),
1151
+ outputTokens: extractTokenCount(chunk.usage?.outputTokens),
1152
+ reasoningTokens: extractReasoningTokens(chunkUsage),
1153
+ cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
1154
+ ...additionalTokenValues
1155
+ };
1156
+ // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1157
+ const rawFinishReason = chunk.finishReason;
1158
+ if (typeof rawFinishReason === 'string') {
1159
+ stopReason = rawFinishReason;
1160
+ } else if (rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason) {
1161
+ stopReason = String(rawFinishReason.unified);
1144
1162
  }
1145
- controller.enqueue(chunk);
1146
- },
1147
- flush: async () => {
1163
+ }
1164
+ };
1165
+ const finalize = (terminalError, isError = false) => {
1166
+ if (finalizationPromise) {
1167
+ return finalizationPromise;
1168
+ }
1169
+ finalizationPromise = (async () => {
1148
1170
  const latency = (Date.now() - startTime) / 1000;
1149
1171
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1150
- // Build content array similar to mapVercelOutput structure
1151
1172
  const content = [];
1152
1173
  if (reasoningText) {
1153
1174
  content.push({
@@ -1161,7 +1182,6 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1161
1182
  text: truncate(generatedText)
1162
1183
  });
1163
1184
  }
1164
- // Add completed tool calls to content
1165
1185
  for (const toolCall of toolCallsInProgress.values()) {
1166
1186
  if (toolCall.toolName) {
1167
1187
  content.push({
@@ -1174,13 +1194,11 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1174
1194
  });
1175
1195
  }
1176
1196
  }
1177
- // Structure output like mapVercelOutput does
1178
1197
  const output = content.length > 0 ? [{
1179
1198
  role: 'assistant',
1180
1199
  content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
1181
1200
  }] : [];
1182
1201
  const webSearchCount = extractWebSearchCount(providerMetadata, usage);
1183
- // Update usage with web search count and raw metadata
1184
1202
  const finalUsage = {
1185
1203
  ...usage,
1186
1204
  webSearchCount,
@@ -1190,29 +1208,65 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1190
1208
  }
1191
1209
  };
1192
1210
  adjustAnthropicV3CacheTokens(model, modelId, provider, finalUsage);
1193
- await captureAiGeneration(phClient, {
1211
+ const finishError = stopReason === 'error' ? new Error('Vercel AI SDK stream finished with an error') : undefined;
1212
+ const error = isError ? terminalError ?? new Error('Vercel AI SDK stream failed') : hasInBandError ? inBandError ?? new Error('Vercel AI SDK stream emitted an error chunk') : finishError;
1213
+ await captureStreamGeneration({
1194
1214
  ...baseOptions,
1195
1215
  model: modelId,
1196
1216
  provider: provider,
1197
1217
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1198
- output: output,
1218
+ output,
1199
1219
  latency,
1200
1220
  timeToFirstToken,
1201
1221
  baseURL,
1202
1222
  modelParameters: getModelParams(mergedParams),
1203
- httpStatus: 200,
1223
+ httpStatus: error ? undefined : 200,
1204
1224
  usage: finalUsage,
1205
1225
  stopReason,
1226
+ error,
1206
1227
  tools: availableTools
1207
1228
  });
1229
+ })().catch(error => {
1230
+ // Building telemetry must not change the provider stream's behavior.
1231
+ console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1232
+ });
1233
+ return finalizationPromise;
1234
+ };
1235
+ const instrumentedStream = new ReadableStream({
1236
+ async pull(controller) {
1237
+ let result;
1238
+ try {
1239
+ result = await reader.read();
1240
+ } catch (error) {
1241
+ void finalize(error, true);
1242
+ controller.error(error);
1243
+ return;
1244
+ }
1245
+ if (result.done) {
1246
+ controller.close();
1247
+ void finalize();
1248
+ return;
1249
+ }
1250
+ try {
1251
+ observeChunk(result.value);
1252
+ } catch {
1253
+ // Instrumentation must not alter or suppress provider chunks.
1254
+ }
1255
+ controller.enqueue(result.value);
1256
+ },
1257
+ cancel(reason) {
1258
+ void finalize(reason ?? new Error('Vercel AI SDK stream was cancelled'), true);
1259
+ return reader.cancel(reason);
1208
1260
  }
1261
+ }, {
1262
+ highWaterMark: 0
1209
1263
  });
1210
1264
  return {
1211
- stream: stream.pipeThrough(transformStream),
1265
+ stream: instrumentedStream,
1212
1266
  ...rest
1213
1267
  };
1214
1268
  } catch (error) {
1215
- await captureAiGeneration(phClient, {
1269
+ await captureStreamGeneration({
1216
1270
  ...baseOptions,
1217
1271
  model: modelId,
1218
1272
  provider: provider,