@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.
@@ -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.1";
5
+ var version = "7.0.0";
6
6
 
7
7
  // Type guards for safer type checking
8
8
 
@@ -147,6 +147,110 @@ 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 or choices[].delta (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) {
202
+ // Check both message (non-streaming) and delta (streaming) for annotations
203
+ const content = ('message' in choice ? choice.message : null) || ('delta' in choice ? choice.delta : null);
204
+ if (typeof content === 'object' && content !== null && 'annotations' in content) {
205
+ const annotations = content.annotations;
206
+ if (Array.isArray(annotations)) {
207
+ const hasUrlCitation = annotations.some(ann => {
208
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
209
+ });
210
+ if (hasUrlCitation) {
211
+ return 1;
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ // Check for annotations in output[].content[] (OpenAI Responses API)
220
+ if ('output' in result && Array.isArray(result.output)) {
221
+ for (const item of result.output) {
222
+ if (typeof item === 'object' && item !== null && 'content' in item) {
223
+ const content = item.content;
224
+ if (Array.isArray(content)) {
225
+ for (const contentItem of content) {
226
+ if (typeof contentItem === 'object' && contentItem !== null && 'annotations' in contentItem) {
227
+ const annotations = contentItem.annotations;
228
+ if (Array.isArray(annotations)) {
229
+ const hasUrlCitation = annotations.some(ann => {
230
+ return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
231
+ });
232
+ if (hasUrlCitation) {
233
+ return 1;
234
+ }
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+ }
242
+
243
+ // Check for grounding_metadata (Gemini)
244
+ if ('candidates' in result && Array.isArray(result.candidates)) {
245
+ for (const candidate of result.candidates) {
246
+ if (typeof candidate === 'object' && candidate !== null && 'grounding_metadata' in candidate && candidate.grounding_metadata) {
247
+ return 1;
248
+ }
249
+ }
250
+ }
251
+ return 0;
252
+ }
253
+
150
254
  /**
151
255
  * Extract available tool calls from the request parameters.
152
256
  * These are the tools provided to the LLM, not the tool calls in the response.
@@ -265,6 +369,9 @@ const sendEventToPosthog = async ({
265
369
  } : {}),
266
370
  ...(usage.cacheCreationInputTokens ? {
267
371
  $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
372
+ } : {}),
373
+ ...(usage.webSearchCount ? {
374
+ $ai_web_search_count: usage.webSearchCount
268
375
  } : {})
269
376
  };
270
377
  const properties = {
@@ -506,13 +613,18 @@ class WrappedCompletions extends Completions {
506
613
  let accumulatedContent = '';
507
614
  let usage = {
508
615
  inputTokens: 0,
509
- outputTokens: 0
616
+ outputTokens: 0,
617
+ webSearchCount: 0
510
618
  };
511
619
 
512
620
  // Map to track in-progress tool calls
513
621
  const toolCallsInProgress = new Map();
514
622
  for await (const chunk of stream1) {
515
623
  const choice = chunk?.choices?.[0];
624
+ const chunkWebSearchCount = calculateWebSearchCount(chunk);
625
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
626
+ usage.webSearchCount = chunkWebSearchCount;
627
+ }
516
628
 
517
629
  // Handle text content
518
630
  const deltaContent = choice?.delta?.content;
@@ -554,6 +666,7 @@ class WrappedCompletions extends Completions {
554
666
  // Handle usage information
555
667
  if (chunk.usage) {
556
668
  usage = {
669
+ ...usage,
557
670
  inputTokens: chunk.usage.prompt_tokens ?? 0,
558
671
  outputTokens: chunk.usage.completion_tokens ?? 0,
559
672
  reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
@@ -608,7 +721,13 @@ class WrappedCompletions extends Completions {
608
721
  baseURL: this.baseURL,
609
722
  params: body,
610
723
  httpStatus: 200,
611
- usage,
724
+ usage: {
725
+ inputTokens: usage.inputTokens,
726
+ outputTokens: usage.outputTokens,
727
+ reasoningTokens: usage.reasoningTokens,
728
+ cacheReadInputTokens: usage.cacheReadInputTokens,
729
+ webSearchCount: usage.webSearchCount
730
+ },
612
731
  tools: availableTools
613
732
  });
614
733
  } catch (error) {
@@ -644,13 +763,14 @@ class WrappedCompletions extends Completions {
644
763
  if ('choices' in result) {
645
764
  const latency = (Date.now() - startTime) / 1000;
646
765
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
766
+ const formattedOutput = formatResponseOpenAI(result);
647
767
  await sendEventToPosthog({
648
768
  client: this.phClient,
649
769
  ...posthogParams,
650
770
  model: openAIParams.model,
651
771
  provider: 'openai',
652
772
  input: sanitizeOpenAI(openAIParams.messages),
653
- output: formatResponseOpenAI(result),
773
+ output: formattedOutput,
654
774
  latency,
655
775
  baseURL: this.baseURL,
656
776
  params: body,
@@ -659,7 +779,8 @@ class WrappedCompletions extends Completions {
659
779
  inputTokens: result.usage?.prompt_tokens ?? 0,
660
780
  outputTokens: result.usage?.completion_tokens ?? 0,
661
781
  reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
662
- cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
782
+ cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
783
+ webSearchCount: calculateWebSearchCount(result)
663
784
  },
664
785
  tools: availableTools
665
786
  });
@@ -721,14 +842,22 @@ class WrappedResponses extends Responses {
721
842
  let finalContent = [];
722
843
  let usage = {
723
844
  inputTokens: 0,
724
- outputTokens: 0
845
+ outputTokens: 0,
846
+ webSearchCount: 0
725
847
  };
726
848
  for await (const chunk of stream1) {
849
+ if ('response' in chunk && chunk.response) {
850
+ const chunkWebSearchCount = calculateWebSearchCount(chunk.response);
851
+ if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
852
+ usage.webSearchCount = chunkWebSearchCount;
853
+ }
854
+ }
727
855
  if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
728
856
  finalContent = chunk.response.output;
729
857
  }
730
858
  if ('response' in chunk && chunk.response?.usage) {
731
859
  usage = {
860
+ ...usage,
732
861
  inputTokens: chunk.response.usage.input_tokens ?? 0,
733
862
  outputTokens: chunk.response.usage.output_tokens ?? 0,
734
863
  reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
@@ -750,7 +879,13 @@ class WrappedResponses extends Responses {
750
879
  baseURL: this.baseURL,
751
880
  params: body,
752
881
  httpStatus: 200,
753
- usage,
882
+ usage: {
883
+ inputTokens: usage.inputTokens,
884
+ outputTokens: usage.outputTokens,
885
+ reasoningTokens: usage.reasoningTokens,
886
+ cacheReadInputTokens: usage.cacheReadInputTokens,
887
+ webSearchCount: usage.webSearchCount
888
+ },
754
889
  tools: availableTools
755
890
  });
756
891
  } catch (error) {
@@ -785,6 +920,9 @@ class WrappedResponses extends Responses {
785
920
  if ('output' in result) {
786
921
  const latency = (Date.now() - startTime) / 1000;
787
922
  const availableTools = extractAvailableToolCalls('openai', openAIParams);
923
+ const formattedOutput = formatResponseOpenAI({
924
+ output: result.output
925
+ });
788
926
  await sendEventToPosthog({
789
927
  client: this.phClient,
790
928
  ...posthogParams,
@@ -792,9 +930,7 @@ class WrappedResponses extends Responses {
792
930
  model: openAIParams.model,
793
931
  provider: 'openai',
794
932
  input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
795
- output: formatResponseOpenAI({
796
- output: result.output
797
- }),
933
+ output: formattedOutput,
798
934
  latency,
799
935
  baseURL: this.baseURL,
800
936
  params: body,
@@ -803,7 +939,8 @@ class WrappedResponses extends Responses {
803
939
  inputTokens: result.usage?.input_tokens ?? 0,
804
940
  outputTokens: result.usage?.output_tokens ?? 0,
805
941
  reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
806
- cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
942
+ cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
943
+ webSearchCount: calculateWebSearchCount(result)
807
944
  },
808
945
  tools: availableTools
809
946
  });
@@ -841,9 +978,9 @@ class WrappedResponses extends Responses {
841
978
  } = extractPosthogParams(body);
842
979
  const startTime = Date.now();
843
980
  const originalCreate = super.create.bind(this);
844
- const originalSelf = this;
845
- const tempCreate = originalSelf.create;
846
- originalSelf.create = originalCreate;
981
+ const originalSelfRecord = this;
982
+ const tempCreate = originalSelfRecord['create'];
983
+ originalSelfRecord['create'] = originalCreate;
847
984
  try {
848
985
  const parentPromise = super.parse(openAIParams, options);
849
986
  const wrappedPromise = parentPromise.then(async result => {
@@ -892,7 +1029,7 @@ class WrappedResponses extends Responses {
892
1029
  return wrappedPromise;
893
1030
  } finally {
894
1031
  // Restore our wrapped create method
895
- originalSelf.create = tempCreate;
1032
+ originalSelfRecord['create'] = tempCreate;
896
1033
  }
897
1034
  }
898
1035
  }