@posthog/ai 8.7.0 → 8.8.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.
- package/dist/anthropic/index.cjs +61 -17
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.mjs +61 -17
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/gemini/index.cjs +73 -17
- package/dist/gemini/index.cjs.map +1 -1
- package/dist/gemini/index.mjs +73 -17
- package/dist/gemini/index.mjs.map +1 -1
- package/dist/index.cjs +83 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +83 -39
- package/dist/index.mjs.map +1 -1
- package/dist/langchain/index.cjs +27 -13
- package/dist/langchain/index.cjs.map +1 -1
- package/dist/langchain/index.mjs +27 -13
- package/dist/langchain/index.mjs.map +1 -1
- package/dist/openai/index.cjs +91 -47
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.mjs +91 -47
- package/dist/openai/index.mjs.map +1 -1
- package/dist/openai-agents/index.cjs +25 -4
- package/dist/openai-agents/index.cjs.map +1 -1
- package/dist/openai-agents/index.mjs +25 -4
- package/dist/openai-agents/index.mjs.map +1 -1
- package/dist/otel/index.cjs +1 -5
- package/dist/otel/index.cjs.map +1 -1
- package/dist/otel/index.mjs +1 -5
- package/dist/otel/index.mjs.map +1 -1
- package/dist/vercel/index.cjs +92 -39
- package/dist/vercel/index.cjs.map +1 -1
- package/dist/vercel/index.mjs +92 -39
- package/dist/vercel/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/vercel/index.mjs
CHANGED
|
@@ -10,6 +10,33 @@ const isObject = value => {
|
|
|
10
10
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
/** @internal */
|
|
14
|
+
|
|
15
|
+
/** @internal */
|
|
16
|
+
|
|
17
|
+
/** @internal */
|
|
18
|
+
function isFullAiCaptureEnabled(client) {
|
|
19
|
+
return client?.enableFullAiCapture === true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @internal */
|
|
23
|
+
function captureAiEvent(client, event) {
|
|
24
|
+
if (isFullAiCaptureEnabled(client) && typeof client.captureAi === 'function') {
|
|
25
|
+
client.captureAi(event);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
client.capture(event);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
async function captureAiEventImmediate(client, event) {
|
|
33
|
+
if (isFullAiCaptureEnabled(client) && typeof client.captureAiImmediate === 'function') {
|
|
34
|
+
await client.captureAiImmediate(event);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await client.captureImmediate(event);
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
const DATA_URL_PREFIX_RE = /^data:([^;,\s]+)(?:;[^;,\s]+)*;base64,/i;
|
|
14
41
|
const BASE64_ALPHABET_RE = /^[A-Za-z0-9+/_=-]+$/;
|
|
15
42
|
class Base64Recognizer {
|
|
@@ -115,7 +142,6 @@ class BinaryContentRedactor {
|
|
|
115
142
|
this.recognizer = recognizer;
|
|
116
143
|
}
|
|
117
144
|
redact(value, mediaType) {
|
|
118
|
-
if (this.isMultimodalEnabled()) return value;
|
|
119
145
|
this.visited = new WeakSet();
|
|
120
146
|
return this.walk(value, mediaType ? new MediaTypeContext(undefined, undefined, mediaType) : MediaTypeContext.EMPTY);
|
|
121
147
|
}
|
|
@@ -159,24 +185,26 @@ class BinaryContentRedactor {
|
|
|
159
185
|
if (mediaType === 'application/octet-stream') return '[base64 file redacted]';
|
|
160
186
|
return `[base64 ${mediaType} redacted]`;
|
|
161
187
|
}
|
|
162
|
-
isMultimodalEnabled() {
|
|
163
|
-
const val = process.env._INTERNAL_LLMA_MULTIMODAL || '';
|
|
164
|
-
return val.toLowerCase() === 'true' || val === '1' || val.toLowerCase() === 'yes';
|
|
165
|
-
}
|
|
166
188
|
}
|
|
167
189
|
|
|
168
190
|
const redactor = new BinaryContentRedactor();
|
|
169
191
|
function redactBase64DataUrl(str, mediaType) {
|
|
170
192
|
return redactor.redact(str, mediaType);
|
|
171
193
|
}
|
|
172
|
-
const
|
|
194
|
+
const sanitize = (data, client) => isFullAiCaptureEnabled(client) ? data : redactor.redact(data);
|
|
195
|
+
const sanitizeVercel = (data, client) => sanitize(data, client);
|
|
173
196
|
|
|
174
197
|
const TOKEN_PROPERTY_KEYS = new Set(['$ai_input_tokens', '$ai_output_tokens', '$ai_cache_read_input_tokens', '$ai_cache_creation_input_tokens', '$ai_total_tokens', '$ai_reasoning_tokens']);
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether the caller supplied their own token counts, which override the ones the SDK
|
|
201
|
+
* derived from the provider response.
|
|
202
|
+
*/
|
|
203
|
+
function hasTokenOverrides(posthogProperties) {
|
|
204
|
+
return !!posthogProperties && Object.keys(posthogProperties).some(key => TOKEN_PROPERTY_KEYS.has(key));
|
|
205
|
+
}
|
|
175
206
|
function getTokensSource(posthogProperties) {
|
|
176
|
-
|
|
177
|
-
return 'passthrough';
|
|
178
|
-
}
|
|
179
|
-
return 'sdk';
|
|
207
|
+
return hasTokenOverrides(posthogProperties) ? 'passthrough' : 'sdk';
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
// limit large outputs by truncating to 200kb (approx 200k bytes)
|
|
@@ -246,11 +274,14 @@ function toSafeString(input) {
|
|
|
246
274
|
return '';
|
|
247
275
|
}
|
|
248
276
|
}
|
|
249
|
-
const truncate = input => {
|
|
277
|
+
const truncate = (input, client) => {
|
|
250
278
|
const str = toSafeString(input);
|
|
251
279
|
if (str === '') {
|
|
252
280
|
return '';
|
|
253
281
|
}
|
|
282
|
+
if (isFullAiCaptureEnabled(client)) {
|
|
283
|
+
return str;
|
|
284
|
+
}
|
|
254
285
|
|
|
255
286
|
// Check if we need to truncate and ensure STRING_FORMAT is respected
|
|
256
287
|
const buffer = sharedTextEncoder.encode(str);
|
|
@@ -406,7 +437,7 @@ function sanitizeValues(obj) {
|
|
|
406
437
|
return jsonSafe;
|
|
407
438
|
}
|
|
408
439
|
|
|
409
|
-
var version = "8.
|
|
440
|
+
var version = "8.8.0";
|
|
410
441
|
|
|
411
442
|
const DEFAULT_MAX_DEPTH = 3;
|
|
412
443
|
const MAX_STACK_LINES = 20;
|
|
@@ -573,6 +604,10 @@ const captureAiGeneration = async (client, options) => {
|
|
|
573
604
|
$ai_total_cost_usd: inputCostUSD + outputCostUSD
|
|
574
605
|
};
|
|
575
606
|
}
|
|
607
|
+
|
|
608
|
+
// The caller's own token counts override the SDK-derived ones further down, via the
|
|
609
|
+
// `options.properties` spread.
|
|
610
|
+
const tokensOverridden = hasTokenOverrides(options.properties);
|
|
576
611
|
const additionalTokenValues = {
|
|
577
612
|
...(usage.reasoningTokens ? {
|
|
578
613
|
$ai_reasoning_tokens: usage.reasoningTokens
|
|
@@ -583,6 +618,18 @@ const captureAiGeneration = async (client, options) => {
|
|
|
583
618
|
...(usage.cacheCreationInputTokens ? {
|
|
584
619
|
$ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
|
|
585
620
|
} : {}),
|
|
621
|
+
// Checked against undefined rather than truthiness, because false is the meaningful
|
|
622
|
+
// value here and a truthiness guard would drop it.
|
|
623
|
+
//
|
|
624
|
+
// Dropped entirely when the caller overrides the token counts: the flag describes how
|
|
625
|
+
// the SDK-derived counts relate to each other, so against passthrough counts it can be
|
|
626
|
+
// wrong in the expensive direction. Declaring inclusive over counts that are actually
|
|
627
|
+
// exclusive makes ingestion subtract the cache pool that was never in the input. A
|
|
628
|
+
// caller who knows their own accounting model can still pass
|
|
629
|
+
// `$ai_cache_reporting_exclusive` themselves, and that value wins.
|
|
630
|
+
...(usage.cacheReportingExclusive !== undefined && !tokensOverridden ? {
|
|
631
|
+
$ai_cache_reporting_exclusive: usage.cacheReportingExclusive
|
|
632
|
+
} : {}),
|
|
586
633
|
...(usage.webSearchCount ? {
|
|
587
634
|
$ai_web_search_count: usage.webSearchCount
|
|
588
635
|
} : {}),
|
|
@@ -639,9 +686,9 @@ const captureAiGeneration = async (client, options) => {
|
|
|
639
686
|
groups: options.groups
|
|
640
687
|
};
|
|
641
688
|
if (options.captureImmediate) {
|
|
642
|
-
await client
|
|
689
|
+
await captureAiEventImmediate(client, event);
|
|
643
690
|
} else {
|
|
644
|
-
client
|
|
691
|
+
captureAiEvent(client, event);
|
|
645
692
|
}
|
|
646
693
|
} catch (error) {
|
|
647
694
|
// Telemetry failures must never affect the instrumented provider call.
|
|
@@ -664,12 +711,12 @@ function getSpecificationVersion(model) {
|
|
|
664
711
|
|
|
665
712
|
// Content types for the output array
|
|
666
713
|
|
|
667
|
-
const redactFileData = (data, mediaType) => {
|
|
714
|
+
const redactFileData = (data, mediaType, client) => {
|
|
668
715
|
if (data instanceof URL) {
|
|
669
|
-
return redactBase64DataUrl(data.toString(), data.protocol === 'data:' ? mediaType : undefined);
|
|
716
|
+
return isFullAiCaptureEnabled(client) ? data.toString() : redactBase64DataUrl(data.toString(), data.protocol === 'data:' ? mediaType : undefined);
|
|
670
717
|
}
|
|
671
718
|
if (isString(data)) {
|
|
672
|
-
return redactBase64DataUrl(data, mediaType);
|
|
719
|
+
return isFullAiCaptureEnabled(client) ? data : redactBase64DataUrl(data, mediaType);
|
|
673
720
|
}
|
|
674
721
|
return undefined;
|
|
675
722
|
};
|
|
@@ -684,7 +731,7 @@ const mapVercelParams = params => {
|
|
|
684
731
|
stream: params.stream
|
|
685
732
|
};
|
|
686
733
|
};
|
|
687
|
-
const mapVercelPrompt = messages => {
|
|
734
|
+
const mapVercelPrompt = (messages, client) => {
|
|
688
735
|
// Map and truncate individual content
|
|
689
736
|
const inputs = messages.map(message => {
|
|
690
737
|
let content;
|
|
@@ -693,7 +740,7 @@ const mapVercelPrompt = messages => {
|
|
|
693
740
|
if (message.role === 'system') {
|
|
694
741
|
content = [{
|
|
695
742
|
type: 'text',
|
|
696
|
-
text: truncate(toContentString(message.content))
|
|
743
|
+
text: truncate(toContentString(message.content), client)
|
|
697
744
|
}];
|
|
698
745
|
} else {
|
|
699
746
|
// Handle other roles which have array content
|
|
@@ -702,11 +749,11 @@ const mapVercelPrompt = messages => {
|
|
|
702
749
|
if (c.type === 'text') {
|
|
703
750
|
return {
|
|
704
751
|
type: 'text',
|
|
705
|
-
text: truncate(c.text)
|
|
752
|
+
text: truncate(c.text, client)
|
|
706
753
|
};
|
|
707
754
|
} else if (c.type === 'file') {
|
|
708
755
|
// Redact base64 data URLs and raw base64 to prevent oversized events
|
|
709
|
-
const fileData = redactFileData(c.data, c.mediaType) ?? 'raw files not supported';
|
|
756
|
+
const fileData = redactFileData(c.data, c.mediaType, client) ?? 'raw files not supported';
|
|
710
757
|
return {
|
|
711
758
|
type: 'file',
|
|
712
759
|
file: fileData,
|
|
@@ -715,7 +762,7 @@ const mapVercelPrompt = messages => {
|
|
|
715
762
|
} else if (c.type === 'reasoning') {
|
|
716
763
|
return {
|
|
717
764
|
type: 'reasoning',
|
|
718
|
-
text: truncate(c.text)
|
|
765
|
+
text: truncate(c.text, client)
|
|
719
766
|
};
|
|
720
767
|
} else if (c.type === 'tool-call') {
|
|
721
768
|
return {
|
|
@@ -729,7 +776,7 @@ const mapVercelPrompt = messages => {
|
|
|
729
776
|
type: 'tool-result',
|
|
730
777
|
toolCallId: c.toolCallId,
|
|
731
778
|
toolName: c.toolName,
|
|
732
|
-
output: sanitizeVercel(c.output),
|
|
779
|
+
output: sanitizeVercel(c.output, client),
|
|
733
780
|
isError: c.isError
|
|
734
781
|
};
|
|
735
782
|
}
|
|
@@ -742,7 +789,7 @@ const mapVercelPrompt = messages => {
|
|
|
742
789
|
// Fallback for non-array content
|
|
743
790
|
content = [{
|
|
744
791
|
type: 'text',
|
|
745
|
-
text: truncate(toContentString(message.content))
|
|
792
|
+
text: truncate(toContentString(message.content), client)
|
|
746
793
|
}];
|
|
747
794
|
}
|
|
748
795
|
}
|
|
@@ -751,6 +798,12 @@ const mapVercelPrompt = messages => {
|
|
|
751
798
|
content
|
|
752
799
|
};
|
|
753
800
|
});
|
|
801
|
+
|
|
802
|
+
// Full AI capture means no truncation of any kind; the aggregate trim below exists
|
|
803
|
+
// only to keep the default-mode payload under MAX_OUTPUT_SIZE.
|
|
804
|
+
if (isFullAiCaptureEnabled(client)) {
|
|
805
|
+
return inputs;
|
|
806
|
+
}
|
|
754
807
|
try {
|
|
755
808
|
// Trim the inputs array until its serialized JSON size fits within MAX_OUTPUT_SIZE.
|
|
756
809
|
// Pre-compute each message's byte size once so we can shift by accumulated budget
|
|
@@ -787,12 +840,12 @@ const mapVercelPrompt = messages => {
|
|
|
787
840
|
}
|
|
788
841
|
return inputs;
|
|
789
842
|
};
|
|
790
|
-
const mapVercelOutput = result => {
|
|
843
|
+
const mapVercelOutput = (result, client) => {
|
|
791
844
|
const content = result.map(item => {
|
|
792
845
|
if (item.type === 'text') {
|
|
793
846
|
return {
|
|
794
847
|
type: 'text',
|
|
795
|
-
text: truncate(item.text)
|
|
848
|
+
text: truncate(item.text, client)
|
|
796
849
|
};
|
|
797
850
|
}
|
|
798
851
|
if (item.type === 'tool-call') {
|
|
@@ -810,15 +863,15 @@ const mapVercelOutput = result => {
|
|
|
810
863
|
if (item.type === 'reasoning') {
|
|
811
864
|
return {
|
|
812
865
|
type: 'reasoning',
|
|
813
|
-
text: truncate(item.text)
|
|
866
|
+
text: truncate(item.text, client)
|
|
814
867
|
};
|
|
815
868
|
}
|
|
816
869
|
if (item.type === 'file') {
|
|
817
870
|
// Handle files similar to input mapping - avoid large base64 data
|
|
818
|
-
let fileData = redactFileData(item.data, item.mediaType) ?? `[binary ${item.mediaType} file]`;
|
|
871
|
+
let fileData = redactFileData(item.data, item.mediaType, client) ?? `[binary ${item.mediaType} file]`;
|
|
819
872
|
|
|
820
|
-
//
|
|
821
|
-
if (typeof item.data === 'string' && fileData === item.data && item.data.length > 1000) {
|
|
873
|
+
// Skipped under full AI capture: media stays untouched, so no placeholder swap either.
|
|
874
|
+
if (!isFullAiCaptureEnabled(client) && typeof item.data === 'string' && fileData === item.data && item.data.length > 1000) {
|
|
822
875
|
fileData = `[${item.mediaType} file - ${item.data.length} bytes]`;
|
|
823
876
|
}
|
|
824
877
|
return {
|
|
@@ -840,7 +893,7 @@ const mapVercelOutput = result => {
|
|
|
840
893
|
// Fallback for unknown types - try to extract text if possible
|
|
841
894
|
return {
|
|
842
895
|
type: 'text',
|
|
843
|
-
text: truncate(JSON.stringify(item))
|
|
896
|
+
text: truncate(JSON.stringify(item), client)
|
|
844
897
|
};
|
|
845
898
|
});
|
|
846
899
|
if (content.length > 0) {
|
|
@@ -853,7 +906,7 @@ const mapVercelOutput = result => {
|
|
|
853
906
|
try {
|
|
854
907
|
const jsonOutput = JSON.stringify(result);
|
|
855
908
|
return [{
|
|
856
|
-
content: truncate(jsonOutput),
|
|
909
|
+
content: truncate(jsonOutput, client),
|
|
857
910
|
role: 'assistant'
|
|
858
911
|
}];
|
|
859
912
|
} catch {
|
|
@@ -1049,7 +1102,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
|
|
|
1049
1102
|
const modelId = mergedOptions.posthogModelOverride ?? (result.response?.modelId ? result.response.modelId : model.modelId);
|
|
1050
1103
|
const provider = mergedOptions.posthogProviderOverride ?? extractProvider(model);
|
|
1051
1104
|
// result.content is undefined when the model returns only tool calls with no text output
|
|
1052
|
-
const content = mapVercelOutput(result.content ?? []);
|
|
1105
|
+
const content = mapVercelOutput(result.content ?? [], phClient);
|
|
1053
1106
|
const latency = (Date.now() - startTime) / 1000;
|
|
1054
1107
|
const providerMetadata = result.providerMetadata;
|
|
1055
1108
|
const additionalTokenValues = extractAdditionalTokenValues(providerMetadata, result.usage);
|
|
@@ -1092,7 +1145,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
|
|
|
1092
1145
|
...baseOptions,
|
|
1093
1146
|
model: modelId,
|
|
1094
1147
|
provider: provider,
|
|
1095
|
-
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
|
|
1148
|
+
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
|
|
1096
1149
|
output: content,
|
|
1097
1150
|
latency,
|
|
1098
1151
|
baseURL,
|
|
@@ -1109,7 +1162,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
|
|
|
1109
1162
|
...baseOptions,
|
|
1110
1163
|
model: modelId,
|
|
1111
1164
|
provider: model.provider,
|
|
1112
|
-
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
|
|
1165
|
+
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
|
|
1113
1166
|
output: [],
|
|
1114
1167
|
latency: 0,
|
|
1115
1168
|
baseURL,
|
|
@@ -1243,13 +1296,13 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
|
|
|
1243
1296
|
if (reasoningText) {
|
|
1244
1297
|
content.push({
|
|
1245
1298
|
type: 'reasoning',
|
|
1246
|
-
text: truncate(reasoningText)
|
|
1299
|
+
text: truncate(reasoningText, phClient)
|
|
1247
1300
|
});
|
|
1248
1301
|
}
|
|
1249
1302
|
if (generatedText) {
|
|
1250
1303
|
content.push({
|
|
1251
1304
|
type: 'text',
|
|
1252
|
-
text: truncate(generatedText)
|
|
1305
|
+
text: truncate(generatedText, phClient)
|
|
1253
1306
|
});
|
|
1254
1307
|
}
|
|
1255
1308
|
for (const toolCall of toolCallsInProgress.values()) {
|
|
@@ -1284,7 +1337,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
|
|
|
1284
1337
|
...baseOptions,
|
|
1285
1338
|
model: modelId,
|
|
1286
1339
|
provider: provider,
|
|
1287
|
-
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
|
|
1340
|
+
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
|
|
1288
1341
|
output,
|
|
1289
1342
|
latency,
|
|
1290
1343
|
timeToFirstToken,
|
|
@@ -1340,7 +1393,7 @@ const wrapVercelLanguageModel = (model, phClient, options) => {
|
|
|
1340
1393
|
...baseOptions,
|
|
1341
1394
|
model: modelId,
|
|
1342
1395
|
provider: provider,
|
|
1343
|
-
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt),
|
|
1396
|
+
input: mergedOptions.posthogPrivacyMode ? '' : mapVercelPrompt(params.prompt, phClient),
|
|
1344
1397
|
output: [],
|
|
1345
1398
|
latency: 0,
|
|
1346
1399
|
baseURL,
|