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