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