@posthog/ai 8.6.2 → 8.6.3

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.
@@ -5,6 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var openai = require('openai');
6
6
  var uuid = require('uuid');
7
7
  var core = require('@posthog/core');
8
+ var streaming = require('openai/streaming');
8
9
 
9
10
  // Type guards for safer type checking
10
11
 
@@ -537,7 +538,7 @@ function formatOpenAIResponsesInput(input, instructions) {
537
538
  return messages;
538
539
  }
539
540
 
540
- var version = "8.6.2";
541
+ var version = "8.6.3";
541
542
 
542
543
  const DEFAULT_MAX_DEPTH = 3;
543
544
  const MAX_STACK_LINES = 20;
@@ -648,125 +649,131 @@ const warnIfPostHogAiGateway = baseURL => {
648
649
  * so callers can re-throw the original error reference safely.
649
650
  */
650
651
  const captureAiGeneration$1 = async (client, options) => {
651
- if (!client.capture) {
652
- return;
653
- }
654
- warnIfPostHogAiGateway(options.baseURL);
655
- const traceId = options.traceId ?? uuid.v4();
656
- const eventType = options.eventType ?? AIEvent.Generation;
657
- const privacyMode = options.privacyMode ?? false;
658
- const usage = options.usage ?? {};
659
-
660
- // Check privacy before reading or traversing input/output. Besides avoiding
661
- // needless work, this ensures hostile getters/proxies cannot observe a value
662
- // that the caller explicitly requested us to redact.
663
- const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
664
- const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
665
- const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
666
- let httpStatus = options.httpStatus;
667
- let errorData = {};
668
- if (options.error) {
669
- if (httpStatus === undefined) {
670
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
671
- httpStatus = options.error.status;
672
- } else {
673
- httpStatus = 500;
674
- }
652
+ try {
653
+ if (!client.capture) {
654
+ return;
675
655
  }
676
- let exceptionId;
677
- if (client.options?.enableExceptionAutocapture) {
678
- exceptionId = core.uuidv7();
679
- client.captureException(options.error, undefined, {
680
- $ai_trace_id: traceId
681
- }, exceptionId);
682
- if (typeof options.error === 'object') {
683
- options.error.__posthog_previously_captured_error = true;
656
+ warnIfPostHogAiGateway(options.baseURL);
657
+ const traceId = options.traceId ?? uuid.v4();
658
+ const eventType = options.eventType ?? AIEvent.Generation;
659
+ const privacyMode = options.privacyMode ?? false;
660
+ const usage = options.usage ?? {};
661
+
662
+ // Check privacy before reading or traversing input/output. Besides avoiding
663
+ // needless work, this ensures hostile getters/proxies cannot observe a value
664
+ // that the caller explicitly requested us to redact.
665
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
666
+ const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
667
+ const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
668
+ let httpStatus = options.httpStatus;
669
+ let errorData = {};
670
+ if (options.error) {
671
+ if (httpStatus === undefined) {
672
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
673
+ httpStatus = options.error.status;
674
+ } else {
675
+ httpStatus = 500;
676
+ }
677
+ }
678
+ let exceptionId;
679
+ if (client.options?.enableExceptionAutocapture) {
680
+ exceptionId = core.uuidv7();
681
+ client.captureException(options.error, undefined, {
682
+ $ai_trace_id: traceId
683
+ }, exceptionId);
684
+ if (typeof options.error === 'object') {
685
+ ;
686
+ options.error.__posthog_previously_captured_error = true;
687
+ }
684
688
  }
689
+ errorData = {
690
+ $ai_is_error: true,
691
+ $ai_error: stringifyError(options.error),
692
+ $exception_event_id: exceptionId
693
+ };
694
+ }
695
+ httpStatus = httpStatus ?? 200;
696
+ let costOverrideData = {};
697
+ if (options.costOverride) {
698
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
699
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
700
+ costOverrideData = {
701
+ $ai_input_cost_usd: inputCostUSD,
702
+ $ai_output_cost_usd: outputCostUSD,
703
+ $ai_total_cost_usd: inputCostUSD + outputCostUSD
704
+ };
685
705
  }
686
- errorData = {
687
- $ai_is_error: true,
688
- $ai_error: stringifyError(options.error),
689
- $exception_event_id: exceptionId
706
+ const additionalTokenValues = {
707
+ ...(usage.reasoningTokens ? {
708
+ $ai_reasoning_tokens: usage.reasoningTokens
709
+ } : {}),
710
+ ...(usage.cacheReadInputTokens ? {
711
+ $ai_cache_read_input_tokens: usage.cacheReadInputTokens
712
+ } : {}),
713
+ ...(usage.cacheCreationInputTokens ? {
714
+ $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
715
+ } : {}),
716
+ ...(usage.webSearchCount ? {
717
+ $ai_web_search_count: usage.webSearchCount
718
+ } : {}),
719
+ ...(usage.rawUsage ? {
720
+ $ai_usage: usage.rawUsage
721
+ } : {})
690
722
  };
691
- }
692
- httpStatus = httpStatus ?? 200;
693
- let costOverrideData = {};
694
- if (options.costOverride) {
695
- const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
696
- const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
697
- costOverrideData = {
698
- $ai_input_cost_usd: inputCostUSD,
699
- $ai_output_cost_usd: outputCostUSD,
700
- $ai_total_cost_usd: inputCostUSD + outputCostUSD
723
+ const properties = {
724
+ $ai_lib: 'posthog-ai',
725
+ $ai_lib_version: version,
726
+ $ai_provider: options.providerOverride ?? options.provider,
727
+ $ai_model: options.modelOverride ?? options.model,
728
+ $ai_model_parameters: options.modelParameters ?? {},
729
+ $ai_input: safeInput,
730
+ $ai_output_choices: safeOutput,
731
+ $ai_http_status: httpStatus,
732
+ $ai_input_tokens: usage.inputTokens ?? 0,
733
+ ...(usage.outputTokens !== undefined ? {
734
+ $ai_output_tokens: usage.outputTokens
735
+ } : {}),
736
+ ...additionalTokenValues,
737
+ $ai_latency: options.latency ?? 0,
738
+ ...(options.timeToFirstToken !== undefined ? {
739
+ $ai_time_to_first_token: options.timeToFirstToken
740
+ } : {}),
741
+ $ai_trace_id: traceId,
742
+ $ai_base_url: options.baseURL ?? '',
743
+ ...options.properties,
744
+ $ai_tokens_source: getTokensSource(options.properties),
745
+ ...(options.distinctId ? {} : {
746
+ $process_person_profile: false
747
+ }),
748
+ ...(options.stopReason ? {
749
+ $ai_stop_reason: options.stopReason
750
+ } : {}),
751
+ ...(options.tools ? {
752
+ $ai_tools: options.tools
753
+ } : {}),
754
+ ...(options.completionId ? {
755
+ $ai_completion_id: options.completionId
756
+ } : {}),
757
+ ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
758
+ $ai_provider_metadata: options.providerMetadata
759
+ } : {}),
760
+ ...errorData,
761
+ ...costOverrideData
701
762
  };
702
- }
703
- const additionalTokenValues = {
704
- ...(usage.reasoningTokens ? {
705
- $ai_reasoning_tokens: usage.reasoningTokens
706
- } : {}),
707
- ...(usage.cacheReadInputTokens ? {
708
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
709
- } : {}),
710
- ...(usage.cacheCreationInputTokens ? {
711
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
712
- } : {}),
713
- ...(usage.webSearchCount ? {
714
- $ai_web_search_count: usage.webSearchCount
715
- } : {}),
716
- ...(usage.rawUsage ? {
717
- $ai_usage: usage.rawUsage
718
- } : {})
719
- };
720
- const properties = {
721
- $ai_lib: 'posthog-ai',
722
- $ai_lib_version: version,
723
- $ai_provider: options.providerOverride ?? options.provider,
724
- $ai_model: options.modelOverride ?? options.model,
725
- $ai_model_parameters: options.modelParameters ?? {},
726
- $ai_input: safeInput,
727
- $ai_output_choices: safeOutput,
728
- $ai_http_status: httpStatus,
729
- $ai_input_tokens: usage.inputTokens ?? 0,
730
- ...(usage.outputTokens !== undefined ? {
731
- $ai_output_tokens: usage.outputTokens
732
- } : {}),
733
- ...additionalTokenValues,
734
- $ai_latency: options.latency ?? 0,
735
- ...(options.timeToFirstToken !== undefined ? {
736
- $ai_time_to_first_token: options.timeToFirstToken
737
- } : {}),
738
- $ai_trace_id: traceId,
739
- $ai_base_url: options.baseURL ?? '',
740
- ...options.properties,
741
- $ai_tokens_source: getTokensSource(options.properties),
742
- ...(options.distinctId ? {} : {
743
- $process_person_profile: false
744
- }),
745
- ...(options.stopReason ? {
746
- $ai_stop_reason: options.stopReason
747
- } : {}),
748
- ...(options.tools ? {
749
- $ai_tools: options.tools
750
- } : {}),
751
- ...(options.completionId ? {
752
- $ai_completion_id: options.completionId
753
- } : {}),
754
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
755
- $ai_provider_metadata: options.providerMetadata
756
- } : {}),
757
- ...errorData,
758
- ...costOverrideData
759
- };
760
- const event = {
761
- distinctId: options.distinctId ?? traceId,
762
- event: eventType,
763
- properties,
764
- groups: options.groups
765
- };
766
- if (options.captureImmediate) {
767
- await client.captureImmediate(event);
768
- } else {
769
- client.capture(event);
763
+ const event = {
764
+ distinctId: options.distinctId ?? traceId,
765
+ event: eventType,
766
+ properties,
767
+ groups: options.groups
768
+ };
769
+ if (options.captureImmediate) {
770
+ await client.captureImmediate(event);
771
+ } else {
772
+ client.capture(event);
773
+ }
774
+ } catch (error) {
775
+ // Telemetry failures must never affect the instrumented provider call.
776
+ console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
770
777
  }
771
778
  };
772
779
 
@@ -843,6 +850,220 @@ function buildProviderMetadata(fields) {
843
850
  return Object.keys(metadata).length > 0 ? metadata : undefined;
844
851
  }
845
852
 
853
+ /**
854
+ * Splits an SDK stream into a monitoring branch and a caller branch without
855
+ * allowing either branch to read ahead of the other. Unlike the SDKs' `tee()`
856
+ * implementations, this keeps at most one result in flight and makes caller
857
+ * cancellation terminate the monitoring branch and the source iterator.
858
+ */
859
+ function monitoredStreamTee(source, createStream) {
860
+ const controller = source.controller ?? new AbortController();
861
+ const sourceIterator = source[Symbol.asyncIterator]();
862
+ const callerQueue = [];
863
+ let monitorPending;
864
+ let monitorActive = true;
865
+ let operationInFlight = false;
866
+ let terminalResult;
867
+ let bufferedMonitorResult;
868
+ let terminalError;
869
+ let hasTerminalError = false;
870
+ let cancellationPromise;
871
+ let abortListener;
872
+ const removeAbortListener = () => {
873
+ if (abortListener) {
874
+ controller.signal.removeEventListener('abort', abortListener);
875
+ abortListener = undefined;
876
+ }
877
+ };
878
+ const settleMonitorTerminal = () => {
879
+ if (!monitorPending) {
880
+ return;
881
+ }
882
+ const pending = monitorPending;
883
+ monitorPending = undefined;
884
+ if (hasTerminalError) {
885
+ pending.reject(terminalError);
886
+ } else if (terminalResult) {
887
+ pending.resolve(terminalResult);
888
+ }
889
+ };
890
+ const settleCallersTerminal = () => {
891
+ while (callerQueue.length > 0) {
892
+ const pending = callerQueue.shift();
893
+ if (hasTerminalError) {
894
+ pending.reject(terminalError);
895
+ } else if (terminalResult) {
896
+ pending.resolve(terminalResult);
897
+ }
898
+ }
899
+ };
900
+ const pump = () => {
901
+ if (operationInFlight || callerQueue.length === 0 || monitorActive && !monitorPending) {
902
+ return;
903
+ }
904
+ const pendingCaller = callerQueue.shift();
905
+ const pendingMonitor = monitorPending;
906
+ monitorPending = undefined;
907
+ operationInFlight = true;
908
+ void sourceIterator.next().then(result => {
909
+ operationInFlight = false;
910
+ if (result.done) {
911
+ terminalResult = result;
912
+ removeAbortListener();
913
+ }
914
+ pendingCaller.resolve(result);
915
+ pendingMonitor?.resolve(result);
916
+ if (result.done) {
917
+ settleCallersTerminal();
918
+ } else {
919
+ pump();
920
+ }
921
+ }, error => {
922
+ operationInFlight = false;
923
+ terminalError = error;
924
+ hasTerminalError = true;
925
+ removeAbortListener();
926
+ pendingCaller.reject(error);
927
+ pendingMonitor?.reject(error);
928
+ settleCallersTerminal();
929
+ });
930
+ };
931
+ const monitoringStream = {
932
+ [Symbol.asyncIterator]() {
933
+ return {
934
+ next: () => {
935
+ if (hasTerminalError) {
936
+ return Promise.reject(terminalError);
937
+ }
938
+ if (terminalResult) {
939
+ return Promise.resolve(terminalResult);
940
+ }
941
+ if (bufferedMonitorResult) {
942
+ const result = bufferedMonitorResult;
943
+ bufferedMonitorResult = undefined;
944
+ return Promise.resolve(result);
945
+ }
946
+ return new Promise((resolve, reject) => {
947
+ monitorPending = {
948
+ resolve,
949
+ reject
950
+ };
951
+ pump();
952
+ });
953
+ },
954
+ return: async value => {
955
+ monitorActive = false;
956
+ monitorPending = undefined;
957
+ pump();
958
+ return {
959
+ done: true,
960
+ value: value
961
+ };
962
+ }
963
+ };
964
+ }
965
+ };
966
+ const cancelSource = value => {
967
+ if (cancellationPromise) {
968
+ return cancellationPromise;
969
+ }
970
+ removeAbortListener();
971
+ if (!controller.signal.aborted) {
972
+ controller.abort();
973
+ }
974
+ cancellationPromise = (async () => {
975
+ try {
976
+ const defaultResult = {
977
+ done: true,
978
+ value
979
+ };
980
+ const result = sourceIterator.return ? await sourceIterator.return(value) : defaultResult;
981
+ if (result.done) {
982
+ terminalResult = result;
983
+ removeAbortListener();
984
+ settleMonitorTerminal();
985
+ settleCallersTerminal();
986
+ } else if (monitorPending) {
987
+ monitorPending.resolve(result);
988
+ monitorPending = undefined;
989
+ cancellationPromise = undefined;
990
+ } else {
991
+ bufferedMonitorResult = result;
992
+ cancellationPromise = undefined;
993
+ }
994
+ return result;
995
+ } catch (error) {
996
+ terminalError = error;
997
+ hasTerminalError = true;
998
+ removeAbortListener();
999
+ settleMonitorTerminal();
1000
+ settleCallersTerminal();
1001
+ throw error;
1002
+ }
1003
+ })();
1004
+ // An AbortController cancellation has no caller awaiting this promise.
1005
+ void cancellationPromise.catch(() => undefined);
1006
+ return cancellationPromise;
1007
+ };
1008
+ abortListener = () => {
1009
+ void cancelSource();
1010
+ };
1011
+ if (controller.signal.aborted) {
1012
+ abortListener();
1013
+ } else {
1014
+ controller.signal.addEventListener('abort', abortListener, {
1015
+ once: true
1016
+ });
1017
+ }
1018
+ const callerStream = createStream(() => ({
1019
+ next: () => {
1020
+ if (hasTerminalError) {
1021
+ return Promise.reject(terminalError);
1022
+ }
1023
+ if (terminalResult) {
1024
+ return Promise.resolve(terminalResult);
1025
+ }
1026
+ return new Promise((resolve, reject) => {
1027
+ callerQueue.push({
1028
+ resolve,
1029
+ reject
1030
+ });
1031
+ pump();
1032
+ });
1033
+ },
1034
+ return: value => cancelSource(value),
1035
+ throw: async error => {
1036
+ if (!sourceIterator.throw) {
1037
+ await cancelSource();
1038
+ throw error;
1039
+ }
1040
+ try {
1041
+ const result = await sourceIterator.throw(error);
1042
+ if (result.done) {
1043
+ terminalResult = result;
1044
+ removeAbortListener();
1045
+ settleCallersTerminal();
1046
+ }
1047
+ if (monitorPending) {
1048
+ monitorPending.resolve(result);
1049
+ monitorPending = undefined;
1050
+ } else {
1051
+ bufferedMonitorResult = result;
1052
+ }
1053
+ return result;
1054
+ } catch (sourceError) {
1055
+ terminalError = sourceError;
1056
+ hasTerminalError = true;
1057
+ removeAbortListener();
1058
+ settleMonitorTerminal();
1059
+ settleCallersTerminal();
1060
+ throw sourceError;
1061
+ }
1062
+ }
1063
+ }), controller);
1064
+ return [monitoringStream, callerStream];
1065
+ }
1066
+
846
1067
  class PostHogAzureOpenAI extends openai.AzureOpenAI {
847
1068
  constructor(config) {
848
1069
  const {
@@ -885,8 +1106,8 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
885
1106
  const parentPromise = super.create(openAIParams, options);
886
1107
  if (openAIParams.stream) {
887
1108
  return parentPromise.then(value => {
888
- if ('tee' in value) {
889
- const [stream1, stream2] = value.tee();
1109
+ if (Symbol.asyncIterator in value) {
1110
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
890
1111
  (async () => {
891
1112
  // Hoisted so the catch block can surface whatever was accumulated
892
1113
  // from the streamed chunks before the failure.
@@ -1137,8 +1358,8 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1137
1358
  const parentPromise = super.create(openAIParams, options);
1138
1359
  if (openAIParams.stream) {
1139
1360
  return parentPromise.then(value => {
1140
- if ('tee' in value && typeof value.tee === 'function') {
1141
- const [stream1, stream2] = value.tee();
1361
+ if (Symbol.asyncIterator in value) {
1362
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
1142
1363
  (async () => {
1143
1364
  // Hoisted so the catch block can surface the completion ID that
1144
1365
  // was accumulated from the streamed chunks before the failure.
@@ -1172,12 +1393,12 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1172
1393
  if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
1173
1394
  finalContent = chunk.response.output;
1174
1395
  }
1175
- if ('usage' in chunk && chunk.usage) {
1396
+ if ('response' in chunk && chunk.response?.usage) {
1176
1397
  usage = {
1177
- inputTokens: chunk.usage.input_tokens ?? 0,
1178
- outputTokens: chunk.usage.output_tokens ?? 0,
1179
- reasoningTokens: chunk.usage.output_tokens_details?.reasoning_tokens ?? 0,
1180
- cacheReadInputTokens: chunk.usage.input_tokens_details?.cached_tokens ?? 0
1398
+ inputTokens: chunk.response.usage.input_tokens ?? 0,
1399
+ outputTokens: chunk.response.usage.output_tokens ?? 0,
1400
+ reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
1401
+ cacheReadInputTokens: chunk.response.usage.input_tokens_details?.cached_tokens ?? 0
1181
1402
  };
1182
1403
  }
1183
1404
  }
@@ -1468,8 +1689,8 @@ class WrappedCompletions extends Completions {
1468
1689
  const parentPromise = super.create(openAIParams, options);
1469
1690
  if (openAIParams.stream) {
1470
1691
  const wrappedPromise = parentPromise.then(value => {
1471
- if ('tee' in value) {
1472
- const [stream1, stream2] = value.tee();
1692
+ if (Symbol.asyncIterator in value) {
1693
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
1473
1694
  (async () => {
1474
1695
  // Hoisted so the catch block can surface whatever was accumulated
1475
1696
  // from the streamed chunks before the failure.
@@ -1749,8 +1970,8 @@ class WrappedResponses extends Responses {
1749
1970
  const parentPromise = super.create(openAIParams, options);
1750
1971
  if (openAIParams.stream) {
1751
1972
  const wrappedPromise = parentPromise.then(value => {
1752
- if ('tee' in value && typeof value.tee === 'function') {
1753
- const [stream1, stream2] = value.tee();
1973
+ if (Symbol.asyncIterator in value) {
1974
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
1754
1975
  (async () => {
1755
1976
  // Hoisted so the catch block can surface the completion ID that
1756
1977
  // was accumulated from the streamed chunks before the failure.
@@ -2082,8 +2303,8 @@ class WrappedTranscriptions extends Transcriptions {
2082
2303
  const parentPromise = openAIParams.stream ? super.create(openAIParams, options) : super.create(openAIParams, options);
2083
2304
  if (openAIParams.stream) {
2084
2305
  const wrappedPromise = parentPromise.then(value => {
2085
- if ('tee' in value && typeof value.tee === 'function') {
2086
- const [stream1, stream2] = value.tee();
2306
+ if (Symbol.asyncIterator in value) {
2307
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
2087
2308
  (async () => {
2088
2309
  try {
2089
2310
  let finalContent = '';