@posthog/ai 6.5.1 → 7.0.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.1";
9
+ var version = "7.0.0";
10
10
 
11
11
  // Type guards for safer type checking
12
12
 
@@ -151,6 +151,110 @@ 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 or choices[].delta (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) {
206
+ // Check both message (non-streaming) and delta (streaming) for annotations
207
+ const content = ('message' in choice ? choice.message : null) || ('delta' in choice ? choice.delta : null);
208
+ if (typeof content === 'object' && content !== null && 'annotations' in content) {
209
+ const annotations = content.annotations;
210
+ if (Array.isArray(annotations)) {
211
+ const hasUrlCitation = annotations.some(ann => {
212
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
213
+ });
214
+ if (hasUrlCitation) {
215
+ return 1;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // Check for annotations in output[].content[] (OpenAI Responses API)
224
+ if ('output' in result && Array.isArray(result.output)) {
225
+ for (const item of result.output) {
226
+ if (typeof item === 'object' && item !== null && 'content' in item) {
227
+ const content = item.content;
228
+ if (Array.isArray(content)) {
229
+ for (const contentItem of content) {
230
+ if (typeof contentItem === 'object' && contentItem !== null && 'annotations' in contentItem) {
231
+ const annotations = contentItem.annotations;
232
+ if (Array.isArray(annotations)) {
233
+ const hasUrlCitation = annotations.some(ann => {
234
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
235
+ });
236
+ if (hasUrlCitation) {
237
+ return 1;
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ // Check for grounding_metadata (Gemini)
248
+ if ('candidates' in result && Array.isArray(result.candidates)) {
249
+ for (const candidate of result.candidates) {
250
+ if (typeof candidate === 'object' && candidate !== null && 'grounding_metadata' in candidate && candidate.grounding_metadata) {
251
+ return 1;
252
+ }
253
+ }
254
+ }
255
+ return 0;
256
+ }
257
+
154
258
  /**
155
259
  * Extract available tool calls from the request parameters.
156
260
  * These are the tools provided to the LLM, not the tool calls in the response.
@@ -269,6 +373,9 @@ const sendEventToPosthog = async ({
269
373
  } : {}),
270
374
  ...(usage.cacheCreationInputTokens ? {
271
375
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
376
+ } : {}),
377
+ ...(usage.webSearchCount ? {
378
+ $ai_web_search_count: usage.webSearchCount
272
379
  } : {})
273
380
  };
274
381
  const properties = {
@@ -510,13 +617,18 @@ class WrappedCompletions extends Completions {
510
617
  let accumulatedContent = '';
511
618
  let usage = {
512
619
  inputTokens: 0,
513
- outputTokens: 0
620
+ outputTokens: 0,
621
+ webSearchCount: 0
514
622
  };
515
623
 
516
624
  // Map to track in-progress tool calls
517
625
  const toolCallsInProgress = new Map();
518
626
  for await (const chunk of stream1) {
519
627
  const choice = chunk?.choices?.[0];
628
+ const chunkWebSearchCount = calculateWebSearchCount(chunk);
629
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
630
+ usage.webSearchCount = chunkWebSearchCount;
631
+ }
520
632
 
521
633
  // Handle text content
522
634
  const deltaContent = choice?.delta?.content;
@@ -558,6 +670,7 @@ class WrappedCompletions extends Completions {
558
670
  // Handle usage information
559
671
  if (chunk.usage) {
560
672
  usage = {
673
+ ...usage,
561
674
  inputTokens: chunk.usage.prompt_tokens ?? 0,
562
675
  outputTokens: chunk.usage.completion_tokens ?? 0,
563
676
  reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
@@ -612,7 +725,13 @@ class WrappedCompletions extends Completions {
612
725
  baseURL: this.baseURL,
613
726
  params: body,
614
727
  httpStatus: 200,
615
- usage,
728
+ usage: {
729
+ inputTokens: usage.inputTokens,
730
+ outputTokens: usage.outputTokens,
731
+ reasoningTokens: usage.reasoningTokens,
732
+ cacheReadInputTokens: usage.cacheReadInputTokens,
733
+ webSearchCount: usage.webSearchCount
734
+ },
616
735
  tools: availableTools
617
736
  });
618
737
  } catch (error) {
@@ -648,13 +767,14 @@ class WrappedCompletions extends Completions {
648
767
  if ('choices' in result) {
649
768
  const latency = (Date.now() - startTime) / 1000;
650
769
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
770
+ const formattedOutput = formatResponseOpenAI(result);
651
771
  await sendEventToPosthog({
652
772
  client: this.phClient,
653
773
  ...posthogParams,
654
774
  model: openAIParams.model,
655
775
  provider: 'openai',
656
776
  input: sanitizeOpenAI(openAIParams.messages),
657
- output: formatResponseOpenAI(result),
777
+ output: formattedOutput,
658
778
  latency,
659
779
  baseURL: this.baseURL,
660
780
  params: body,
@@ -663,7 +783,8 @@ class WrappedCompletions extends Completions {
663
783
  inputTokens: result.usage?.prompt_tokens ?? 0,
664
784
  outputTokens: result.usage?.completion_tokens ?? 0,
665
785
  reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
666
- cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
786
+ cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
787
+ webSearchCount: calculateWebSearchCount(result)
667
788
  },
668
789
  tools: availableTools
669
790
  });
@@ -725,14 +846,22 @@ class WrappedResponses extends Responses {
725
846
  let finalContent = [];
726
847
  let usage = {
727
848
  inputTokens: 0,
728
- outputTokens: 0
849
+ outputTokens: 0,
850
+ webSearchCount: 0
729
851
  };
730
852
  for await (const chunk of stream1) {
853
+ if ('response' in chunk && chunk.response) {
854
+ const chunkWebSearchCount = calculateWebSearchCount(chunk.response);
855
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
856
+ usage.webSearchCount = chunkWebSearchCount;
857
+ }
858
+ }
731
859
  if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
732
860
  finalContent = chunk.response.output;
733
861
  }
734
862
  if ('response' in chunk && chunk.response?.usage) {
735
863
  usage = {
864
+ ...usage,
736
865
  inputTokens: chunk.response.usage.input_tokens ?? 0,
737
866
  outputTokens: chunk.response.usage.output_tokens ?? 0,
738
867
  reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
@@ -754,7 +883,13 @@ class WrappedResponses extends Responses {
754
883
  baseURL: this.baseURL,
755
884
  params: body,
756
885
  httpStatus: 200,
757
- usage,
886
+ usage: {
887
+ inputTokens: usage.inputTokens,
888
+ outputTokens: usage.outputTokens,
889
+ reasoningTokens: usage.reasoningTokens,
890
+ cacheReadInputTokens: usage.cacheReadInputTokens,
891
+ webSearchCount: usage.webSearchCount
892
+ },
758
893
  tools: availableTools
759
894
  });
760
895
  } catch (error) {
@@ -789,6 +924,9 @@ class WrappedResponses extends Responses {
789
924
  if ('output' in result) {
790
925
  const latency = (Date.now() - startTime) / 1000;
791
926
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
927
+ const formattedOutput = formatResponseOpenAI({
928
+ output: result.output
929
+ });
792
930
  await sendEventToPosthog({
793
931
  client: this.phClient,
794
932
  ...posthogParams,
@@ -796,9 +934,7 @@ class WrappedResponses extends Responses {
796
934
  model: openAIParams.model,
797
935
  provider: 'openai',
798
936
  input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
799
- output: formatResponseOpenAI({
800
- output: result.output
801
- }),
937
+ output: formattedOutput,
802
938
  latency,
803
939
  baseURL: this.baseURL,
804
940
  params: body,
@@ -807,7 +943,8 @@ class WrappedResponses extends Responses {
807
943
  inputTokens: result.usage?.input_tokens ?? 0,
808
944
  outputTokens: result.usage?.output_tokens ?? 0,
809
945
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
810
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
946
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
947
+ webSearchCount: calculateWebSearchCount(result)
811
948
  },
812
949
  tools: availableTools
813
950
  });
@@ -845,9 +982,9 @@ class WrappedResponses extends Responses {
845
982
  } = extractPosthogParams(body);
846
983
  const startTime = Date.now();
847
984
  const originalCreate = super.create.bind(this);
848
- const originalSelf = this;
849
- const tempCreate = originalSelf.create;
850
- originalSelf.create = originalCreate;
985
+ const originalSelfRecord = this;
986
+ const tempCreate = originalSelfRecord['create'];
987
+ originalSelfRecord['create'] = originalCreate;
851
988
  try {
852
989
  const parentPromise = super.parse(openAIParams, options);
853
990
  const wrappedPromise = parentPromise.then(async result => {
@@ -896,7 +1033,7 @@ class WrappedResponses extends Responses {
896
1033
  return wrappedPromise;
897
1034
  } finally {
898
1035
  // Restore our wrapped create method
899
- originalSelf.create = tempCreate;
1036
+ originalSelfRecord['create'] = tempCreate;
900
1037
  }
901
1038
  }
902
1039
  }