@posthog/ai 7.5.3 → 7.6.0

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.
@@ -7,7 +7,7 @@ var buffer = require('buffer');
7
7
  var uuid = require('uuid');
8
8
  var core = require('@posthog/core');
9
9
 
10
- var version = "7.5.3";
10
+ var version = "7.6.0";
11
11
 
12
12
  // Type guards for safer type checking
13
13
 
@@ -18,6 +18,139 @@ const isObject = value => {
18
18
  return value !== null && typeof value === 'object' && !Array.isArray(value);
19
19
  };
20
20
 
21
+ const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
22
+
23
+ // ============================================
24
+ // Multimodal Feature Toggle
25
+ // ============================================
26
+
27
+ const isMultimodalEnabled = () => {
28
+ const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
29
+ return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
30
+ };
31
+
32
+ // ============================================
33
+ // Base64 Detection Helpers
34
+ // ============================================
35
+
36
+ const isBase64DataUrl = str => {
37
+ return /^data:([^;]+);base64,/.test(str);
38
+ };
39
+ const isValidUrl = str => {
40
+ try {
41
+ new URL(str);
42
+ return true;
43
+ } catch {
44
+ // Not an absolute URL, check if it's a relative URL or path
45
+ return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
46
+ }
47
+ };
48
+ const isRawBase64 = str => {
49
+ // Skip if it's a valid URL or path
50
+ if (isValidUrl(str)) {
51
+ return false;
52
+ }
53
+
54
+ // Check if it's a valid base64 string
55
+ // Base64 images are typically at least a few hundred chars, but we'll be conservative
56
+ return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
57
+ };
58
+ function redactBase64DataUrl(str) {
59
+ if (isMultimodalEnabled()) return str;
60
+ if (!isString(str)) return str;
61
+
62
+ // Check for data URL format
63
+ if (isBase64DataUrl(str)) {
64
+ return REDACTED_IMAGE_PLACEHOLDER;
65
+ }
66
+
67
+ // Check for raw base64 (Vercel sends raw base64 for inline images)
68
+ if (isRawBase64(str)) {
69
+ return REDACTED_IMAGE_PLACEHOLDER;
70
+ }
71
+ return str;
72
+ }
73
+
74
+ // ============================================
75
+ // Common Message Processing
76
+ // ============================================
77
+
78
+ const processMessages = (messages, transformContent) => {
79
+ if (!messages) return messages;
80
+ const processContent = content => {
81
+ if (typeof content === 'string') return content;
82
+ if (!content) return content;
83
+ if (Array.isArray(content)) {
84
+ return content.map(transformContent);
85
+ }
86
+
87
+ // Handle single object content
88
+ return transformContent(content);
89
+ };
90
+ const processMessage = msg => {
91
+ if (!isObject(msg) || !('content' in msg)) return msg;
92
+ return {
93
+ ...msg,
94
+ content: processContent(msg.content)
95
+ };
96
+ };
97
+
98
+ // Handle both arrays and single messages
99
+ if (Array.isArray(messages)) {
100
+ return messages.map(processMessage);
101
+ }
102
+ return processMessage(messages);
103
+ };
104
+
105
+ // ============================================
106
+ // Provider-Specific Image Sanitizers
107
+ // ============================================
108
+
109
+ const sanitizeOpenAIImage = item => {
110
+ if (!isObject(item)) return item;
111
+
112
+ // Handle image_url format
113
+ if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
114
+ return {
115
+ ...item,
116
+ image_url: {
117
+ ...item.image_url,
118
+ url: redactBase64DataUrl(item.image_url.url)
119
+ }
120
+ };
121
+ }
122
+
123
+ // Handle audio format
124
+ if (item.type === 'audio' && 'data' in item) {
125
+ if (isMultimodalEnabled()) return item;
126
+ return {
127
+ ...item,
128
+ data: REDACTED_IMAGE_PLACEHOLDER
129
+ };
130
+ }
131
+ return item;
132
+ };
133
+ const sanitizeOpenAIResponseImage = item => {
134
+ if (!isObject(item)) return item;
135
+
136
+ // Handle input_image format
137
+ if (item.type === 'input_image' && 'image_url' in item) {
138
+ return {
139
+ ...item,
140
+ image_url: redactBase64DataUrl(item.image_url)
141
+ };
142
+ }
143
+ return item;
144
+ };
145
+
146
+ // Export individual sanitizers for tree-shaking
147
+ const sanitizeOpenAI = data => {
148
+ return processMessages(data, sanitizeOpenAIImage);
149
+ };
150
+ const sanitizeOpenAIResponse = data => {
151
+ return processMessages(data, sanitizeOpenAIResponseImage);
152
+ };
153
+
21
154
  const STRING_FORMAT = 'utf8';
22
155
 
23
156
  /**
@@ -413,6 +546,9 @@ const sendEventToPosthog = async ({
413
546
  } : {}),
414
547
  ...(usage.webSearchCount ? {
415
548
  $ai_web_search_count: usage.webSearchCount
549
+ } : {}),
550
+ ...(usage.rawUsage ? {
551
+ $ai_usage: usage.rawUsage
416
552
  } : {})
417
553
  };
418
554
  const properties = {
@@ -502,124 +638,6 @@ function formatOpenAIResponsesInput(input, instructions) {
502
638
  return messages;
503
639
  }
504
640
 
505
- const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
506
-
507
- // ============================================
508
- // Multimodal Feature Toggle
509
- // ============================================
510
-
511
- const isMultimodalEnabled = () => {
512
- const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
513
- return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
514
- };
515
-
516
- // ============================================
517
- // Base64 Detection Helpers
518
- // ============================================
519
-
520
- const isBase64DataUrl = str => {
521
- return /^data:([^;]+);base64,/.test(str);
522
- };
523
- const isValidUrl = str => {
524
- try {
525
- new URL(str);
526
- return true;
527
- } catch {
528
- // Not an absolute URL, check if it's a relative URL or path
529
- return str.startsWith('/') || str.startsWith('./') || str.startsWith('../');
530
- }
531
- };
532
- const isRawBase64 = str => {
533
- // Skip if it's a valid URL or path
534
- if (isValidUrl(str)) {
535
- return false;
536
- }
537
-
538
- // Check if it's a valid base64 string
539
- // Base64 images are typically at least a few hundred chars, but we'll be conservative
540
- return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
541
- };
542
- function redactBase64DataUrl(str) {
543
- if (isMultimodalEnabled()) return str;
544
- if (!isString(str)) return str;
545
-
546
- // Check for data URL format
547
- if (isBase64DataUrl(str)) {
548
- return REDACTED_IMAGE_PLACEHOLDER;
549
- }
550
-
551
- // Check for raw base64 (Vercel sends raw base64 for inline images)
552
- if (isRawBase64(str)) {
553
- return REDACTED_IMAGE_PLACEHOLDER;
554
- }
555
- return str;
556
- }
557
-
558
- // ============================================
559
- // Common Message Processing
560
- // ============================================
561
-
562
- const processMessages = (messages, transformContent) => {
563
- if (!messages) return messages;
564
- const processContent = content => {
565
- if (typeof content === 'string') return content;
566
- if (!content) return content;
567
- if (Array.isArray(content)) {
568
- return content.map(transformContent);
569
- }
570
-
571
- // Handle single object content
572
- return transformContent(content);
573
- };
574
- const processMessage = msg => {
575
- if (!isObject(msg) || !('content' in msg)) return msg;
576
- return {
577
- ...msg,
578
- content: processContent(msg.content)
579
- };
580
- };
581
-
582
- // Handle both arrays and single messages
583
- if (Array.isArray(messages)) {
584
- return messages.map(processMessage);
585
- }
586
- return processMessage(messages);
587
- };
588
-
589
- // ============================================
590
- // Provider-Specific Image Sanitizers
591
- // ============================================
592
-
593
- const sanitizeOpenAIImage = item => {
594
- if (!isObject(item)) return item;
595
-
596
- // Handle image_url format
597
- if (item.type === 'image_url' && 'image_url' in item && isObject(item.image_url) && 'url' in item.image_url) {
598
- return {
599
- ...item,
600
- image_url: {
601
- ...item.image_url,
602
- url: redactBase64DataUrl(item.image_url.url)
603
- }
604
- };
605
- }
606
-
607
- // Handle audio format
608
- if (item.type === 'audio' && 'data' in item) {
609
- if (isMultimodalEnabled()) return item;
610
- return {
611
- ...item,
612
- data: REDACTED_IMAGE_PLACEHOLDER
613
- };
614
- }
615
- return item;
616
- };
617
-
618
- // Export individual sanitizers for tree-shaking
619
- const sanitizeOpenAI = data => {
620
- return processMessages(data, sanitizeOpenAIImage);
621
- };
622
-
623
641
  const Chat = openai.OpenAI.Chat;
624
642
  const Completions = Chat.Completions;
625
643
  const Responses = openai.OpenAI.Responses;
@@ -684,6 +702,7 @@ class WrappedCompletions extends Completions {
684
702
 
685
703
  // Map to track in-progress tool calls
686
704
  const toolCallsInProgress = new Map();
705
+ let rawUsageData;
687
706
  for await (const chunk of stream1) {
688
707
  // Extract model from chunk (Chat Completions chunks have model field)
689
708
  if (!modelFromResponse && chunk.model) {
@@ -734,6 +753,7 @@ class WrappedCompletions extends Completions {
734
753
 
735
754
  // Handle usage information
736
755
  if (chunk.usage) {
756
+ rawUsageData = chunk.usage;
737
757
  usage = {
738
758
  ...usage,
739
759
  inputTokens: chunk.usage.prompt_tokens ?? 0,
@@ -795,7 +815,8 @@ class WrappedCompletions extends Completions {
795
815
  outputTokens: usage.outputTokens,
796
816
  reasoningTokens: usage.reasoningTokens,
797
817
  cacheReadInputTokens: usage.cacheReadInputTokens,
798
- webSearchCount: usage.webSearchCount
818
+ webSearchCount: usage.webSearchCount,
819
+ rawUsage: rawUsageData
799
820
  },
800
821
  tools: availableTools
801
822
  });
@@ -847,7 +868,8 @@ class WrappedCompletions extends Completions {
847
868
  outputTokens: result.usage?.completion_tokens ?? 0,
848
869
  reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
849
870
  cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
850
- webSearchCount: calculateWebSearchCount(result)
871
+ webSearchCount: calculateWebSearchCount(result),
872
+ rawUsage: result.usage
851
873
  },
852
874
  tools: availableTools
853
875
  });
@@ -912,6 +934,7 @@ class WrappedResponses extends Responses {
912
934
  outputTokens: 0,
913
935
  webSearchCount: 0
914
936
  };
937
+ let rawUsageData;
915
938
  for await (const chunk of stream1) {
916
939
  if ('response' in chunk && chunk.response) {
917
940
  // Extract model from response object in chunk (for stored prompts)
@@ -927,6 +950,7 @@ class WrappedResponses extends Responses {
927
950
  finalContent = chunk.response.output;
928
951
  }
929
952
  if ('response' in chunk && chunk.response?.usage) {
953
+ rawUsageData = chunk.response.usage;
930
954
  usage = {
931
955
  ...usage,
932
956
  inputTokens: chunk.response.usage.input_tokens ?? 0,
@@ -943,7 +967,7 @@ class WrappedResponses extends Responses {
943
967
  ...posthogParams,
944
968
  model: openAIParams.model ?? modelFromResponse,
945
969
  provider: 'openai',
946
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
970
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
947
971
  output: finalContent,
948
972
  latency,
949
973
  baseURL: this.baseURL,
@@ -954,7 +978,8 @@ class WrappedResponses extends Responses {
954
978
  outputTokens: usage.outputTokens,
955
979
  reasoningTokens: usage.reasoningTokens,
956
980
  cacheReadInputTokens: usage.cacheReadInputTokens,
957
- webSearchCount: usage.webSearchCount
981
+ webSearchCount: usage.webSearchCount,
982
+ rawUsage: rawUsageData
958
983
  },
959
984
  tools: availableTools
960
985
  });
@@ -964,7 +989,7 @@ class WrappedResponses extends Responses {
964
989
  ...posthogParams,
965
990
  model: openAIParams.model,
966
991
  provider: 'openai',
967
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
992
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
968
993
  output: [],
969
994
  latency: 0,
970
995
  baseURL: this.baseURL,
@@ -995,7 +1020,7 @@ class WrappedResponses extends Responses {
995
1020
  ...posthogParams,
996
1021
  model: openAIParams.model ?? result.model,
997
1022
  provider: 'openai',
998
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1023
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
999
1024
  output: formattedOutput,
1000
1025
  latency,
1001
1026
  baseURL: this.baseURL,
@@ -1006,7 +1031,8 @@ class WrappedResponses extends Responses {
1006
1031
  outputTokens: result.usage?.output_tokens ?? 0,
1007
1032
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1008
1033
  cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1009
- webSearchCount: calculateWebSearchCount(result)
1034
+ webSearchCount: calculateWebSearchCount(result),
1035
+ rawUsage: result.usage
1010
1036
  },
1011
1037
  tools: availableTools
1012
1038
  });
@@ -1019,7 +1045,7 @@ class WrappedResponses extends Responses {
1019
1045
  ...posthogParams,
1020
1046
  model: openAIParams.model,
1021
1047
  provider: 'openai',
1022
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1048
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1023
1049
  output: [],
1024
1050
  latency: 0,
1025
1051
  baseURL: this.baseURL,
@@ -1055,7 +1081,7 @@ class WrappedResponses extends Responses {
1055
1081
  ...posthogParams,
1056
1082
  model: openAIParams.model ?? result.model,
1057
1083
  provider: 'openai',
1058
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1084
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1059
1085
  output: result.output,
1060
1086
  latency,
1061
1087
  baseURL: this.baseURL,
@@ -1065,7 +1091,8 @@ class WrappedResponses extends Responses {
1065
1091
  inputTokens: result.usage?.input_tokens ?? 0,
1066
1092
  outputTokens: result.usage?.output_tokens ?? 0,
1067
1093
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
1068
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
1094
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
1095
+ rawUsage: result.usage
1069
1096
  }
1070
1097
  });
1071
1098
  return result;
@@ -1075,7 +1102,7 @@ class WrappedResponses extends Responses {
1075
1102
  ...posthogParams,
1076
1103
  model: openAIParams.model,
1077
1104
  provider: 'openai',
1078
- input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
1105
+ input: formatOpenAIResponsesInput(sanitizeOpenAIResponse(openAIParams.input), openAIParams.instructions),
1079
1106
  output: [],
1080
1107
  latency: 0,
1081
1108
  baseURL: this.baseURL,
@@ -1124,7 +1151,8 @@ class WrappedEmbeddings extends Embeddings {
1124
1151
  params: body,
1125
1152
  httpStatus: 200,
1126
1153
  usage: {
1127
- inputTokens: result.usage?.prompt_tokens ?? 0
1154
+ inputTokens: result.usage?.prompt_tokens ?? 0,
1155
+ rawUsage: result.usage
1128
1156
  }
1129
1157
  });
1130
1158
  return result;
@@ -1207,7 +1235,8 @@ class WrappedTranscriptions extends Transcriptions {
1207
1235
  if ('usage' in chunk && chunk.usage) {
1208
1236
  usage = {
1209
1237
  inputTokens: chunk.usage?.type === 'tokens' ? chunk.usage.input_tokens ?? 0 : 0,
1210
- outputTokens: chunk.usage?.type === 'tokens' ? chunk.usage.output_tokens ?? 0 : 0
1238
+ outputTokens: chunk.usage?.type === 'tokens' ? chunk.usage.output_tokens ?? 0 : 0,
1239
+ rawUsage: chunk.usage
1211
1240
  };
1212
1241
  }
1213
1242
  }
@@ -1268,7 +1297,8 @@ class WrappedTranscriptions extends Transcriptions {
1268
1297
  httpStatus: 200,
1269
1298
  usage: {
1270
1299
  inputTokens: result.usage?.type === 'tokens' ? result.usage.input_tokens ?? 0 : 0,
1271
- outputTokens: result.usage?.type === 'tokens' ? result.usage.output_tokens ?? 0 : 0
1300
+ outputTokens: result.usage?.type === 'tokens' ? result.usage.output_tokens ?? 0 : 0,
1301
+ rawUsage: result.usage
1272
1302
  }
1273
1303
  });
1274
1304
  return result;