@posthog/ai 7.2.1 → 7.3.0

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,10 +1,9 @@
1
1
  'use strict';
2
2
 
3
- var ai = require('ai');
4
3
  var uuid = require('uuid');
5
4
  var buffer = require('buffer');
6
5
 
7
- var version = "7.2.1";
6
+ var version = "7.3.0";
8
7
 
9
8
  // Type guards for safer type checking
10
9
 
@@ -333,6 +332,15 @@ const sendEventToPosthog = async ({
333
332
 
334
333
  const REDACTED_IMAGE_PLACEHOLDER = '[base64 image redacted]';
335
334
 
335
+ // ============================================
336
+ // Multimodal Feature Toggle
337
+ // ============================================
338
+
339
+ const isMultimodalEnabled = () => {
340
+ const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
341
+ return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
342
+ };
343
+
336
344
  // ============================================
337
345
  // Base64 Detection Helpers
338
346
  // ============================================
@@ -360,6 +368,7 @@ const isRawBase64 = str => {
360
368
  return str.length > 20 && /^[A-Za-z0-9+/]+=*$/.test(str);
361
369
  };
362
370
  function redactBase64DataUrl(str) {
371
+ if (isMultimodalEnabled()) return str;
363
372
  if (!isString(str)) return str;
364
373
 
365
374
  // Check for data URL format
@@ -574,68 +583,126 @@ const extractProvider = model => {
574
583
  const providerName = provider.split('.')[0];
575
584
  return providerName;
576
585
  };
577
- const createInstrumentationMiddleware = (phClient, model, options) => {
578
- const middleware = {
579
- wrapGenerate: async ({
580
- doGenerate,
581
- params
582
- }) => {
586
+
587
+ // Extract web search count from provider metadata (works for both V2 and V3)
588
+ const extractWebSearchCount = (providerMetadata, usage) => {
589
+ // Try Anthropic-specific extraction
590
+ if (providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'server_tool_use' in providerMetadata.anthropic) {
591
+ const serverToolUse = providerMetadata.anthropic.server_tool_use;
592
+ if (serverToolUse && typeof serverToolUse === 'object' && 'web_search_requests' in serverToolUse && typeof serverToolUse.web_search_requests === 'number') {
593
+ return serverToolUse.web_search_requests;
594
+ }
595
+ }
596
+
597
+ // Fall back to generic calculation
598
+ return calculateWebSearchCount({
599
+ usage,
600
+ providerMetadata
601
+ });
602
+ };
603
+
604
+ // Extract additional token values from provider metadata
605
+ const extractAdditionalTokenValues = providerMetadata => {
606
+ if (providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'cacheCreationInputTokens' in providerMetadata.anthropic) {
607
+ return {
608
+ cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
609
+ };
610
+ }
611
+ return {};
612
+ };
613
+
614
+ // Helper to extract numeric token value from V2 (number) or V3 (object with .total) usage formats
615
+ const extractTokenCount = value => {
616
+ if (typeof value === 'number') {
617
+ return value;
618
+ }
619
+ if (value && typeof value === 'object' && 'total' in value && typeof value.total === 'number') {
620
+ return value.total;
621
+ }
622
+ return undefined;
623
+ };
624
+
625
+ // Helper to extract reasoning tokens from V2 (usage.reasoningTokens) or V3 (usage.outputTokens.reasoning)
626
+ const extractReasoningTokens = usage => {
627
+ // V2 style: top-level reasoningTokens
628
+ if ('reasoningTokens' in usage) {
629
+ return usage.reasoningTokens;
630
+ }
631
+ // V3 style: nested in outputTokens.reasoning
632
+ if ('outputTokens' in usage && usage.outputTokens && typeof usage.outputTokens === 'object' && 'reasoning' in usage.outputTokens) {
633
+ return usage.outputTokens.reasoning;
634
+ }
635
+ return undefined;
636
+ };
637
+
638
+ // Helper to extract cached input tokens from V2 (usage.cachedInputTokens) or V3 (usage.inputTokens.cacheRead)
639
+ const extractCacheReadTokens = usage => {
640
+ // V2 style: top-level cachedInputTokens
641
+ if ('cachedInputTokens' in usage) {
642
+ return usage.cachedInputTokens;
643
+ }
644
+ // V3 style: nested in inputTokens.cacheRead
645
+ if ('inputTokens' in usage && usage.inputTokens && typeof usage.inputTokens === 'object' && 'cacheRead' in usage.inputTokens) {
646
+ return usage.inputTokens.cacheRead;
647
+ }
648
+ return undefined;
649
+ };
650
+
651
+ /**
652
+ * Wraps a Vercel AI SDK language model (V2 or V3) with PostHog tracing.
653
+ * Automatically detects the model version and applies appropriate instrumentation.
654
+ */
655
+ const wrapVercelLanguageModel = (model, phClient, options) => {
656
+ const traceId = options.posthogTraceId ?? uuid.v4();
657
+ const mergedOptions = {
658
+ ...options,
659
+ posthogTraceId: traceId,
660
+ posthogDistinctId: options.posthogDistinctId,
661
+ posthogProperties: {
662
+ ...options.posthogProperties,
663
+ $ai_framework: 'vercel',
664
+ $ai_framework_version: model.specificationVersion === 'v3' ? '6' : '5'
665
+ }
666
+ };
667
+
668
+ // Create wrapped model that preserves the original type
669
+ const wrappedModel = {
670
+ ...model,
671
+ doGenerate: async params => {
583
672
  const startTime = Date.now();
584
673
  const mergedParams = {
585
- ...options,
586
- ...mapVercelParams(params),
587
- posthogProperties: {
588
- ...options.posthogProperties,
589
- $ai_framework: 'vercel'
590
- }
674
+ ...mergedOptions,
675
+ ...mapVercelParams(params)
591
676
  };
592
677
  const availableTools = extractAvailableToolCalls('vercel', params);
593
678
  try {
594
- const result = await doGenerate();
595
- const modelId = options.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
596
- const provider = options.posthogProviderOverride ?? extractProvider(model);
679
+ const result = await model.doGenerate(params);
680
+ const modelId = mergedOptions.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
681
+ const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
597
682
  const baseURL = ''; // cannot currently get baseURL from vercel
598
683
  const content = mapVercelOutput(result.content);
599
684
  const latency = (Date.now() - startTime) / 1000;
600
685
  const providerMetadata = result.providerMetadata;
601
- const additionalTokenValues = {
602
- ...(providerMetadata?.anthropic ? {
603
- cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
604
- } : {})
605
- };
686
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata);
687
+ const webSearchCount = extractWebSearchCount(providerMetadata, result.usage);
606
688
 
607
- // Calculate web search count based on provider
608
- let webSearchCount = 0;
609
- if (providerMetadata?.anthropic && typeof providerMetadata.anthropic === 'object' && 'server_tool_use' in providerMetadata.anthropic) {
610
- // Anthropic-specific extraction
611
- const serverToolUse = providerMetadata.anthropic.server_tool_use;
612
- if (serverToolUse && typeof serverToolUse === 'object' && 'web_search_requests' in serverToolUse && typeof serverToolUse.web_search_requests === 'number') {
613
- webSearchCount = serverToolUse.web_search_requests;
614
- }
615
- } else {
616
- // For other providers through Vercel, pass available metadata to helper
617
- // Note: Vercel abstracts provider responses, so we may not have access to
618
- // raw citations/annotations unless Vercel exposes them in usage/metadata
619
- webSearchCount = calculateWebSearchCount({
620
- usage: result.usage,
621
- providerMetadata: providerMetadata
622
- });
623
- }
689
+ // V2 usage has simple numbers, V3 has objects with .total - normalize both
690
+ const usageObj = result.usage;
624
691
  const usage = {
625
- inputTokens: result.usage.inputTokens,
626
- outputTokens: result.usage.outputTokens,
627
- reasoningTokens: result.usage.reasoningTokens,
628
- cacheReadInputTokens: result.usage.cachedInputTokens,
692
+ inputTokens: extractTokenCount(result.usage.inputTokens),
693
+ outputTokens: extractTokenCount(result.usage.outputTokens),
694
+ reasoningTokens: extractReasoningTokens(usageObj),
695
+ cacheReadInputTokens: extractCacheReadTokens(usageObj),
629
696
  webSearchCount,
630
697
  ...additionalTokenValues
631
698
  };
632
699
  await sendEventToPosthog({
633
700
  client: phClient,
634
- distinctId: options.posthogDistinctId,
635
- traceId: options.posthogTraceId ?? uuid.v4(),
701
+ distinctId: mergedOptions.posthogDistinctId,
702
+ traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
636
703
  model: modelId,
637
704
  provider: provider,
638
- input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
705
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
639
706
  output: content,
640
707
  latency,
641
708
  baseURL,
@@ -643,18 +710,18 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
643
710
  httpStatus: 200,
644
711
  usage,
645
712
  tools: availableTools,
646
- captureImmediate: options.posthogCaptureImmediate
713
+ captureImmediate: mergedOptions.posthogCaptureImmediate
647
714
  });
648
715
  return result;
649
716
  } catch (error) {
650
717
  const modelId = model.modelId;
651
718
  await sendEventToPosthog({
652
719
  client: phClient,
653
- distinctId: options.posthogDistinctId,
654
- traceId: options.posthogTraceId ?? uuid.v4(),
720
+ distinctId: mergedOptions.posthogDistinctId,
721
+ traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
655
722
  model: modelId,
656
723
  provider: model.provider,
657
- input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
724
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
658
725
  output: [],
659
726
  latency: 0,
660
727
  baseURL: '',
@@ -667,30 +734,23 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
667
734
  isError: true,
668
735
  error: truncate(JSON.stringify(error)),
669
736
  tools: availableTools,
670
- captureImmediate: options.posthogCaptureImmediate
737
+ captureImmediate: mergedOptions.posthogCaptureImmediate
671
738
  });
672
739
  throw error;
673
740
  }
674
741
  },
675
- wrapStream: async ({
676
- doStream,
677
- params
678
- }) => {
742
+ doStream: async params => {
679
743
  const startTime = Date.now();
680
744
  let generatedText = '';
681
745
  let reasoningText = '';
682
746
  let usage = {};
683
747
  let providerMetadata = undefined;
684
748
  const mergedParams = {
685
- ...options,
686
- ...mapVercelParams(params),
687
- posthogProperties: {
688
- ...options.posthogProperties,
689
- $ai_framework: 'vercel'
690
- }
749
+ ...mergedOptions,
750
+ ...mapVercelParams(params)
691
751
  };
692
- const modelId = options.posthogModelOverride ?? model.modelId;
693
- const provider = options.posthogProviderOverride ?? extractProvider(model);
752
+ const modelId = mergedOptions.posthogModelOverride ?? model.modelId;
753
+ const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
694
754
  const availableTools = extractAvailableToolCalls('vercel', params);
695
755
  const baseURL = ''; // cannot currently get baseURL from vercel
696
756
 
@@ -700,15 +760,15 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
700
760
  const {
701
761
  stream,
702
762
  ...rest
703
- } = await doStream();
763
+ } = await model.doStream(params);
704
764
  const transformStream = new TransformStream({
705
765
  transform(chunk, controller) {
706
- // Handle new v5 streaming patterns
766
+ // Handle streaming patterns - compatible with both V2 and V3
707
767
  if (chunk.type === 'text-delta') {
708
768
  generatedText += chunk.delta;
709
769
  }
710
770
  if (chunk.type === 'reasoning-delta') {
711
- reasoningText += chunk.delta; // New in v5
771
+ reasoningText += chunk.delta;
712
772
  }
713
773
 
714
774
  // Handle tool call chunks
@@ -729,7 +789,6 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
729
789
  }
730
790
  if (chunk.type === 'tool-input-end') {
731
791
  // Tool call is complete, keep it in the map for final processing
732
- // Nothing specific to do here, the tool call is already complete
733
792
  }
734
793
  if (chunk.type === 'tool-call') {
735
794
  // Direct tool call chunk (complete tool call)
@@ -741,14 +800,13 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
741
800
  }
742
801
  if (chunk.type === 'finish') {
743
802
  providerMetadata = chunk.providerMetadata;
744
- const additionalTokenValues = providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'cacheCreationInputTokens' in providerMetadata.anthropic ? {
745
- cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
746
- } : {};
803
+ const additionalTokenValues = extractAdditionalTokenValues(providerMetadata);
804
+ const chunkUsage = chunk.usage || {};
747
805
  usage = {
748
- inputTokens: chunk.usage?.inputTokens,
749
- outputTokens: chunk.usage?.outputTokens,
750
- reasoningTokens: chunk.usage?.reasoningTokens,
751
- cacheReadInputTokens: chunk.usage?.cachedInputTokens,
806
+ inputTokens: extractTokenCount(chunk.usage?.inputTokens),
807
+ outputTokens: extractTokenCount(chunk.usage?.outputTokens),
808
+ reasoningTokens: extractReasoningTokens(chunkUsage),
809
+ cacheReadInputTokens: extractCacheReadTokens(chunkUsage),
752
810
  ...additionalTokenValues
753
811
  };
754
812
  }
@@ -790,24 +848,7 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
790
848
  role: 'assistant',
791
849
  content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
792
850
  }] : [];
793
-
794
- // Calculate web search count based on provider
795
- let webSearchCount = 0;
796
- if (providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'server_tool_use' in providerMetadata.anthropic) {
797
- // Anthropic-specific extraction
798
- const serverToolUse = providerMetadata.anthropic.server_tool_use;
799
- if (serverToolUse && typeof serverToolUse === 'object' && 'web_search_requests' in serverToolUse && typeof serverToolUse.web_search_requests === 'number') {
800
- webSearchCount = serverToolUse.web_search_requests;
801
- }
802
- } else {
803
- // For other providers through Vercel, pass available metadata to helper
804
- // Note: Vercel abstracts provider responses, so we may not have access to
805
- // raw citations/annotations unless Vercel exposes them in usage/metadata
806
- webSearchCount = calculateWebSearchCount({
807
- usage: usage,
808
- providerMetadata: providerMetadata
809
- });
810
- }
851
+ const webSearchCount = extractWebSearchCount(providerMetadata, usage);
811
852
 
812
853
  // Update usage with web search count
813
854
  const finalUsage = {
@@ -816,11 +857,11 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
816
857
  };
817
858
  await sendEventToPosthog({
818
859
  client: phClient,
819
- distinctId: options.posthogDistinctId,
820
- traceId: options.posthogTraceId ?? uuid.v4(),
860
+ distinctId: mergedOptions.posthogDistinctId,
861
+ traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
821
862
  model: modelId,
822
863
  provider: provider,
823
- input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
864
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
824
865
  output: output,
825
866
  latency,
826
867
  baseURL,
@@ -828,7 +869,7 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
828
869
  httpStatus: 200,
829
870
  usage: finalUsage,
830
871
  tools: availableTools,
831
- captureImmediate: options.posthogCaptureImmediate
872
+ captureImmediate: mergedOptions.posthogCaptureImmediate
832
873
  });
833
874
  }
834
875
  });
@@ -839,11 +880,11 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
839
880
  } catch (error) {
840
881
  await sendEventToPosthog({
841
882
  client: phClient,
842
- distinctId: options.posthogDistinctId,
843
- traceId: options.posthogTraceId ?? uuid.v4(),
883
+ distinctId: mergedOptions.posthogDistinctId,
884
+ traceId: mergedOptions.posthogTraceId ?? uuid.v4(),
844
885
  model: modelId,
845
886
  provider: provider,
846
- input: options.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
887
+ input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
847
888
  output: [],
848
889
  latency: 0,
849
890
  baseURL: '',
@@ -856,25 +897,12 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
856
897
  isError: true,
857
898
  error: truncate(JSON.stringify(error)),
858
899
  tools: availableTools,
859
- captureImmediate: options.posthogCaptureImmediate
900
+ captureImmediate: mergedOptions.posthogCaptureImmediate
860
901
  });
861
902
  throw error;
862
903
  }
863
904
  }
864
905
  };
865
- return middleware;
866
- };
867
- const wrapVercelLanguageModel = (model, phClient, options) => {
868
- const traceId = options.posthogTraceId ?? uuid.v4();
869
- const middleware = createInstrumentationMiddleware(phClient, model, {
870
- ...options,
871
- posthogTraceId: traceId,
872
- posthogDistinctId: options.posthogDistinctId
873
- });
874
- const wrappedModel = ai.wrapLanguageModel({
875
- model,
876
- middleware
877
- });
878
906
  return wrappedModel;
879
907
  };
880
908