@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 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.3";
295
296
 
296
297
  const DEFAULT_MAX_DEPTH = 3;
297
298
  const MAX_STACK_LINES = 20;
@@ -402,127 +403,347 @@ 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 {
429
+ httpStatus = 500;
430
+ }
428
431
  }
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;
432
+ let exceptionId;
433
+ if (client.options?.enableExceptionAutocapture) {
434
+ exceptionId = uuidv7();
435
+ client.captureException(options.error, undefined, {
436
+ $ai_trace_id: traceId
437
+ }, exceptionId);
438
+ if (typeof options.error === 'object') {
439
+ ;
440
+ options.error.__posthog_previously_captured_error = true;
441
+ }
438
442
  }
443
+ errorData = {
444
+ $ai_is_error: true,
445
+ $ai_error: stringifyError(options.error),
446
+ $exception_event_id: exceptionId
447
+ };
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
458
+ };
439
459
  }
440
- errorData = {
441
- $ai_is_error: true,
442
- $ai_error: stringifyError(options.error),
443
- $exception_event_id: exceptionId
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
+ } : {})
444
476
  };
445
- }
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
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: safeInput,
484
+ $ai_output_choices: 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
455
516
  };
517
+ const event = {
518
+ distinctId: options.distinctId ?? traceId,
519
+ event: eventType,
520
+ properties,
521
+ groups: options.groups
522
+ };
523
+ if (options.captureImmediate) {
524
+ await client.captureImmediate(event);
525
+ } else {
526
+ client.capture(event);
527
+ }
528
+ } catch (error) {
529
+ // Telemetry failures must never affect the instrumented provider call.
530
+ console.warn('[PostHog AI] Failed to capture generation telemetry:', error);
456
531
  }
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
- } : {})
532
+ };
533
+
534
+ /**
535
+ * Splits an SDK stream into a monitoring branch and a caller branch without
536
+ * allowing either branch to read ahead of the other. Unlike the SDKs' `tee()`
537
+ * implementations, this keeps at most one result in flight and makes caller
538
+ * cancellation terminate the monitoring branch and the source iterator.
539
+ */
540
+ function monitoredStreamTee(source, createStream) {
541
+ const controller = source.controller ?? new AbortController();
542
+ const sourceIterator = source[Symbol.asyncIterator]();
543
+ const callerQueue = [];
544
+ let monitorPending;
545
+ let monitorActive = true;
546
+ let operationInFlight = false;
547
+ let terminalResult;
548
+ let bufferedMonitorResult;
549
+ let terminalError;
550
+ let hasTerminalError = false;
551
+ let cancellationPromise;
552
+ let abortListener;
553
+ const removeAbortListener = () => {
554
+ if (abortListener) {
555
+ controller.signal.removeEventListener('abort', abortListener);
556
+ abortListener = undefined;
557
+ }
473
558
  };
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
559
+ const settleMonitorTerminal = () => {
560
+ if (!monitorPending) {
561
+ return;
562
+ }
563
+ const pending = monitorPending;
564
+ monitorPending = undefined;
565
+ if (hasTerminalError) {
566
+ pending.reject(terminalError);
567
+ } else if (terminalResult) {
568
+ pending.resolve(terminalResult);
569
+ }
513
570
  };
514
- const event = {
515
- distinctId: options.distinctId ?? traceId,
516
- event: eventType,
517
- properties,
518
- groups: options.groups
571
+ const settleCallersTerminal = () => {
572
+ while (callerQueue.length > 0) {
573
+ const pending = callerQueue.shift();
574
+ if (hasTerminalError) {
575
+ pending.reject(terminalError);
576
+ } else if (terminalResult) {
577
+ pending.resolve(terminalResult);
578
+ }
579
+ }
580
+ };
581
+ const pump = () => {
582
+ if (operationInFlight || callerQueue.length === 0 || monitorActive && !monitorPending) {
583
+ return;
584
+ }
585
+ const pendingCaller = callerQueue.shift();
586
+ const pendingMonitor = monitorPending;
587
+ monitorPending = undefined;
588
+ operationInFlight = true;
589
+ void sourceIterator.next().then(result => {
590
+ operationInFlight = false;
591
+ if (result.done) {
592
+ terminalResult = result;
593
+ removeAbortListener();
594
+ }
595
+ pendingCaller.resolve(result);
596
+ pendingMonitor?.resolve(result);
597
+ if (result.done) {
598
+ settleCallersTerminal();
599
+ } else {
600
+ pump();
601
+ }
602
+ }, error => {
603
+ operationInFlight = false;
604
+ terminalError = error;
605
+ hasTerminalError = true;
606
+ removeAbortListener();
607
+ pendingCaller.reject(error);
608
+ pendingMonitor?.reject(error);
609
+ settleCallersTerminal();
610
+ });
519
611
  };
520
- if (options.captureImmediate) {
521
- await client.captureImmediate(event);
612
+ const monitoringStream = {
613
+ [Symbol.asyncIterator]() {
614
+ return {
615
+ next: () => {
616
+ if (hasTerminalError) {
617
+ return Promise.reject(terminalError);
618
+ }
619
+ if (terminalResult) {
620
+ return Promise.resolve(terminalResult);
621
+ }
622
+ if (bufferedMonitorResult) {
623
+ const result = bufferedMonitorResult;
624
+ bufferedMonitorResult = undefined;
625
+ return Promise.resolve(result);
626
+ }
627
+ return new Promise((resolve, reject) => {
628
+ monitorPending = {
629
+ resolve,
630
+ reject
631
+ };
632
+ pump();
633
+ });
634
+ },
635
+ return: async value => {
636
+ monitorActive = false;
637
+ monitorPending = undefined;
638
+ pump();
639
+ return {
640
+ done: true,
641
+ value: value
642
+ };
643
+ }
644
+ };
645
+ }
646
+ };
647
+ const cancelSource = value => {
648
+ if (cancellationPromise) {
649
+ return cancellationPromise;
650
+ }
651
+ removeAbortListener();
652
+ if (!controller.signal.aborted) {
653
+ controller.abort();
654
+ }
655
+ cancellationPromise = (async () => {
656
+ try {
657
+ const defaultResult = {
658
+ done: true,
659
+ value
660
+ };
661
+ const result = sourceIterator.return ? await sourceIterator.return(value) : defaultResult;
662
+ if (result.done) {
663
+ terminalResult = result;
664
+ removeAbortListener();
665
+ settleMonitorTerminal();
666
+ settleCallersTerminal();
667
+ } else if (monitorPending) {
668
+ monitorPending.resolve(result);
669
+ monitorPending = undefined;
670
+ cancellationPromise = undefined;
671
+ } else {
672
+ bufferedMonitorResult = result;
673
+ cancellationPromise = undefined;
674
+ }
675
+ return result;
676
+ } catch (error) {
677
+ terminalError = error;
678
+ hasTerminalError = true;
679
+ removeAbortListener();
680
+ settleMonitorTerminal();
681
+ settleCallersTerminal();
682
+ throw error;
683
+ }
684
+ })();
685
+ // An AbortController cancellation has no caller awaiting this promise.
686
+ void cancellationPromise.catch(() => undefined);
687
+ return cancellationPromise;
688
+ };
689
+ abortListener = () => {
690
+ void cancelSource();
691
+ };
692
+ if (controller.signal.aborted) {
693
+ abortListener();
522
694
  } else {
523
- client.capture(event);
695
+ controller.signal.addEventListener('abort', abortListener, {
696
+ once: true
697
+ });
524
698
  }
525
- };
699
+ const callerStream = createStream(() => ({
700
+ next: () => {
701
+ if (hasTerminalError) {
702
+ return Promise.reject(terminalError);
703
+ }
704
+ if (terminalResult) {
705
+ return Promise.resolve(terminalResult);
706
+ }
707
+ return new Promise((resolve, reject) => {
708
+ callerQueue.push({
709
+ resolve,
710
+ reject
711
+ });
712
+ pump();
713
+ });
714
+ },
715
+ return: value => cancelSource(value),
716
+ throw: async error => {
717
+ if (!sourceIterator.throw) {
718
+ await cancelSource();
719
+ throw error;
720
+ }
721
+ try {
722
+ const result = await sourceIterator.throw(error);
723
+ if (result.done) {
724
+ terminalResult = result;
725
+ removeAbortListener();
726
+ settleCallersTerminal();
727
+ }
728
+ if (monitorPending) {
729
+ monitorPending.resolve(result);
730
+ monitorPending = undefined;
731
+ } else {
732
+ bufferedMonitorResult = result;
733
+ }
734
+ return result;
735
+ } catch (sourceError) {
736
+ terminalError = sourceError;
737
+ hasTerminalError = true;
738
+ removeAbortListener();
739
+ settleMonitorTerminal();
740
+ settleCallersTerminal();
741
+ throw sourceError;
742
+ }
743
+ }
744
+ }), controller);
745
+ return [monitoringStream, callerStream];
746
+ }
526
747
 
527
748
  class PostHogAnthropic extends AnthropicOriginal {
528
749
  constructor(config) {
@@ -564,8 +785,8 @@ class WrappedMessages extends AnthropicOriginal.Messages {
564
785
  webSearchCount: 0
565
786
  };
566
787
  let lastRawUsage;
567
- if ('tee' in value) {
568
- const [stream1, stream2] = value.tee();
788
+ if (Symbol.asyncIterator in value) {
789
+ const [stream1, stream2] = monitoredStreamTee(value, (iterator, controller) => new Stream(iterator, controller));
569
790
  (async () => {
570
791
  try {
571
792
  for await (const chunk of stream1) {