@posthog/ai 6.5.0 → 6.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.
@@ -6,7 +6,7 @@ var openai = require('openai');
6
6
  var buffer = require('buffer');
7
7
  var uuid = require('uuid');
8
8
 
9
- var version = "6.5.0";
9
+ var version = "6.6.0";
10
10
 
11
11
  // Type guards for safer type checking
12
12
 
@@ -151,6 +151,109 @@ const withPrivacyMode = (client, privacyMode, input) => {
151
151
  return client.privacy_mode || privacyMode ? null : input;
152
152
  };
153
153
 
154
+ /**
155
+ * Calculate web search count from raw API response.
156
+ *
157
+ * Uses a two-tier detection strategy:
158
+ * Priority 1 (Exact Count): Count actual web search calls when available
159
+ * Priority 2 (Binary Detection): Return 1 if web search indicators are present, 0 otherwise
160
+ *
161
+ * @param result - Raw API response from any provider (OpenAI, Perplexity, OpenRouter, Gemini, etc.)
162
+ * @returns Number of web searches performed (exact count or binary 1/0)
163
+ */
164
+ function calculateWebSearchCount(result) {
165
+ if (!result || typeof result !== 'object') {
166
+ return 0;
167
+ }
168
+
169
+ // Priority 1: Exact Count
170
+ // Check for OpenAI Responses API web_search_call items
171
+ if ('output' in result && Array.isArray(result.output)) {
172
+ let count = 0;
173
+ for (const item of result.output) {
174
+ if (typeof item === 'object' && item !== null && 'type' in item && item.type === 'web_search_call') {
175
+ count++;
176
+ }
177
+ }
178
+ if (count > 0) {
179
+ return count;
180
+ }
181
+ }
182
+
183
+ // Priority 2: Binary Detection (1 or 0)
184
+
185
+ // Check for citations at root level (Perplexity)
186
+ if ('citations' in result && Array.isArray(result.citations) && result.citations.length > 0) {
187
+ return 1;
188
+ }
189
+
190
+ // Check for search_results at root level (Perplexity via OpenRouter)
191
+ if ('search_results' in result && Array.isArray(result.search_results) && result.search_results.length > 0) {
192
+ return 1;
193
+ }
194
+
195
+ // Check for usage.search_context_size (Perplexity via OpenRouter)
196
+ if ('usage' in result && typeof result.usage === 'object' && result.usage !== null) {
197
+ if ('search_context_size' in result.usage && result.usage.search_context_size) {
198
+ return 1;
199
+ }
200
+ }
201
+
202
+ // Check for annotations with url_citation in choices[].message (OpenAI/Perplexity)
203
+ if ('choices' in result && Array.isArray(result.choices)) {
204
+ for (const choice of result.choices) {
205
+ if (typeof choice === 'object' && choice !== null && 'message' in choice) {
206
+ const message = choice.message;
207
+ if (typeof message === 'object' && message !== null && 'annotations' in message) {
208
+ const annotations = message.annotations;
209
+ if (Array.isArray(annotations)) {
210
+ const hasUrlCitation = annotations.some(ann => {
211
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
212
+ });
213
+ if (hasUrlCitation) {
214
+ return 1;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ // Check for annotations in output[].content[] (OpenAI Responses API)
223
+ if ('output' in result && Array.isArray(result.output)) {
224
+ for (const item of result.output) {
225
+ if (typeof item === 'object' && item !== null && 'content' in item) {
226
+ const content = item.content;
227
+ if (Array.isArray(content)) {
228
+ for (const contentItem of content) {
229
+ if (typeof contentItem === 'object' && contentItem !== null && 'annotations' in contentItem) {
230
+ const annotations = contentItem.annotations;
231
+ if (Array.isArray(annotations)) {
232
+ const hasUrlCitation = annotations.some(ann => {
233
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
234
+ });
235
+ if (hasUrlCitation) {
236
+ return 1;
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ // Check for grounding_metadata (Gemini)
247
+ if ('candidates' in result && Array.isArray(result.candidates)) {
248
+ for (const candidate of result.candidates) {
249
+ if (typeof candidate === 'object' && candidate !== null && 'grounding_metadata' in candidate && candidate.grounding_metadata) {
250
+ return 1;
251
+ }
252
+ }
253
+ }
254
+ return 0;
255
+ }
256
+
154
257
  /**
155
258
  * Extract available tool calls from the request parameters.
156
259
  * These are the tools provided to the LLM, not the tool calls in the response.
@@ -269,6 +372,9 @@ const sendEventToPosthog = async ({
269
372
  } : {}),
270
373
  ...(usage.cacheCreationInputTokens ? {
271
374
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
375
+ } : {}),
376
+ ...(usage.webSearchCount ? {
377
+ $ai_web_search_count: usage.webSearchCount
272
378
  } : {})
273
379
  };
274
380
  const properties = {
@@ -510,13 +616,18 @@ class WrappedCompletions extends Completions {
510
616
  let accumulatedContent = '';
511
617
  let usage = {
512
618
  inputTokens: 0,
513
- outputTokens: 0
619
+ outputTokens: 0,
620
+ webSearchCount: 0
514
621
  };
515
622
 
516
623
  // Map to track in-progress tool calls
517
624
  const toolCallsInProgress = new Map();
518
625
  for await (const chunk of stream1) {
519
626
  const choice = chunk?.choices?.[0];
627
+ const chunkWebSearchCount = calculateWebSearchCount(chunk);
628
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
629
+ usage.webSearchCount = chunkWebSearchCount;
630
+ }
520
631
 
521
632
  // Handle text content
522
633
  const deltaContent = choice?.delta?.content;
@@ -558,6 +669,7 @@ class WrappedCompletions extends Completions {
558
669
  // Handle usage information
559
670
  if (chunk.usage) {
560
671
  usage = {
672
+ ...usage,
561
673
  inputTokens: chunk.usage.prompt_tokens ?? 0,
562
674
  outputTokens: chunk.usage.completion_tokens ?? 0,
563
675
  reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
@@ -612,7 +724,13 @@ class WrappedCompletions extends Completions {
612
724
  baseURL: this.baseURL,
613
725
  params: body,
614
726
  httpStatus: 200,
615
- usage,
727
+ usage: {
728
+ inputTokens: usage.inputTokens,
729
+ outputTokens: usage.outputTokens,
730
+ reasoningTokens: usage.reasoningTokens,
731
+ cacheReadInputTokens: usage.cacheReadInputTokens,
732
+ webSearchCount: usage.webSearchCount
733
+ },
616
734
  tools: availableTools
617
735
  });
618
736
  } catch (error) {
@@ -648,13 +766,14 @@ class WrappedCompletions extends Completions {
648
766
  if ('choices' in result) {
649
767
  const latency = (Date.now() - startTime) / 1000;
650
768
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
769
+ const formattedOutput = formatResponseOpenAI(result);
651
770
  await sendEventToPosthog({
652
771
  client: this.phClient,
653
772
  ...posthogParams,
654
773
  model: openAIParams.model,
655
774
  provider: 'openai',
656
775
  input: sanitizeOpenAI(openAIParams.messages),
657
- output: formatResponseOpenAI(result),
776
+ output: formattedOutput,
658
777
  latency,
659
778
  baseURL: this.baseURL,
660
779
  params: body,
@@ -663,7 +782,8 @@ class WrappedCompletions extends Completions {
663
782
  inputTokens: result.usage?.prompt_tokens ?? 0,
664
783
  outputTokens: result.usage?.completion_tokens ?? 0,
665
784
  reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
666
- cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
785
+ cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
786
+ webSearchCount: calculateWebSearchCount(result)
667
787
  },
668
788
  tools: availableTools
669
789
  });
@@ -725,14 +845,22 @@ class WrappedResponses extends Responses {
725
845
  let finalContent = [];
726
846
  let usage = {
727
847
  inputTokens: 0,
728
- outputTokens: 0
848
+ outputTokens: 0,
849
+ webSearchCount: 0
729
850
  };
730
851
  for await (const chunk of stream1) {
852
+ if ('response' in chunk && chunk.response) {
853
+ const chunkWebSearchCount = calculateWebSearchCount(chunk.response);
854
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
855
+ usage.webSearchCount = chunkWebSearchCount;
856
+ }
857
+ }
731
858
  if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
732
859
  finalContent = chunk.response.output;
733
860
  }
734
861
  if ('response' in chunk && chunk.response?.usage) {
735
862
  usage = {
863
+ ...usage,
736
864
  inputTokens: chunk.response.usage.input_tokens ?? 0,
737
865
  outputTokens: chunk.response.usage.output_tokens ?? 0,
738
866
  reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
@@ -754,7 +882,13 @@ class WrappedResponses extends Responses {
754
882
  baseURL: this.baseURL,
755
883
  params: body,
756
884
  httpStatus: 200,
757
- usage,
885
+ usage: {
886
+ inputTokens: usage.inputTokens,
887
+ outputTokens: usage.outputTokens,
888
+ reasoningTokens: usage.reasoningTokens,
889
+ cacheReadInputTokens: usage.cacheReadInputTokens,
890
+ webSearchCount: usage.webSearchCount
891
+ },
758
892
  tools: availableTools
759
893
  });
760
894
  } catch (error) {
@@ -789,6 +923,9 @@ class WrappedResponses extends Responses {
789
923
  if ('output' in result) {
790
924
  const latency = (Date.now() - startTime) / 1000;
791
925
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
926
+ const formattedOutput = formatResponseOpenAI({
927
+ output: result.output
928
+ });
792
929
  await sendEventToPosthog({
793
930
  client: this.phClient,
794
931
  ...posthogParams,
@@ -796,9 +933,7 @@ class WrappedResponses extends Responses {
796
933
  model: openAIParams.model,
797
934
  provider: 'openai',
798
935
  input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
799
- output: formatResponseOpenAI({
800
- output: result.output
801
- }),
936
+ output: formattedOutput,
802
937
  latency,
803
938
  baseURL: this.baseURL,
804
939
  params: body,
@@ -807,7 +942,8 @@ class WrappedResponses extends Responses {
807
942
  inputTokens: result.usage?.input_tokens ?? 0,
808
943
  outputTokens: result.usage?.output_tokens ?? 0,
809
944
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
810
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
945
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
946
+ webSearchCount: calculateWebSearchCount(result)
811
947
  },
812
948
  tools: availableTools
813
949
  });
@@ -845,9 +981,9 @@ class WrappedResponses extends Responses {
845
981
  } = extractPosthogParams(body);
846
982
  const startTime = Date.now();
847
983
  const originalCreate = super.create.bind(this);
848
- const originalSelf = this;
849
- const tempCreate = originalSelf.create;
850
- originalSelf.create = originalCreate;
984
+ const originalSelfRecord = this;
985
+ const tempCreate = originalSelfRecord['create'];
986
+ originalSelfRecord['create'] = originalCreate;
851
987
  try {
852
988
  const parentPromise = super.parse(openAIParams, options);
853
989
  const wrappedPromise = parentPromise.then(async result => {
@@ -896,7 +1032,7 @@ class WrappedResponses extends Responses {
896
1032
  return wrappedPromise;
897
1033
  } finally {
898
1034
  // Restore our wrapped create method
899
- originalSelf.create = tempCreate;
1035
+ originalSelfRecord['create'] = tempCreate;
900
1036
  }
901
1037
  }
902
1038
  }