@posthog/ai 8.6.1 → 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.1";
541
+ var version = "8.6.3";
541
542
 
542
543
  const DEFAULT_MAX_DEPTH = 3;
543
544
  const MAX_STACK_LINES = 20;
@@ -648,120 +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
- const safeInput = sanitizeValues(options.input);
660
- const safeOutput = sanitizeValues(options.output);
661
- let httpStatus = options.httpStatus;
662
- let errorData = {};
663
- if (options.error) {
664
- if (httpStatus === undefined) {
665
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
666
- httpStatus = options.error.status;
667
- } else {
668
- httpStatus = 500;
669
- }
652
+ try {
653
+ if (!client.capture) {
654
+ return;
670
655
  }
671
- let exceptionId;
672
- if (client.options?.enableExceptionAutocapture) {
673
- exceptionId = core.uuidv7();
674
- client.captureException(options.error, undefined, {
675
- $ai_trace_id: traceId
676
- }, exceptionId);
677
- if (typeof options.error === 'object') {
678
- 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
+ }
679
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
+ };
680
705
  }
681
- errorData = {
682
- $ai_is_error: true,
683
- $ai_error: stringifyError(options.error),
684
- $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
+ } : {})
685
722
  };
686
- }
687
- httpStatus = httpStatus ?? 200;
688
- let costOverrideData = {};
689
- if (options.costOverride) {
690
- const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
691
- const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
692
- costOverrideData = {
693
- $ai_input_cost_usd: inputCostUSD,
694
- $ai_output_cost_usd: outputCostUSD,
695
- $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
696
762
  };
697
- }
698
- const additionalTokenValues = {
699
- ...(usage.reasoningTokens ? {
700
- $ai_reasoning_tokens: usage.reasoningTokens
701
- } : {}),
702
- ...(usage.cacheReadInputTokens ? {
703
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
704
- } : {}),
705
- ...(usage.cacheCreationInputTokens ? {
706
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
707
- } : {}),
708
- ...(usage.webSearchCount ? {
709
- $ai_web_search_count: usage.webSearchCount
710
- } : {}),
711
- ...(usage.rawUsage ? {
712
- $ai_usage: usage.rawUsage
713
- } : {})
714
- };
715
- const properties = {
716
- $ai_lib: 'posthog-ai',
717
- $ai_lib_version: version,
718
- $ai_provider: options.providerOverride ?? options.provider,
719
- $ai_model: options.modelOverride ?? options.model,
720
- $ai_model_parameters: options.modelParameters ?? {},
721
- $ai_input: withPrivacyMode(client, privacyMode, safeInput),
722
- $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
723
- $ai_http_status: httpStatus,
724
- $ai_input_tokens: usage.inputTokens ?? 0,
725
- ...(usage.outputTokens !== undefined ? {
726
- $ai_output_tokens: usage.outputTokens
727
- } : {}),
728
- ...additionalTokenValues,
729
- $ai_latency: options.latency ?? 0,
730
- ...(options.timeToFirstToken !== undefined ? {
731
- $ai_time_to_first_token: options.timeToFirstToken
732
- } : {}),
733
- $ai_trace_id: traceId,
734
- $ai_base_url: options.baseURL ?? '',
735
- ...options.properties,
736
- $ai_tokens_source: getTokensSource(options.properties),
737
- ...(options.distinctId ? {} : {
738
- $process_person_profile: false
739
- }),
740
- ...(options.stopReason ? {
741
- $ai_stop_reason: options.stopReason
742
- } : {}),
743
- ...(options.tools ? {
744
- $ai_tools: options.tools
745
- } : {}),
746
- ...(options.completionId ? {
747
- $ai_completion_id: options.completionId
748
- } : {}),
749
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
750
- $ai_provider_metadata: options.providerMetadata
751
- } : {}),
752
- ...errorData,
753
- ...costOverrideData
754
- };
755
- const event = {
756
- distinctId: options.distinctId ?? traceId,
757
- event: eventType,
758
- properties,
759
- groups: options.groups
760
- };
761
- if (options.captureImmediate) {
762
- await client.captureImmediate(event);
763
- } else {
764
- 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);
765
777
  }
766
778
  };
767
779
 
@@ -838,6 +850,220 @@ function buildProviderMetadata(fields) {
838
850
  return Object.keys(metadata).length > 0 ? metadata : undefined;
839
851
  }
840
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
+
841
1067
  class PostHogAzureOpenAI extends openai.AzureOpenAI {
842
1068
  constructor(config) {
843
1069
  const {
@@ -880,8 +1106,8 @@ let WrappedCompletions$1 = class WrappedCompletions extends openai.AzureOpenAI.C
880
1106
  const parentPromise = super.create(openAIParams, options);
881
1107
  if (openAIParams.stream) {
882
1108
  return parentPromise.then(value => {
883
- if ('tee' in value) {
884
- const [stream1, stream2] = value.tee();
1109
+ if (Symbol.asyncIterator in value) {
1110
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
885
1111
  (async () => {
886
1112
  // Hoisted so the catch block can surface whatever was accumulated
887
1113
  // from the streamed chunks before the failure.
@@ -1132,8 +1358,8 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1132
1358
  const parentPromise = super.create(openAIParams, options);
1133
1359
  if (openAIParams.stream) {
1134
1360
  return parentPromise.then(value => {
1135
- if ('tee' in value && typeof value.tee === 'function') {
1136
- const [stream1, stream2] = value.tee();
1361
+ if (Symbol.asyncIterator in value) {
1362
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
1137
1363
  (async () => {
1138
1364
  // Hoisted so the catch block can surface the completion ID that
1139
1365
  // was accumulated from the streamed chunks before the failure.
@@ -1167,12 +1393,12 @@ let WrappedResponses$1 = class WrappedResponses extends openai.AzureOpenAI.Respo
1167
1393
  if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
1168
1394
  finalContent = chunk.response.output;
1169
1395
  }
1170
- if ('usage' in chunk && chunk.usage) {
1396
+ if ('response' in chunk && chunk.response?.usage) {
1171
1397
  usage = {
1172
- inputTokens: chunk.usage.input_tokens ?? 0,
1173
- outputTokens: chunk.usage.output_tokens ?? 0,
1174
- reasoningTokens: chunk.usage.output_tokens_details?.reasoning_tokens ?? 0,
1175
- 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
1176
1402
  };
1177
1403
  }
1178
1404
  }
@@ -1463,8 +1689,8 @@ class WrappedCompletions extends Completions {
1463
1689
  const parentPromise = super.create(openAIParams, options);
1464
1690
  if (openAIParams.stream) {
1465
1691
  const wrappedPromise = parentPromise.then(value => {
1466
- if ('tee' in value) {
1467
- const [stream1, stream2] = value.tee();
1692
+ if (Symbol.asyncIterator in value) {
1693
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
1468
1694
  (async () => {
1469
1695
  // Hoisted so the catch block can surface whatever was accumulated
1470
1696
  // from the streamed chunks before the failure.
@@ -1744,8 +1970,8 @@ class WrappedResponses extends Responses {
1744
1970
  const parentPromise = super.create(openAIParams, options);
1745
1971
  if (openAIParams.stream) {
1746
1972
  const wrappedPromise = parentPromise.then(value => {
1747
- if ('tee' in value && typeof value.tee === 'function') {
1748
- const [stream1, stream2] = value.tee();
1973
+ if (Symbol.asyncIterator in value) {
1974
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
1749
1975
  (async () => {
1750
1976
  // Hoisted so the catch block can surface the completion ID that
1751
1977
  // was accumulated from the streamed chunks before the failure.
@@ -2077,8 +2303,8 @@ class WrappedTranscriptions extends Transcriptions {
2077
2303
  const parentPromise = openAIParams.stream ? super.create(openAIParams, options) : super.create(openAIParams, options);
2078
2304
  if (openAIParams.stream) {
2079
2305
  const wrappedPromise = parentPromise.then(value => {
2080
- if ('tee' in value && typeof value.tee === 'function') {
2081
- const [stream1, stream2] = value.tee();
2306
+ if (Symbol.asyncIterator in value) {
2307
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
2082
2308
  (async () => {
2083
2309
  try {
2084
2310
  let finalContent = '';