@posthog/ai 8.6.1 → 8.6.2

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.2";
401
401
 
402
402
  const DEFAULT_MAX_DEPTH = 3;
403
403
  const MAX_STACK_LINES = 20;
@@ -516,8 +516,13 @@ const captureAiGeneration = async (client, options) => {
516
516
  const eventType = options.eventType ?? AIEvent.Generation;
517
517
  const privacyMode = options.privacyMode ?? false;
518
518
  const usage = options.usage ?? {};
519
- const safeInput = sanitizeValues(options.input);
520
- const safeOutput = sanitizeValues(options.output);
519
+
520
+ // Check privacy before reading or traversing input/output. Besides avoiding
521
+ // needless work, this ensures hostile getters/proxies cannot observe a value
522
+ // that the caller explicitly requested us to redact.
523
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
524
+ const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
525
+ const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
521
526
  let httpStatus = options.httpStatus;
522
527
  let errorData = {};
523
528
  if (options.error) {
@@ -578,8 +583,8 @@ const captureAiGeneration = async (client, options) => {
578
583
  $ai_provider: options.providerOverride ?? options.provider,
579
584
  $ai_model: options.modelOverride ?? options.model,
580
585
  $ai_model_parameters: options.modelParameters ?? {},
581
- $ai_input: withPrivacyMode(client, privacyMode, safeInput),
582
- $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
586
+ $ai_input: safeInput,
587
+ $ai_output_choices: safeOutput,
583
588
  $ai_http_status: httpStatus,
584
589
  $ai_input_tokens: usage.inputTokens ?? 0,
585
590
  ...(usage.outputTokens !== undefined ? {
@@ -1121,86 +1126,97 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1121
1126
 
1122
1127
  // Map to track in-progress tool calls
1123
1128
  const toolCallsInProgress = new Map();
1129
+ const captureStreamGeneration = async captureOptions => {
1130
+ try {
1131
+ await captureAiGeneration(phClient, captureOptions);
1132
+ } catch (error) {
1133
+ // Telemetry must never change the provider stream's behavior.
1134
+ console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1135
+ }
1136
+ };
1124
1137
  try {
1125
1138
  const {
1126
1139
  stream,
1127
1140
  ...rest
1128
1141
  } = 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;
1142
+ const reader = stream.getReader();
1143
+ let inBandError;
1144
+ let hasInBandError = false;
1145
+ let finalizationPromise;
1146
+ const observeChunk = chunk => {
1147
+ // Handle streaming patterns - compatible with both V2 and V3
1148
+ if (chunk.type === 'text-delta') {
1149
+ if (firstTokenTime === undefined) {
1150
+ firstTokenTime = Date.now();
1137
1151
  }
1138
- if (chunk.type === 'reasoning-delta') {
1139
- if (firstTokenTime === undefined) {
1140
- firstTokenTime = Date.now();
1141
- }
1142
- reasoningText += chunk.delta;
1152
+ generatedText += chunk.delta;
1153
+ }
1154
+ if (chunk.type === 'reasoning-delta') {
1155
+ if (firstTokenTime === undefined) {
1156
+ firstTokenTime = Date.now();
1143
1157
  }
1158
+ reasoningText += chunk.delta;
1159
+ }
1144
1160
 
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
- });
1161
+ // Handle tool call chunks
1162
+ if (chunk.type === 'tool-input-start') {
1163
+ if (firstTokenTime === undefined) {
1164
+ firstTokenTime = Date.now();
1156
1165
  }
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
- }
1163
- }
1164
- if (chunk.type === 'tool-input-end') {
1165
- // Tool call is complete, keep it in the map for final processing
1166
+ toolCallsInProgress.set(chunk.id, {
1167
+ toolCallId: chunk.id,
1168
+ toolName: chunk.toolName,
1169
+ input: ''
1170
+ });
1171
+ }
1172
+ if (chunk.type === 'tool-input-delta') {
1173
+ const toolCall = toolCallsInProgress.get(chunk.id);
1174
+ if (toolCall) {
1175
+ toolCall.input += chunk.delta;
1166
1176
  }
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
- });
1177
+ }
1178
+ if (chunk.type === 'tool-call') {
1179
+ if (firstTokenTime === undefined) {
1180
+ firstTokenTime = Date.now();
1177
1181
  }
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
- };
1182
+ toolCallsInProgress.set(chunk.toolCallId, {
1183
+ toolCallId: chunk.toolCallId,
1184
+ toolName: chunk.toolName,
1185
+ input: chunk.input
1186
+ });
1187
+ }
1188
+ if (chunk.type === 'error') {
1189
+ hasInBandError = true;
1190
+ inBandError = chunk.error;
1191
+ }
1192
+ if (chunk.type === 'finish') {
1193
+ providerMetadata = chunk.providerMetadata;
1194
+ const chunkUsage = chunk.usage || {};
1195
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, chunkUsage);
1196
+ usage = {
1197
+ inputTokens: extractTokenCount(chunk.usage?.inputTokens),
1198
+ outputTokens: extractTokenCount(chunk.usage?.outputTokens),
1199
+ reasoningTokens: extractReasoningTokens(chunkUsage),
1200
+ cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
1201
+ ...additionalTokenValues
1202
+ };
1189
1203
 
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
- }
1204
+ // Extract finish reason - V2 returns a string, V3 returns an object with .unified
1205
+ const rawFinishReason = chunk.finishReason;
1206
+ if (typeof rawFinishReason === 'string') {
1207
+ stopReason = rawFinishReason;
1208
+ } else if (rawFinishReason && typeof rawFinishReason === 'object' && 'unified' in rawFinishReason) {
1209
+ stopReason = String(rawFinishReason.unified);
1197
1210
  }
1198
- controller.enqueue(chunk);
1199
- },
1200
- flush: async () => {
1211
+ }
1212
+ };
1213
+ const finalize = (terminalError, isError = false) => {
1214
+ if (finalizationPromise) {
1215
+ return finalizationPromise;
1216
+ }
1217
+ finalizationPromise = (async () => {
1201
1218
  const latency = (Date.now() - startTime) / 1000;
1202
1219
  const timeToFirstToken = firstTokenTime !== undefined ? (firstTokenTime - startTime) / 1000 : undefined;
1203
- // Build content array similar to mapVercelOutput structure
1204
1220
  const content = [];
1205
1221
  if (reasoningText) {
1206
1222
  content.push({
@@ -1214,8 +1230,6 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1214
1230
  text: truncate(generatedText)
1215
1231
  });
1216
1232
  }
1217
-
1218
- // Add completed tool calls to content
1219
1233
  for (const toolCall of toolCallsInProgress.values()) {
1220
1234
  if (toolCall.toolName) {
1221
1235
  content.push({
@@ -1228,15 +1242,11 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1228
1242
  });
1229
1243
  }
1230
1244
  }
1231
-
1232
- // Structure output like mapVercelOutput does
1233
1245
  const output = content.length > 0 ? [{
1234
1246
  role: 'assistant',
1235
1247
  content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
1236
1248
  }] : [];
1237
1249
  const webSearchCount = extractWebSearchCount(providerMetadata, usage);
1238
-
1239
- // Update usage with web search count and raw metadata
1240
1250
  const finalUsage = {
1241
1251
  ...usage,
1242
1252
  webSearchCount,
@@ -1246,29 +1256,65 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
1246
1256
  }
1247
1257
  };
1248
1258
  adjustAnthropicV3CacheTokens(model, modelId, provider, finalUsage);
1249
- await captureAiGeneration(phClient, {
1259
+ const finishError = stopReason === 'error' ? new Error('Vercel AI SDK stream finished with an error') : undefined;
1260
+ const error = isError ? terminalError ?? new Error('Vercel AI SDK stream failed') : hasInBandError ? inBandError ?? new Error('Vercel AI SDK stream emitted an error chunk') : finishError;
1261
+ await captureStreamGeneration({
1250
1262
  ...baseOptions,
1251
1263
  model: modelId,
1252
1264
  provider: provider,
1253
1265
  input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
1254
- output: output,
1266
+ output,
1255
1267
  latency,
1256
1268
  timeToFirstToken,
1257
1269
  baseURL,
1258
1270
  modelParameters: getModelParams(mergedParams),
1259
- httpStatus: 200,
1271
+ httpStatus: error ? undefined : 200,
1260
1272
  usage: finalUsage,
1261
1273
  stopReason,
1274
+ error,
1262
1275
  tools: availableTools
1263
1276
  });
1277
+ })().catch(error => {
1278
+ // Building telemetry must not change the provider stream's behavior.
1279
+ console.warn('[PostHog AI] Failed to capture Vercel stream telemetry:', error);
1280
+ });
1281
+ return finalizationPromise;
1282
+ };
1283
+ const instrumentedStream = new ReadableStream({
1284
+ async pull(controller) {
1285
+ let result;
1286
+ try {
1287
+ result = await reader.read();
1288
+ } catch (error) {
1289
+ void finalize(error, true);
1290
+ controller.error(error);
1291
+ return;
1292
+ }
1293
+ if (result.done) {
1294
+ controller.close();
1295
+ void finalize();
1296
+ return;
1297
+ }
1298
+ try {
1299
+ observeChunk(result.value);
1300
+ } catch {
1301
+ // Instrumentation must not alter or suppress provider chunks.
1302
+ }
1303
+ controller.enqueue(result.value);
1304
+ },
1305
+ cancel(reason) {
1306
+ void finalize(reason ?? new Error('Vercel AI SDK stream was cancelled'), true);
1307
+ return reader.cancel(reason);
1264
1308
  }
1309
+ }, {
1310
+ highWaterMark: 0
1265
1311
  });
1266
1312
  return {
1267
- stream: stream.pipeThrough(transformStream),
1313
+ stream: instrumentedStream,
1268
1314
  ...rest
1269
1315
  };
1270
1316
  } catch (error) {
1271
- await captureAiGeneration(phClient, {
1317
+ await captureStreamGeneration({
1272
1318
  ...baseOptions,
1273
1319
  model: modelId,
1274
1320
  provider: provider,