@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 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.1";
303
+ var version = "8.6.3";
303
304
 
304
305
  const DEFAULT_MAX_DEPTH = 3;
305
306
  const MAX_STACK_LINES = 20;
@@ -410,122 +411,347 @@ 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
- const safeInput = sanitizeValues(options.input);
422
- const safeOutput = sanitizeValues(options.output);
423
- let httpStatus = options.httpStatus;
424
- let errorData = {};
425
- if (options.error) {
426
- if (httpStatus === undefined) {
427
- if (typeof options.error === 'object' && 'status' in options.error && typeof options.error.status === 'number') {
428
- httpStatus = options.error.status;
429
- } else {
430
- httpStatus = 500;
431
- }
414
+ try {
415
+ if (!client.capture) {
416
+ return;
432
417
  }
433
- let exceptionId;
434
- if (client.options?.enableExceptionAutocapture) {
435
- exceptionId = core.uuidv7();
436
- client.captureException(options.error, undefined, {
437
- $ai_trace_id: traceId
438
- }, exceptionId);
439
- if (typeof options.error === 'object') {
440
- 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 {
437
+ httpStatus = 500;
438
+ }
441
439
  }
440
+ let exceptionId;
441
+ if (client.options?.enableExceptionAutocapture) {
442
+ exceptionId = core.uuidv7();
443
+ client.captureException(options.error, undefined, {
444
+ $ai_trace_id: traceId
445
+ }, exceptionId);
446
+ if (typeof options.error === 'object') {
447
+ ;
448
+ options.error.__posthog_previously_captured_error = true;
449
+ }
450
+ }
451
+ errorData = {
452
+ $ai_is_error: true,
453
+ $ai_error: stringifyError(options.error),
454
+ $exception_event_id: exceptionId
455
+ };
456
+ }
457
+ httpStatus = httpStatus ?? 200;
458
+ let costOverrideData = {};
459
+ if (options.costOverride) {
460
+ const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
461
+ const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
462
+ costOverrideData = {
463
+ $ai_input_cost_usd: inputCostUSD,
464
+ $ai_output_cost_usd: outputCostUSD,
465
+ $ai_total_cost_usd: inputCostUSD + outputCostUSD
466
+ };
442
467
  }
443
- errorData = {
444
- $ai_is_error: true,
445
- $ai_error: stringifyError(options.error),
446
- $exception_event_id: exceptionId
468
+ const additionalTokenValues = {
469
+ ...(usage.reasoningTokens ? {
470
+ $ai_reasoning_tokens: usage.reasoningTokens
471
+ } : {}),
472
+ ...(usage.cacheReadInputTokens ? {
473
+ $ai_cache_read_input_tokens: usage.cacheReadInputTokens
474
+ } : {}),
475
+ ...(usage.cacheCreationInputTokens ? {
476
+ $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
477
+ } : {}),
478
+ ...(usage.webSearchCount ? {
479
+ $ai_web_search_count: usage.webSearchCount
480
+ } : {}),
481
+ ...(usage.rawUsage ? {
482
+ $ai_usage: usage.rawUsage
483
+ } : {})
447
484
  };
448
- }
449
- httpStatus = httpStatus ?? 200;
450
- let costOverrideData = {};
451
- if (options.costOverride) {
452
- const inputCostUSD = (options.costOverride.inputCost ?? 0) * (usage.inputTokens ?? 0);
453
- const outputCostUSD = (options.costOverride.outputCost ?? 0) * (usage.outputTokens ?? 0);
454
- costOverrideData = {
455
- $ai_input_cost_usd: inputCostUSD,
456
- $ai_output_cost_usd: outputCostUSD,
457
- $ai_total_cost_usd: inputCostUSD + outputCostUSD
485
+ const properties = {
486
+ $ai_lib: 'posthog-ai',
487
+ $ai_lib_version: version,
488
+ $ai_provider: options.providerOverride ?? options.provider,
489
+ $ai_model: options.modelOverride ?? options.model,
490
+ $ai_model_parameters: options.modelParameters ?? {},
491
+ $ai_input: safeInput,
492
+ $ai_output_choices: safeOutput,
493
+ $ai_http_status: httpStatus,
494
+ $ai_input_tokens: usage.inputTokens ?? 0,
495
+ ...(usage.outputTokens !== undefined ? {
496
+ $ai_output_tokens: usage.outputTokens
497
+ } : {}),
498
+ ...additionalTokenValues,
499
+ $ai_latency: options.latency ?? 0,
500
+ ...(options.timeToFirstToken !== undefined ? {
501
+ $ai_time_to_first_token: options.timeToFirstToken
502
+ } : {}),
503
+ $ai_trace_id: traceId,
504
+ $ai_base_url: options.baseURL ?? '',
505
+ ...options.properties,
506
+ $ai_tokens_source: getTokensSource(options.properties),
507
+ ...(options.distinctId ? {} : {
508
+ $process_person_profile: false
509
+ }),
510
+ ...(options.stopReason ? {
511
+ $ai_stop_reason: options.stopReason
512
+ } : {}),
513
+ ...(options.tools ? {
514
+ $ai_tools: options.tools
515
+ } : {}),
516
+ ...(options.completionId ? {
517
+ $ai_completion_id: options.completionId
518
+ } : {}),
519
+ ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
520
+ $ai_provider_metadata: options.providerMetadata
521
+ } : {}),
522
+ ...errorData,
523
+ ...costOverrideData
524
+ };
525
+ const event = {
526
+ distinctId: options.distinctId ?? traceId,
527
+ event: eventType,
528
+ properties,
529
+ groups: options.groups
458
530
  };
531
+ if (options.captureImmediate) {
532
+ await client.captureImmediate(event);
533
+ } else {
534
+ client.capture(event);
535
+ }
536
+ } catch (error) {
537
+ // Telemetry failures must never affect the instrumented provider call.
538
+ console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
459
539
  }
460
- const additionalTokenValues = {
461
- ...(usage.reasoningTokens ? {
462
- $ai_reasoning_tokens: usage.reasoningTokens
463
- } : {}),
464
- ...(usage.cacheReadInputTokens ? {
465
- $ai_cache_read_input_tokens: usage.cacheReadInputTokens
466
- } : {}),
467
- ...(usage.cacheCreationInputTokens ? {
468
- $ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
469
- } : {}),
470
- ...(usage.webSearchCount ? {
471
- $ai_web_search_count: usage.webSearchCount
472
- } : {}),
473
- ...(usage.rawUsage ? {
474
- $ai_usage: usage.rawUsage
475
- } : {})
540
+ };
541
+
542
+ /**
543
+ * Splits an SDK stream into a monitoring branch and a caller branch without
544
+ * allowing either branch to read ahead of the other. Unlike the SDKs' `tee()`
545
+ * implementations, this keeps at most one result in flight and makes caller
546
+ * cancellation terminate the monitoring branch and the source iterator.
547
+ */
548
+ function monitoredStreamTee(source, createStream) {
549
+ const controller = source.controller ?? new AbortController();
550
+ const sourceIterator = source[Symbol.asyncIterator]();
551
+ const callerQueue = [];
552
+ let monitorPending;
553
+ let monitorActive = true;
554
+ let operationInFlight = false;
555
+ let terminalResult;
556
+ let bufferedMonitorResult;
557
+ let terminalError;
558
+ let hasTerminalError = false;
559
+ let cancellationPromise;
560
+ let abortListener;
561
+ const removeAbortListener = () => {
562
+ if (abortListener) {
563
+ controller.signal.removeEventListener('abort', abortListener);
564
+ abortListener = undefined;
565
+ }
566
+ };
567
+ const settleMonitorTerminal = () => {
568
+ if (!monitorPending) {
569
+ return;
570
+ }
571
+ const pending = monitorPending;
572
+ monitorPending = undefined;
573
+ if (hasTerminalError) {
574
+ pending.reject(terminalError);
575
+ } else if (terminalResult) {
576
+ pending.resolve(terminalResult);
577
+ }
578
+ };
579
+ const settleCallersTerminal = () => {
580
+ while (callerQueue.length > 0) {
581
+ const pending = callerQueue.shift();
582
+ if (hasTerminalError) {
583
+ pending.reject(terminalError);
584
+ } else if (terminalResult) {
585
+ pending.resolve(terminalResult);
586
+ }
587
+ }
588
+ };
589
+ const pump = () => {
590
+ if (operationInFlight || callerQueue.length === 0 || monitorActive && !monitorPending) {
591
+ return;
592
+ }
593
+ const pendingCaller = callerQueue.shift();
594
+ const pendingMonitor = monitorPending;
595
+ monitorPending = undefined;
596
+ operationInFlight = true;
597
+ void sourceIterator.next().then(result => {
598
+ operationInFlight = false;
599
+ if (result.done) {
600
+ terminalResult = result;
601
+ removeAbortListener();
602
+ }
603
+ pendingCaller.resolve(result);
604
+ pendingMonitor?.resolve(result);
605
+ if (result.done) {
606
+ settleCallersTerminal();
607
+ } else {
608
+ pump();
609
+ }
610
+ }, error => {
611
+ operationInFlight = false;
612
+ terminalError = error;
613
+ hasTerminalError = true;
614
+ removeAbortListener();
615
+ pendingCaller.reject(error);
616
+ pendingMonitor?.reject(error);
617
+ settleCallersTerminal();
618
+ });
476
619
  };
477
- const properties = {
478
- $ai_lib: 'posthog-ai',
479
- $ai_lib_version: version,
480
- $ai_provider: options.providerOverride ?? options.provider,
481
- $ai_model: options.modelOverride ?? options.model,
482
- $ai_model_parameters: options.modelParameters ?? {},
483
- $ai_input: withPrivacyMode(client, privacyMode, safeInput),
484
- $ai_output_choices: withPrivacyMode(client, privacyMode, safeOutput),
485
- $ai_http_status: httpStatus,
486
- $ai_input_tokens: usage.inputTokens ?? 0,
487
- ...(usage.outputTokens !== undefined ? {
488
- $ai_output_tokens: usage.outputTokens
489
- } : {}),
490
- ...additionalTokenValues,
491
- $ai_latency: options.latency ?? 0,
492
- ...(options.timeToFirstToken !== undefined ? {
493
- $ai_time_to_first_token: options.timeToFirstToken
494
- } : {}),
495
- $ai_trace_id: traceId,
496
- $ai_base_url: options.baseURL ?? '',
497
- ...options.properties,
498
- $ai_tokens_source: getTokensSource(options.properties),
499
- ...(options.distinctId ? {} : {
500
- $process_person_profile: false
501
- }),
502
- ...(options.stopReason ? {
503
- $ai_stop_reason: options.stopReason
504
- } : {}),
505
- ...(options.tools ? {
506
- $ai_tools: options.tools
507
- } : {}),
508
- ...(options.completionId ? {
509
- $ai_completion_id: options.completionId
510
- } : {}),
511
- ...(options.providerMetadata && Object.keys(options.providerMetadata).length > 0 ? {
512
- $ai_provider_metadata: options.providerMetadata
513
- } : {}),
514
- ...errorData,
515
- ...costOverrideData
620
+ const monitoringStream = {
621
+ [Symbol.asyncIterator]() {
622
+ return {
623
+ next: () => {
624
+ if (hasTerminalError) {
625
+ return Promise.reject(terminalError);
626
+ }
627
+ if (terminalResult) {
628
+ return Promise.resolve(terminalResult);
629
+ }
630
+ if (bufferedMonitorResult) {
631
+ const result = bufferedMonitorResult;
632
+ bufferedMonitorResult = undefined;
633
+ return Promise.resolve(result);
634
+ }
635
+ return new Promise((resolve, reject) => {
636
+ monitorPending = {
637
+ resolve,
638
+ reject
639
+ };
640
+ pump();
641
+ });
642
+ },
643
+ return: async value => {
644
+ monitorActive = false;
645
+ monitorPending = undefined;
646
+ pump();
647
+ return {
648
+ done: true,
649
+ value: value
650
+ };
651
+ }
652
+ };
653
+ }
654
+ };
655
+ const cancelSource = value => {
656
+ if (cancellationPromise) {
657
+ return cancellationPromise;
658
+ }
659
+ removeAbortListener();
660
+ if (!controller.signal.aborted) {
661
+ controller.abort();
662
+ }
663
+ cancellationPromise = (async () => {
664
+ try {
665
+ const defaultResult = {
666
+ done: true,
667
+ value
668
+ };
669
+ const result = sourceIterator.return ? await sourceIterator.return(value) : defaultResult;
670
+ if (result.done) {
671
+ terminalResult = result;
672
+ removeAbortListener();
673
+ settleMonitorTerminal();
674
+ settleCallersTerminal();
675
+ } else if (monitorPending) {
676
+ monitorPending.resolve(result);
677
+ monitorPending = undefined;
678
+ cancellationPromise = undefined;
679
+ } else {
680
+ bufferedMonitorResult = result;
681
+ cancellationPromise = undefined;
682
+ }
683
+ return result;
684
+ } catch (error) {
685
+ terminalError = error;
686
+ hasTerminalError = true;
687
+ removeAbortListener();
688
+ settleMonitorTerminal();
689
+ settleCallersTerminal();
690
+ throw error;
691
+ }
692
+ })();
693
+ // An AbortController cancellation has no caller awaiting this promise.
694
+ void cancellationPromise.catch(() => undefined);
695
+ return cancellationPromise;
516
696
  };
517
- const event = {
518
- distinctId: options.distinctId ?? traceId,
519
- event: eventType,
520
- properties,
521
- groups: options.groups
697
+ abortListener = () => {
698
+ void cancelSource();
522
699
  };
523
- if (options.captureImmediate) {
524
- await client.captureImmediate(event);
700
+ if (controller.signal.aborted) {
701
+ abortListener();
525
702
  } else {
526
- client.capture(event);
703
+ controller.signal.addEventListener('abort', abortListener, {
704
+ once: true
705
+ });
527
706
  }
528
- };
707
+ const callerStream = createStream(() => ({
708
+ next: () => {
709
+ if (hasTerminalError) {
710
+ return Promise.reject(terminalError);
711
+ }
712
+ if (terminalResult) {
713
+ return Promise.resolve(terminalResult);
714
+ }
715
+ return new Promise((resolve, reject) => {
716
+ callerQueue.push({
717
+ resolve,
718
+ reject
719
+ });
720
+ pump();
721
+ });
722
+ },
723
+ return: value => cancelSource(value),
724
+ throw: async error => {
725
+ if (!sourceIterator.throw) {
726
+ await cancelSource();
727
+ throw error;
728
+ }
729
+ try {
730
+ const result = await sourceIterator.throw(error);
731
+ if (result.done) {
732
+ terminalResult = result;
733
+ removeAbortListener();
734
+ settleCallersTerminal();
735
+ }
736
+ if (monitorPending) {
737
+ monitorPending.resolve(result);
738
+ monitorPending = undefined;
739
+ } else {
740
+ bufferedMonitorResult = result;
741
+ }
742
+ return result;
743
+ } catch (sourceError) {
744
+ terminalError = sourceError;
745
+ hasTerminalError = true;
746
+ removeAbortListener();
747
+ settleMonitorTerminal();
748
+ settleCallersTerminal();
749
+ throw sourceError;
750
+ }
751
+ }
752
+ }), controller);
753
+ return [monitoringStream, callerStream];
754
+ }
529
755
 
530
756
  class PostHogAnthropic extends AnthropicOriginal__default.default {
531
757
  constructor(config) {
@@ -567,8 +793,8 @@ class WrappedMessages extends AnthropicOriginal__default.default.Messages {
567
793
  webSearchCount: 0
568
794
  };
569
795
  let lastRawUsage;
570
- if ('tee' in value) {
571
- const [stream1, stream2] = value.tee();
796
+ if (Symbol.asyncIterator in value) {
797
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new streaming.Stream(iterator, controller));
572
798
  (async () => {
573
799
  try {
574
800
  for await (const chunk of stream1) {