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