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