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