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