@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.
@@ -2,7 +2,7 @@ import { OpenAI } from 'openai';
2
2
  import { Buffer } from 'buffer';
3
3
  import { v4 } from 'uuid';
4
4
 
5
- var version = "6.5.0";
5
+ var version = "6.6.0";
6
6
 
7
7
  // Type guards for safer type checking
8
8
 
@@ -147,6 +147,109 @@ const withPrivacyMode = (client, privacyMode, input) => {
147
147
  return client.privacy_mode || privacyMode ? null : input;
148
148
  };
149
149
 
150
+ /**
151
+ * Calculate web search count from raw API response.
152
+ *
153
+ * Uses a two-tier detection strategy:
154
+ * Priority 1 (Exact Count): Count actual web search calls when available
155
+ * Priority 2 (Binary Detection): Return 1 if web search indicators are present, 0 otherwise
156
+ *
157
+ * @param result - Raw API response from any provider (OpenAI, Perplexity, OpenRouter, Gemini, etc.)
158
+ * @returns Number of web searches performed (exact count or binary 1/0)
159
+ */
160
+ function calculateWebSearchCount(result) {
161
+ if (!result || typeof result !== 'object') {
162
+ return 0;
163
+ }
164
+
165
+ // Priority 1: Exact Count
166
+ // Check for OpenAI Responses API web_search_call items
167
+ if ('output' in result && Array.isArray(result.output)) {
168
+ let count = 0;
169
+ for (const item of result.output) {
170
+ if (typeof item === 'object' && item !== null && 'type' in item && item.type === 'web_search_call') {
171
+ count++;
172
+ }
173
+ }
174
+ if (count > 0) {
175
+ return count;
176
+ }
177
+ }
178
+
179
+ // Priority 2: Binary Detection (1 or 0)
180
+
181
+ // Check for citations at root level (Perplexity)
182
+ if ('citations' in result && Array.isArray(result.citations) && result.citations.length > 0) {
183
+ return 1;
184
+ }
185
+
186
+ // Check for search_results at root level (Perplexity via OpenRouter)
187
+ if ('search_results' in result && Array.isArray(result.search_results) && result.search_results.length > 0) {
188
+ return 1;
189
+ }
190
+
191
+ // Check for usage.search_context_size (Perplexity via OpenRouter)
192
+ if ('usage' in result && typeof result.usage === 'object' && result.usage !== null) {
193
+ if ('search_context_size' in result.usage && result.usage.search_context_size) {
194
+ return 1;
195
+ }
196
+ }
197
+
198
+ // Check for annotations with url_citation in choices[].message (OpenAI/Perplexity)
199
+ if ('choices' in result && Array.isArray(result.choices)) {
200
+ for (const choice of result.choices) {
201
+ if (typeof choice === 'object' && choice !== null && 'message' in choice) {
202
+ const message = choice.message;
203
+ if (typeof message === 'object' && message !== null && 'annotations' in message) {
204
+ const annotations = message.annotations;
205
+ if (Array.isArray(annotations)) {
206
+ const hasUrlCitation = annotations.some(ann => {
207
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
208
+ });
209
+ if (hasUrlCitation) {
210
+ return 1;
211
+ }
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ // Check for annotations in output[].content[] (OpenAI Responses API)
219
+ if ('output' in result && Array.isArray(result.output)) {
220
+ for (const item of result.output) {
221
+ if (typeof item === 'object' && item !== null && 'content' in item) {
222
+ const content = item.content;
223
+ if (Array.isArray(content)) {
224
+ for (const contentItem of content) {
225
+ if (typeof contentItem === 'object' && contentItem !== null && 'annotations' in contentItem) {
226
+ const annotations = contentItem.annotations;
227
+ if (Array.isArray(annotations)) {
228
+ const hasUrlCitation = annotations.some(ann => {
229
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
230
+ });
231
+ if (hasUrlCitation) {
232
+ return 1;
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ // Check for grounding_metadata (Gemini)
243
+ if ('candidates' in result && Array.isArray(result.candidates)) {
244
+ for (const candidate of result.candidates) {
245
+ if (typeof candidate === 'object' && candidate !== null && 'grounding_metadata' in candidate && candidate.grounding_metadata) {
246
+ return 1;
247
+ }
248
+ }
249
+ }
250
+ return 0;
251
+ }
252
+
150
253
  /**
151
254
  * Extract available tool calls from the request parameters.
152
255
  * These are the tools provided to the LLM, not the tool calls in the response.
@@ -265,6 +368,9 @@ const sendEventToPosthog = async ({
265
368
  } : {}),
266
369
  ...(usage.cacheCreationInputTokens ? {
267
370
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
371
+ } : {}),
372
+ ...(usage.webSearchCount ? {
373
+ $ai_web_search_count: usage.webSearchCount
268
374
  } : {})
269
375
  };
270
376
  const properties = {
@@ -506,13 +612,18 @@ class WrappedCompletions extends Completions {
506
612
  let accumulatedContent = '';
507
613
  let usage = {
508
614
  inputTokens: 0,
509
- outputTokens: 0
615
+ outputTokens: 0,
616
+ webSearchCount: 0
510
617
  };
511
618
 
512
619
  // Map to track in-progress tool calls
513
620
  const toolCallsInProgress = new Map();
514
621
  for await (const chunk of stream1) {
515
622
  const choice = chunk?.choices?.[0];
623
+ const chunkWebSearchCount = calculateWebSearchCount(chunk);
624
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
625
+ usage.webSearchCount = chunkWebSearchCount;
626
+ }
516
627
 
517
628
  // Handle text content
518
629
  const deltaContent = choice?.delta?.content;
@@ -554,6 +665,7 @@ class WrappedCompletions extends Completions {
554
665
  // Handle usage information
555
666
  if (chunk.usage) {
556
667
  usage = {
668
+ ...usage,
557
669
  inputTokens: chunk.usage.prompt_tokens ?? 0,
558
670
  outputTokens: chunk.usage.completion_tokens ?? 0,
559
671
  reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
@@ -608,7 +720,13 @@ class WrappedCompletions extends Completions {
608
720
  baseURL: this.baseURL,
609
721
  params: body,
610
722
  httpStatus: 200,
611
- usage,
723
+ usage: {
724
+ inputTokens: usage.inputTokens,
725
+ outputTokens: usage.outputTokens,
726
+ reasoningTokens: usage.reasoningTokens,
727
+ cacheReadInputTokens: usage.cacheReadInputTokens,
728
+ webSearchCount: usage.webSearchCount
729
+ },
612
730
  tools: availableTools
613
731
  });
614
732
  } catch (error) {
@@ -644,13 +762,14 @@ class WrappedCompletions extends Completions {
644
762
  if ('choices' in result) {
645
763
  const latency = (Date.now() - startTime) / 1000;
646
764
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
765
+ const formattedOutput = formatResponseOpenAI(result);
647
766
  await sendEventToPosthog({
648
767
  client: this.phClient,
649
768
  ...posthogParams,
650
769
  model: openAIParams.model,
651
770
  provider: 'openai',
652
771
  input: sanitizeOpenAI(openAIParams.messages),
653
- output: formatResponseOpenAI(result),
772
+ output: formattedOutput,
654
773
  latency,
655
774
  baseURL: this.baseURL,
656
775
  params: body,
@@ -659,7 +778,8 @@ class WrappedCompletions extends Completions {
659
778
  inputTokens: result.usage?.prompt_tokens ?? 0,
660
779
  outputTokens: result.usage?.completion_tokens ?? 0,
661
780
  reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
662
- cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
781
+ cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
782
+ webSearchCount: calculateWebSearchCount(result)
663
783
  },
664
784
  tools: availableTools
665
785
  });
@@ -721,14 +841,22 @@ class WrappedResponses extends Responses {
721
841
  let finalContent = [];
722
842
  let usage = {
723
843
  inputTokens: 0,
724
- outputTokens: 0
844
+ outputTokens: 0,
845
+ webSearchCount: 0
725
846
  };
726
847
  for await (const chunk of stream1) {
848
+ if ('response' in chunk && chunk.response) {
849
+ const chunkWebSearchCount = calculateWebSearchCount(chunk.response);
850
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
851
+ usage.webSearchCount = chunkWebSearchCount;
852
+ }
853
+ }
727
854
  if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
728
855
  finalContent = chunk.response.output;
729
856
  }
730
857
  if ('response' in chunk && chunk.response?.usage) {
731
858
  usage = {
859
+ ...usage,
732
860
  inputTokens: chunk.response.usage.input_tokens ?? 0,
733
861
  outputTokens: chunk.response.usage.output_tokens ?? 0,
734
862
  reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
@@ -750,7 +878,13 @@ class WrappedResponses extends Responses {
750
878
  baseURL: this.baseURL,
751
879
  params: body,
752
880
  httpStatus: 200,
753
- usage,
881
+ usage: {
882
+ inputTokens: usage.inputTokens,
883
+ outputTokens: usage.outputTokens,
884
+ reasoningTokens: usage.reasoningTokens,
885
+ cacheReadInputTokens: usage.cacheReadInputTokens,
886
+ webSearchCount: usage.webSearchCount
887
+ },
754
888
  tools: availableTools
755
889
  });
756
890
  } catch (error) {
@@ -785,6 +919,9 @@ class WrappedResponses extends Responses {
785
919
  if ('output' in result) {
786
920
  const latency = (Date.now() - startTime) / 1000;
787
921
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
922
+ const formattedOutput = formatResponseOpenAI({
923
+ output: result.output
924
+ });
788
925
  await sendEventToPosthog({
789
926
  client: this.phClient,
790
927
  ...posthogParams,
@@ -792,9 +929,7 @@ class WrappedResponses extends Responses {
792
929
  model: openAIParams.model,
793
930
  provider: 'openai',
794
931
  input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
795
- output: formatResponseOpenAI({
796
- output: result.output
797
- }),
932
+ output: formattedOutput,
798
933
  latency,
799
934
  baseURL: this.baseURL,
800
935
  params: body,
@@ -803,7 +938,8 @@ class WrappedResponses extends Responses {
803
938
  inputTokens: result.usage?.input_tokens ?? 0,
804
939
  outputTokens: result.usage?.output_tokens ?? 0,
805
940
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
806
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
941
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
942
+ webSearchCount: calculateWebSearchCount(result)
807
943
  },
808
944
  tools: availableTools
809
945
  });
@@ -841,9 +977,9 @@ class WrappedResponses extends Responses {
841
977
  } = extractPosthogParams(body);
842
978
  const startTime = Date.now();
843
979
  const originalCreate = super.create.bind(this);
844
- const originalSelf = this;
845
- const tempCreate = originalSelf.create;
846
- originalSelf.create = originalCreate;
980
+ const originalSelfRecord = this;
981
+ const tempCreate = originalSelfRecord['create'];
982
+ originalSelfRecord['create'] = originalCreate;
847
983
  try {
848
984
  const parentPromise = super.parse(openAIParams, options);
849
985
  const wrappedPromise = parentPromise.then(async result => {
@@ -892,7 +1028,7 @@ class WrappedResponses extends Responses {
892
1028
  return wrappedPromise;
893
1029
  } finally {
894
1030
  // Restore our wrapped create method
895
- originalSelf.create = tempCreate;
1031
+ originalSelfRecord['create'] = tempCreate;
896
1032
  }
897
1033
  }
898
1034
  }