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