@posthog/ai 8.6.2 → 8.6.4

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 AnthropicOriginal = require('@anthropic-ai/sdk');
6
6
  var uuid = require('uuid');
7
7
  var core = require('@posthog/core');
8
+ var streaming = require('@anthropic-ai/sdk/streaming');
8
9
 
9
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
11
 
@@ -299,7 +300,7 @@ function addDefaults(params) {
299
300
  };
300
301
  }
301
302
 
302
- var version = "8.6.2";
303
+ var version = "8.6.4";
303
304
 
304
305
  const DEFAULT_MAX_DEPTH = 3;
305
306
  const MAX_STACK_LINES = 20;
@@ -410,127 +411,420 @@ const warnIfPostHogAiGateway = baseURL => {
410
411
  * so callers can re-throw the original error reference safely.
411
412
  */
412
413
  const captureAiGeneration = async (client, options) => {
413
- if (!client.capture) {
414
- return;
415
- }
416
- warnIfPostHogAiGateway(options.baseURL);
417
- const traceId = options.traceId ?? uuid.v4();
418
- const eventType = options.eventType ?? AIEvent.Generation;
419
- const privacyMode = options.privacyMode ?? false;
420
- const usage = options.usage ?? {};
421
-
422
- // Check privacy before reading or traversing input/output. Besides avoiding
423
- // needless work, this ensures hostile getters/proxies cannot observe a value
424
- // that the caller explicitly requested us to redact.
425
- const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
426
- const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
427
- const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
428
- let httpStatus = options.httpStatus;
429
- let errorData = {};
430
- if (options.error) {
431
- if (httpStatus === undefined) {
432
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
433
- httpStatus = options.error.status;
434
- } else {
435
- httpStatus = 500;
436
- }
414
+ try {
415
+ if (!client.capture) {
416
+ return;
437
417
  }
438
- let exceptionId;
439
- if (client.options?.enableExceptionAutocapture) {
440
- exceptionId = core.uuidv7();
441
- client.captureException(options.error, undefined, {
442
- $ai_trace_id: traceId
443
- }, exceptionId);
444
- if (typeof options.error === 'object') {
445
- options.error.__posthog_previously_captured_error = true;
418
+ warnIfPostHogAiGateway(options.baseURL);
419
+ const traceId = options.traceId ?? uuid.v4();
420
+ const eventType = options.eventType ?? AIEvent.Generation;
421
+ const privacyMode = options.privacyMode ?? false;
422
+ const usage = options.usage ?? {};
423
+
424
+ // Check privacy before reading or traversing input/output. Besides avoiding
425
+ // needless work, this ensures hostile getters/proxies cannot observe a value
426
+ // that the caller explicitly requested us to redact.
427
+ const shouldRedact = withPrivacyMode(client, privacyMode, false) === null;
428
+ const safeInput = shouldRedact ? null : core.toJsonSafeValue(options.input);
429
+ const safeOutput = shouldRedact ? null : core.toJsonSafeValue(options.output);
430
+ let httpStatus = options.httpStatus;
431
+ let errorData = {};
432
+ if (options.error) {
433
+ if (httpStatus === undefined) {
434
+ if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
435
+ httpStatus = options.error.status;
436
+ } else if (typeof options.error === 'object' && 'statusCode' in options.error && typeof options.error.statusCode === 'number') {
437
+ httpStatus = options.error.statusCode;
438
+ } else {
439
+ httpStatus = 500;
440
+ }
441
+ }
442
+ let exceptionId;
443
+ if (client.options?.enableExceptionAutocapture) {
444
+ exceptionId = core.uuidv7();
445
+ client.captureException(options.error, undefined, {
446
+ $ai_trace_id: traceId
447
+ }, exceptionId);
448
+ if (typeof options.error === 'object') {
449
+ ;
450
+ options.error.__posthog_previously_captured_error = true;
451
+ }
446
452
  }
453
+ errorData = {
454
+ $ai_is_error: true,
455
+ $ai_error: stringifyError(options.error),
456
+ $exception_event_id: exceptionId
457
+ };
447
458
  }
448
- errorData = {
449
- $ai_is_error: true,
450
- $ai_error: stringifyError(options.error),
451
- $exception_event_id: exceptionId
459
+ httpStatus = httpStatus ?? 200;
460
+ let costOverrideData = {};
461
+ if (options.costOverride) {
462
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
463
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
464
+ costOverrideData = {
465
+ $ai_input_cost_usd: inputCostUSD,
466
+ $ai_output_cost_usd: outputCostUSD,
467
+ $ai_total_cost_usd: inputCostUSD + outputCostUSD
468
+ };
469
+ }
470
+ const additionalTokenValues = {
471
+ ...(usage.reasoningTokens ? {
472
+ $ai_reasoning_tokens: usage.reasoningTokens
473
+ } : {}),
474
+ ...(usage.cacheReadInputTokens ? {
475
+ $ai_cache_read_input_tokens: usage.cacheReadInputTokens
476
+ } : {}),
477
+ ...(usage.cacheCreationInputTokens ? {
478
+ $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
479
+ } : {}),
480
+ ...(usage.webSearchCount ? {
481
+ $ai_web_search_count: usage.webSearchCount
482
+ } : {}),
483
+ ...(usage.rawUsage ? {
484
+ $ai_usage: usage.rawUsage
485
+ } : {})
486
+ };
487
+ const properties = {
488
+ $ai_lib: 'posthog-ai',
489
+ $ai_lib_version: version,
490
+ $ai_provider: options.providerOverride ?? options.provider,
491
+ $ai_model: options.modelOverride ?? options.model,
492
+ $ai_model_parameters: options.modelParameters ?? {},
493
+ $ai_input: safeInput,
494
+ $ai_output_choices: safeOutput,
495
+ $ai_http_status: httpStatus,
496
+ $ai_input_tokens: usage.inputTokens ?? 0,
497
+ ...(usage.outputTokens !== undefined ? {
498
+ $ai_output_tokens: usage.outputTokens
499
+ } : {}),
500
+ ...additionalTokenValues,
501
+ $ai_latency: options.latency ?? 0,
502
+ ...(options.timeToFirstToken !== undefined ? {
503
+ $ai_time_to_first_token: options.timeToFirstToken
504
+ } : {}),
505
+ $ai_trace_id: traceId,
506
+ $ai_base_url: options.baseURL ?? '',
507
+ ...options.properties,
508
+ $ai_tokens_source: getTokensSource(options.properties),
509
+ ...(options.distinctId ? {} : {
510
+ $process_person_profile: false
511
+ }),
512
+ ...(options.stopReason ? {
513
+ $ai_stop_reason: options.stopReason
514
+ } : {}),
515
+ ...(options.tools ? {
516
+ $ai_tools: options.tools
517
+ } : {}),
518
+ ...(options.completionId ? {
519
+ $ai_completion_id: options.completionId
520
+ } : {}),
521
+ ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
522
+ $ai_provider_metadata: options.providerMetadata
523
+ } : {}),
524
+ ...errorData,
525
+ ...costOverrideData
526
+ };
527
+ const event = {
528
+ distinctId: options.distinctId ?? traceId,
529
+ event: eventType,
530
+ properties,
531
+ groups: options.groups
532
+ };
533
+ if (options.captureImmediate) {
534
+ await client.captureImmediate(event);
535
+ } else {
536
+ client.capture(event);
537
+ }
538
+ } catch (error) {
539
+ // Telemetry failures must never affect the instrumented provider call.
540
+ console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
541
+ }
542
+ };
543
+
544
+ function addRequestId(result, response, requestIdHeader) {
545
+ if (!result || typeof result !== 'object' || Array.isArray(result)) {
546
+ return result;
547
+ }
548
+ return Object.defineProperty(result, '_request_id', {
549
+ value: response.headers.get(requestIdHeader),
550
+ enumerable: false
551
+ });
552
+ }
553
+ function getResponsePropsPromise(parentPromise) {
554
+ const responsePromise = parentPromise.responsePromise;
555
+ if (!responsePromise || typeof responsePromise.then !== 'function') {
556
+ return undefined;
557
+ }
558
+ return responsePromise;
559
+ }
560
+ function decorateProviderPromise(wrappedPromise, responsePropsPromise, requestIdHeader, preserveThenUnwrap) {
561
+ const providerPromise = wrappedPromise;
562
+ if (responsePropsPromise) {
563
+ providerPromise.asResponse = async () => (await responsePropsPromise).response;
564
+ providerPromise.withResponse = async () => {
565
+ const [props, data] = await Promise.all([responsePropsPromise, wrappedPromise]);
566
+ return {
567
+ response: props.response,
568
+ data,
569
+ request_id: props.response.headers.get(requestIdHeader)
570
+ };
452
571
  };
453
572
  }
454
- httpStatus = httpStatus ?? 200;
455
- let costOverrideData = {};
456
- if (options.costOverride) {
457
- const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
458
- const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
459
- costOverrideData = {
460
- $ai_input_cost_usd: inputCostUSD,
461
- $ai_output_cost_usd: outputCostUSD,
462
- $ai_total_cost_usd: inputCostUSD + outputCostUSD
573
+ if (preserveThenUnwrap) {
574
+ providerPromise._thenUnwrap = transform => {
575
+ if (!responsePropsPromise) {
576
+ throw new Error('The provider promise response metadata is unavailable');
577
+ }
578
+ const transformedPromise = Promise.all([wrappedPromise, responsePropsPromise]).then(([data, props]) => addRequestId(transform(data, props), props.response, requestIdHeader));
579
+ return decorateProviderPromise(transformedPromise, responsePropsPromise, requestIdHeader, true);
463
580
  };
464
581
  }
465
- const additionalTokenValues = {
466
- ...(usage.reasoningTokens ? {
467
- $ai_reasoning_tokens: usage.reasoningTokens
468
- } : {}),
469
- ...(usage.cacheReadInputTokens ? {
470
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
471
- } : {}),
472
- ...(usage.cacheCreationInputTokens ? {
473
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
474
- } : {}),
475
- ...(usage.webSearchCount ? {
476
- $ai_web_search_count: usage.webSearchCount
477
- } : {}),
478
- ...(usage.rawUsage ? {
479
- $ai_usage: usage.rawUsage
480
- } : {})
582
+ return providerPromise;
583
+ }
584
+
585
+ /**
586
+ * Keep the provider SDK helpers on a promise whose resolved value is instrumented.
587
+ * OpenAI's parse helpers compose create calls through `_thenUnwrap`, while both
588
+ * OpenAI and Anthropic expose the raw response through `asResponse` and
589
+ * `withResponse`.
590
+ */
591
+
592
+ function preserveProviderPromise(parentPromise, wrappedPromise, options = {}) {
593
+ const responsePropsPromise = getResponsePropsPromise(parentPromise);
594
+ const preserveThenUnwrap = typeof parentPromise._thenUnwrap === 'function';
595
+ const providerPromise = decorateProviderPromise(wrappedPromise, responsePropsPromise, options.requestIdHeader ?? 'x-request-id', preserveThenUnwrap);
596
+ if (!responsePropsPromise) {
597
+ const asResponse = parentPromise.asResponse?.bind(parentPromise);
598
+ if (asResponse) {
599
+ providerPromise.asResponse = asResponse;
600
+ }
601
+ const withResponse = parentPromise.withResponse?.bind(parentPromise);
602
+ if (withResponse) {
603
+ providerPromise.withResponse = async () => {
604
+ const [response, data] = await Promise.all([withResponse(), wrappedPromise]);
605
+ return {
606
+ ...response,
607
+ data
608
+ };
609
+ };
610
+ }
611
+ }
612
+ return providerPromise;
613
+ }
614
+
615
+ /**
616
+ * Splits an SDK stream into a monitoring branch and a caller branch without
617
+ * allowing either branch to read ahead of the other. Unlike the SDKs' `tee()`
618
+ * implementations, this keeps at most one result in flight and makes caller
619
+ * cancellation terminate the monitoring branch and the source iterator.
620
+ */
621
+ function monitoredStreamTee(source, createStream) {
622
+ const controller = source.controller ?? new AbortController();
623
+ const sourceIterator = source[Symbol.asyncIterator]();
624
+ const callerQueue = [];
625
+ let monitorPending;
626
+ let monitorActive = true;
627
+ let operationInFlight = false;
628
+ let terminalResult;
629
+ let bufferedMonitorResult;
630
+ let terminalError;
631
+ let hasTerminalError = false;
632
+ let cancellationPromise;
633
+ let abortListener;
634
+ const removeAbortListener = () => {
635
+ if (abortListener) {
636
+ controller.signal.removeEventListener('abort', abortListener);
637
+ abortListener = undefined;
638
+ }
639
+ };
640
+ const settleMonitorTerminal = () => {
641
+ if (!monitorPending) {
642
+ return;
643
+ }
644
+ const pending = monitorPending;
645
+ monitorPending = undefined;
646
+ if (hasTerminalError) {
647
+ pending.reject(terminalError);
648
+ } else if (terminalResult) {
649
+ pending.resolve(terminalResult);
650
+ }
481
651
  };
482
- const properties = {
483
- $ai_lib: 'posthog-ai',
484
- $ai_lib_version: version,
485
- $ai_provider: options.providerOverride ?? options.provider,
486
- $ai_model: options.modelOverride ?? options.model,
487
- $ai_model_parameters: options.modelParameters ?? {},
488
- $ai_input: safeInput,
489
- $ai_output_choices: safeOutput,
490
- $ai_http_status: httpStatus,
491
- $ai_input_tokens: usage.inputTokens ?? 0,
492
- ...(usage.outputTokens !== undefined ? {
493
- $ai_output_tokens: usage.outputTokens
494
- } : {}),
495
- ...additionalTokenValues,
496
- $ai_latency: options.latency ?? 0,
497
- ...(options.timeToFirstToken !== undefined ? {
498
- $ai_time_to_first_token: options.timeToFirstToken
499
- } : {}),
500
- $ai_trace_id: traceId,
501
- $ai_base_url: options.baseURL ?? '',
502
- ...options.properties,
503
- $ai_tokens_source: getTokensSource(options.properties),
504
- ...(options.distinctId ? {} : {
505
- $process_person_profile: false
506
- }),
507
- ...(options.stopReason ? {
508
- $ai_stop_reason: options.stopReason
509
- } : {}),
510
- ...(options.tools ? {
511
- $ai_tools: options.tools
512
- } : {}),
513
- ...(options.completionId ? {
514
- $ai_completion_id: options.completionId
515
- } : {}),
516
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
517
- $ai_provider_metadata: options.providerMetadata
518
- } : {}),
519
- ...errorData,
520
- ...costOverrideData
652
+ const settleCallersTerminal = () => {
653
+ while (callerQueue.length > 0) {
654
+ const pending = callerQueue.shift();
655
+ if (hasTerminalError) {
656
+ pending.reject(terminalError);
657
+ } else if (terminalResult) {
658
+ pending.resolve(terminalResult);
659
+ }
660
+ }
661
+ };
662
+ const pump = () => {
663
+ if (operationInFlight || callerQueue.length === 0 || monitorActive && !monitorPending) {
664
+ return;
665
+ }
666
+ const pendingCaller = callerQueue.shift();
667
+ const pendingMonitor = monitorPending;
668
+ monitorPending = undefined;
669
+ operationInFlight = true;
670
+ void sourceIterator.next().then(result => {
671
+ operationInFlight = false;
672
+ if (result.done) {
673
+ terminalResult = result;
674
+ removeAbortListener();
675
+ }
676
+ pendingCaller.resolve(result);
677
+ pendingMonitor?.resolve(result);
678
+ if (result.done) {
679
+ settleCallersTerminal();
680
+ } else {
681
+ pump();
682
+ }
683
+ }, error => {
684
+ operationInFlight = false;
685
+ terminalError = error;
686
+ hasTerminalError = true;
687
+ removeAbortListener();
688
+ pendingCaller.reject(error);
689
+ pendingMonitor?.reject(error);
690
+ settleCallersTerminal();
691
+ });
692
+ };
693
+ const monitoringStream = {
694
+ [Symbol.asyncIterator]() {
695
+ return {
696
+ next: () => {
697
+ if (hasTerminalError) {
698
+ return Promise.reject(terminalError);
699
+ }
700
+ if (terminalResult) {
701
+ return Promise.resolve(terminalResult);
702
+ }
703
+ if (bufferedMonitorResult) {
704
+ const result = bufferedMonitorResult;
705
+ bufferedMonitorResult = undefined;
706
+ return Promise.resolve(result);
707
+ }
708
+ return new Promise((resolve, reject) => {
709
+ monitorPending = {
710
+ resolve,
711
+ reject
712
+ };
713
+ pump();
714
+ });
715
+ },
716
+ return: async value => {
717
+ monitorActive = false;
718
+ monitorPending = undefined;
719
+ pump();
720
+ return {
721
+ done: true,
722
+ value: value
723
+ };
724
+ }
725
+ };
726
+ }
727
+ };
728
+ const cancelSource = value => {
729
+ if (cancellationPromise) {
730
+ return cancellationPromise;
731
+ }
732
+ removeAbortListener();
733
+ if (!controller.signal.aborted) {
734
+ controller.abort();
735
+ }
736
+ cancellationPromise = (async () => {
737
+ try {
738
+ const defaultResult = {
739
+ done: true,
740
+ value
741
+ };
742
+ const result = sourceIterator.return ? await sourceIterator.return(value) : defaultResult;
743
+ if (result.done) {
744
+ terminalResult = result;
745
+ removeAbortListener();
746
+ settleMonitorTerminal();
747
+ settleCallersTerminal();
748
+ } else if (monitorPending) {
749
+ monitorPending.resolve(result);
750
+ monitorPending = undefined;
751
+ cancellationPromise = undefined;
752
+ } else {
753
+ bufferedMonitorResult = result;
754
+ cancellationPromise = undefined;
755
+ }
756
+ return result;
757
+ } catch (error) {
758
+ terminalError = error;
759
+ hasTerminalError = true;
760
+ removeAbortListener();
761
+ settleMonitorTerminal();
762
+ settleCallersTerminal();
763
+ throw error;
764
+ }
765
+ })();
766
+ // An AbortController cancellation has no caller awaiting this promise.
767
+ void cancellationPromise.catch(() => undefined);
768
+ return cancellationPromise;
521
769
  };
522
- const event = {
523
- distinctId: options.distinctId ?? traceId,
524
- event: eventType,
525
- properties,
526
- groups: options.groups
770
+ abortListener = () => {
771
+ void cancelSource();
527
772
  };
528
- if (options.captureImmediate) {
529
- await client.captureImmediate(event);
773
+ if (controller.signal.aborted) {
774
+ abortListener();
530
775
  } else {
531
- client.capture(event);
776
+ controller.signal.addEventListener('abort', abortListener, {
777
+ once: true
778
+ });
532
779
  }
533
- };
780
+ const callerStream = createStream(() => ({
781
+ next: () => {
782
+ if (hasTerminalError) {
783
+ return Promise.reject(terminalError);
784
+ }
785
+ if (terminalResult) {
786
+ return Promise.resolve(terminalResult);
787
+ }
788
+ return new Promise((resolve, reject) => {
789
+ callerQueue.push({
790
+ resolve,
791
+ reject
792
+ });
793
+ pump();
794
+ });
795
+ },
796
+ return: value => cancelSource(value),
797
+ throw: async error => {
798
+ if (!sourceIterator.throw) {
799
+ await cancelSource();
800
+ throw error;
801
+ }
802
+ try {
803
+ const result = await sourceIterator.throw(error);
804
+ if (result.done) {
805
+ terminalResult = result;
806
+ removeAbortListener();
807
+ settleCallersTerminal();
808
+ }
809
+ if (monitorPending) {
810
+ monitorPending.resolve(result);
811
+ monitorPending = undefined;
812
+ } else {
813
+ bufferedMonitorResult = result;
814
+ }
815
+ return result;
816
+ } catch (sourceError) {
817
+ terminalError = sourceError;
818
+ hasTerminalError = true;
819
+ removeAbortListener();
820
+ settleMonitorTerminal();
821
+ settleCallersTerminal();
822
+ throw sourceError;
823
+ }
824
+ }
825
+ }), controller);
826
+ return [monitoringStream, callerStream];
827
+ }
534
828
 
535
829
  class PostHogAnthropic extends AnthropicOriginal__default.default {
536
830
  constructor(config) {
@@ -555,9 +849,9 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
555
849
  posthogParams
556
850
  } = extractPosthogParams(body);
557
851
  const startTime = Date.now();
558
- const parentPromise = super.create(anthropicParams, options);
559
852
  if (anthropicParams.stream) {
560
- return parentPromise.then(value => {
853
+ const parentPromise = super.create(anthropicParams, options);
854
+ const wrappedPromise = parentPromise.then(value => {
561
855
  let accumulatedContent = '';
562
856
  const contentBlocks = [];
563
857
  const toolsInProgress = new Map();
@@ -572,8 +866,8 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
572
866
  webSearchCount: 0
573
867
  };
574
868
  let lastRawUsage;
575
- if ('tee' in value) {
576
- const [stream1, stream2] = value.tee();
869
+ if (Symbol.asyncIterator in value) {
870
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
577
871
  (async () => {
578
872
  try {
579
873
  for await (const chunk of stream1) {
@@ -734,7 +1028,11 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
734
1028
  }
735
1029
  return value;
736
1030
  });
1031
+ return preserveProviderPromise(parentPromise, wrappedPromise, {
1032
+ requestIdHeader: 'request-id'
1033
+ });
737
1034
  } else {
1035
+ const parentPromise = super.create(anthropicParams, options);
738
1036
  const wrappedPromise = parentPromise.then(async result => {
739
1037
  if ('content' in result) {
740
1038
  const latency = (Date.now() - startTime) / 1000;
@@ -781,7 +1079,9 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
781
1079
  });
782
1080
  throw error;
783
1081
  });
784
- return wrappedPromise;
1082
+ return preserveProviderPromise(parentPromise, wrappedPromise, {
1083
+ requestIdHeader: 'request-id'
1084
+ });
785
1085
  }
786
1086
  }
787
1087
  }