@posthog/ai 6.5.0 → 6.6.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 +13 -3
- package/dist/anthropic/index.cjs.map +1 -1
- package/dist/anthropic/index.mjs +13 -3
- package/dist/anthropic/index.mjs.map +1 -1
- package/dist/gemini/index.cjs +74 -5
- package/dist/gemini/index.cjs.map +1 -1
- package/dist/gemini/index.mjs +74 -5
- package/dist/gemini/index.mjs.map +1 -1
- package/dist/index.cjs +306 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.mjs +306 -28
- package/dist/index.mjs.map +1 -1
- package/dist/langchain/index.cjs +48 -1
- package/dist/langchain/index.cjs.map +1 -1
- package/dist/langchain/index.mjs +48 -1
- package/dist/langchain/index.mjs.map +1 -1
- package/dist/openai/index.cjs +151 -15
- package/dist/openai/index.cjs.map +1 -1
- package/dist/openai/index.d.ts +1 -1
- package/dist/openai/index.mjs +151 -15
- package/dist/openai/index.mjs.map +1 -1
- package/dist/vercel/index.cjs +156 -8
- package/dist/vercel/index.cjs.map +1 -1
- package/dist/vercel/index.mjs +156 -8
- package/dist/vercel/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ interface MonitoringOpenAIConfig$1 extends ClientOptions$1 {
|
|
|
54
54
|
posthog: PostHog;
|
|
55
55
|
baseURL?: string;
|
|
56
56
|
}
|
|
57
|
-
type RequestOptions$2 = Record<string,
|
|
57
|
+
type RequestOptions$2 = Record<string, unknown>;
|
|
58
58
|
declare class PostHogOpenAI extends OpenAI {
|
|
59
59
|
private readonly phClient;
|
|
60
60
|
chat: WrappedChat$1;
|
package/dist/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { wrapLanguageModel } from 'ai';
|
|
|
6
6
|
import AnthropicOriginal from '@anthropic-ai/sdk';
|
|
7
7
|
import { GoogleGenAI } from '@google/genai';
|
|
8
8
|
|
|
9
|
-
var version = "6.
|
|
9
|
+
var version = "6.6.0";
|
|
10
10
|
|
|
11
11
|
// Type guards for safer type checking
|
|
12
12
|
const isString = value => {
|
|
@@ -277,6 +277,100 @@ const truncate = input => {
|
|
|
277
277
|
}
|
|
278
278
|
return `${truncatedStr}... [truncated]`;
|
|
279
279
|
};
|
|
280
|
+
/**
|
|
281
|
+
* Calculate web search count from raw API response.
|
|
282
|
+
*
|
|
283
|
+
* Uses a two-tier detection strategy:
|
|
284
|
+
* Priority 1 (Exact Count): Count actual web search calls when available
|
|
285
|
+
* Priority 2 (Binary Detection): Return 1 if web search indicators are present, 0 otherwise
|
|
286
|
+
*
|
|
287
|
+
* @param result - Raw API response from any provider (OpenAI, Perplexity, OpenRouter, Gemini, etc.)
|
|
288
|
+
* @returns Number of web searches performed (exact count or binary 1/0)
|
|
289
|
+
*/
|
|
290
|
+
function calculateWebSearchCount(result) {
|
|
291
|
+
if (!result || typeof result !== 'object') {
|
|
292
|
+
return 0;
|
|
293
|
+
}
|
|
294
|
+
// Priority 1: Exact Count
|
|
295
|
+
// Check for OpenAI Responses API web_search_call items
|
|
296
|
+
if ('output' in result && Array.isArray(result.output)) {
|
|
297
|
+
let count = 0;
|
|
298
|
+
for (const item of result.output) {
|
|
299
|
+
if (typeof item === 'object' && item !== null && 'type' in item && item.type === 'web_search_call') {
|
|
300
|
+
count++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (count > 0) {
|
|
304
|
+
return count;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Priority 2: Binary Detection (1 or 0)
|
|
308
|
+
// Check for citations at root level (Perplexity)
|
|
309
|
+
if ('citations' in result && Array.isArray(result.citations) && result.citations.length > 0) {
|
|
310
|
+
return 1;
|
|
311
|
+
}
|
|
312
|
+
// Check for search_results at root level (Perplexity via OpenRouter)
|
|
313
|
+
if ('search_results' in result && Array.isArray(result.search_results) && result.search_results.length > 0) {
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
// Check for usage.search_context_size (Perplexity via OpenRouter)
|
|
317
|
+
if ('usage' in result && typeof result.usage === 'object' && result.usage !== null) {
|
|
318
|
+
if ('search_context_size' in result.usage && result.usage.search_context_size) {
|
|
319
|
+
return 1;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Check for annotations with url_citation in choices[].message (OpenAI/Perplexity)
|
|
323
|
+
if ('choices' in result && Array.isArray(result.choices)) {
|
|
324
|
+
for (const choice of result.choices) {
|
|
325
|
+
if (typeof choice === 'object' && choice !== null && 'message' in choice) {
|
|
326
|
+
const message = choice.message;
|
|
327
|
+
if (typeof message === 'object' && message !== null && 'annotations' in message) {
|
|
328
|
+
const annotations = message.annotations;
|
|
329
|
+
if (Array.isArray(annotations)) {
|
|
330
|
+
const hasUrlCitation = annotations.some(ann => {
|
|
331
|
+
return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
|
|
332
|
+
});
|
|
333
|
+
if (hasUrlCitation) {
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Check for annotations in output[].content[] (OpenAI Responses API)
|
|
342
|
+
if ('output' in result && Array.isArray(result.output)) {
|
|
343
|
+
for (const item of result.output) {
|
|
344
|
+
if (typeof item === 'object' && item !== null && 'content' in item) {
|
|
345
|
+
const content = item.content;
|
|
346
|
+
if (Array.isArray(content)) {
|
|
347
|
+
for (const contentItem of content) {
|
|
348
|
+
if (typeof contentItem === 'object' && contentItem !== null && 'annotations' in contentItem) {
|
|
349
|
+
const annotations = contentItem.annotations;
|
|
350
|
+
if (Array.isArray(annotations)) {
|
|
351
|
+
const hasUrlCitation = annotations.some(ann => {
|
|
352
|
+
return typeof ann === 'object' && ann !== null && 'type' in ann && ann.type === 'url_citation';
|
|
353
|
+
});
|
|
354
|
+
if (hasUrlCitation) {
|
|
355
|
+
return 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Check for grounding_metadata (Gemini)
|
|
365
|
+
if ('candidates' in result && Array.isArray(result.candidates)) {
|
|
366
|
+
for (const candidate of result.candidates) {
|
|
367
|
+
if (typeof candidate === 'object' && candidate !== null && 'grounding_metadata' in candidate && candidate.grounding_metadata) {
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
280
374
|
/**
|
|
281
375
|
* Extract available tool calls from the request parameters.
|
|
282
376
|
* These are the tools provided to the LLM, not the tool calls in the response.
|
|
@@ -411,6 +505,9 @@ const sendEventToPosthog = async ({
|
|
|
411
505
|
} : {}),
|
|
412
506
|
...(usage.cacheCreationInputTokens ? {
|
|
413
507
|
$ai_cache_creation_input_tokens: usage.cacheCreationInputTokens
|
|
508
|
+
} : {}),
|
|
509
|
+
...(usage.webSearchCount ? {
|
|
510
|
+
$ai_web_search_count: usage.webSearchCount
|
|
414
511
|
} : {})
|
|
415
512
|
};
|
|
416
513
|
const properties = {
|
|
@@ -721,12 +818,17 @@ let WrappedCompletions$1 = class WrappedCompletions extends Completions {
|
|
|
721
818
|
let accumulatedContent = '';
|
|
722
819
|
let usage = {
|
|
723
820
|
inputTokens: 0,
|
|
724
|
-
outputTokens: 0
|
|
821
|
+
outputTokens: 0,
|
|
822
|
+
webSearchCount: 0
|
|
725
823
|
};
|
|
726
824
|
// Map to track in-progress tool calls
|
|
727
825
|
const toolCallsInProgress = new Map();
|
|
728
826
|
for await (const chunk of stream1) {
|
|
729
827
|
const choice = chunk?.choices?.[0];
|
|
828
|
+
const chunkWebSearchCount = calculateWebSearchCount(chunk);
|
|
829
|
+
if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
|
|
830
|
+
usage.webSearchCount = chunkWebSearchCount;
|
|
831
|
+
}
|
|
730
832
|
// Handle text content
|
|
731
833
|
const deltaContent = choice?.delta?.content;
|
|
732
834
|
if (deltaContent) {
|
|
@@ -765,6 +867,7 @@ let WrappedCompletions$1 = class WrappedCompletions extends Completions {
|
|
|
765
867
|
// Handle usage information
|
|
766
868
|
if (chunk.usage) {
|
|
767
869
|
usage = {
|
|
870
|
+
...usage,
|
|
768
871
|
inputTokens: chunk.usage.prompt_tokens ?? 0,
|
|
769
872
|
outputTokens: chunk.usage.completion_tokens ?? 0,
|
|
770
873
|
reasoningTokens: chunk.usage.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
@@ -816,7 +919,13 @@ let WrappedCompletions$1 = class WrappedCompletions extends Completions {
|
|
|
816
919
|
baseURL: this.baseURL,
|
|
817
920
|
params: body,
|
|
818
921
|
httpStatus: 200,
|
|
819
|
-
usage
|
|
922
|
+
usage: {
|
|
923
|
+
inputTokens: usage.inputTokens,
|
|
924
|
+
outputTokens: usage.outputTokens,
|
|
925
|
+
reasoningTokens: usage.reasoningTokens,
|
|
926
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
927
|
+
webSearchCount: usage.webSearchCount
|
|
928
|
+
},
|
|
820
929
|
tools: availableTools
|
|
821
930
|
});
|
|
822
931
|
} catch (error) {
|
|
@@ -851,13 +960,14 @@ let WrappedCompletions$1 = class WrappedCompletions extends Completions {
|
|
|
851
960
|
if ('choices' in result) {
|
|
852
961
|
const latency = (Date.now() - startTime) / 1000;
|
|
853
962
|
const availableTools = extractAvailableToolCalls('openai', openAIParams);
|
|
963
|
+
const formattedOutput = formatResponseOpenAI(result);
|
|
854
964
|
await sendEventToPosthog({
|
|
855
965
|
client: this.phClient,
|
|
856
966
|
...posthogParams,
|
|
857
967
|
model: openAIParams.model,
|
|
858
968
|
provider: 'openai',
|
|
859
969
|
input: sanitizeOpenAI(openAIParams.messages),
|
|
860
|
-
output:
|
|
970
|
+
output: formattedOutput,
|
|
861
971
|
latency,
|
|
862
972
|
baseURL: this.baseURL,
|
|
863
973
|
params: body,
|
|
@@ -866,7 +976,8 @@ let WrappedCompletions$1 = class WrappedCompletions extends Completions {
|
|
|
866
976
|
inputTokens: result.usage?.prompt_tokens ?? 0,
|
|
867
977
|
outputTokens: result.usage?.completion_tokens ?? 0,
|
|
868
978
|
reasoningTokens: result.usage?.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
869
|
-
cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0
|
|
979
|
+
cacheReadInputTokens: result.usage?.prompt_tokens_details?.cached_tokens ?? 0,
|
|
980
|
+
webSearchCount: calculateWebSearchCount(result)
|
|
870
981
|
},
|
|
871
982
|
tools: availableTools
|
|
872
983
|
});
|
|
@@ -921,14 +1032,22 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
921
1032
|
let finalContent = [];
|
|
922
1033
|
let usage = {
|
|
923
1034
|
inputTokens: 0,
|
|
924
|
-
outputTokens: 0
|
|
1035
|
+
outputTokens: 0,
|
|
1036
|
+
webSearchCount: 0
|
|
925
1037
|
};
|
|
926
1038
|
for await (const chunk of stream1) {
|
|
1039
|
+
if ('response' in chunk && chunk.response) {
|
|
1040
|
+
const chunkWebSearchCount = calculateWebSearchCount(chunk.response);
|
|
1041
|
+
if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
|
|
1042
|
+
usage.webSearchCount = chunkWebSearchCount;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
927
1045
|
if (chunk.type === 'response.completed' && 'response' in chunk && chunk.response?.output && chunk.response.output.length > 0) {
|
|
928
1046
|
finalContent = chunk.response.output;
|
|
929
1047
|
}
|
|
930
1048
|
if ('response' in chunk && chunk.response?.usage) {
|
|
931
1049
|
usage = {
|
|
1050
|
+
...usage,
|
|
932
1051
|
inputTokens: chunk.response.usage.input_tokens ?? 0,
|
|
933
1052
|
outputTokens: chunk.response.usage.output_tokens ?? 0,
|
|
934
1053
|
reasoningTokens: chunk.response.usage.output_tokens_details?.reasoning_tokens ?? 0,
|
|
@@ -950,7 +1069,13 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
950
1069
|
baseURL: this.baseURL,
|
|
951
1070
|
params: body,
|
|
952
1071
|
httpStatus: 200,
|
|
953
|
-
usage
|
|
1072
|
+
usage: {
|
|
1073
|
+
inputTokens: usage.inputTokens,
|
|
1074
|
+
outputTokens: usage.outputTokens,
|
|
1075
|
+
reasoningTokens: usage.reasoningTokens,
|
|
1076
|
+
cacheReadInputTokens: usage.cacheReadInputTokens,
|
|
1077
|
+
webSearchCount: usage.webSearchCount
|
|
1078
|
+
},
|
|
954
1079
|
tools: availableTools
|
|
955
1080
|
});
|
|
956
1081
|
} catch (error) {
|
|
@@ -985,6 +1110,9 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
985
1110
|
if ('output' in result) {
|
|
986
1111
|
const latency = (Date.now() - startTime) / 1000;
|
|
987
1112
|
const availableTools = extractAvailableToolCalls('openai', openAIParams);
|
|
1113
|
+
const formattedOutput = formatResponseOpenAI({
|
|
1114
|
+
output: result.output
|
|
1115
|
+
});
|
|
988
1116
|
await sendEventToPosthog({
|
|
989
1117
|
client: this.phClient,
|
|
990
1118
|
...posthogParams,
|
|
@@ -992,9 +1120,7 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
992
1120
|
model: openAIParams.model,
|
|
993
1121
|
provider: 'openai',
|
|
994
1122
|
input: formatOpenAIResponsesInput(openAIParams.input, openAIParams.instructions),
|
|
995
|
-
output:
|
|
996
|
-
output: result.output
|
|
997
|
-
}),
|
|
1123
|
+
output: formattedOutput,
|
|
998
1124
|
latency,
|
|
999
1125
|
baseURL: this.baseURL,
|
|
1000
1126
|
params: body,
|
|
@@ -1003,7 +1129,8 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
1003
1129
|
inputTokens: result.usage?.input_tokens ?? 0,
|
|
1004
1130
|
outputTokens: result.usage?.output_tokens ?? 0,
|
|
1005
1131
|
reasoningTokens: result.usage?.output_tokens_details?.reasoning_tokens ?? 0,
|
|
1006
|
-
cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0
|
|
1132
|
+
cacheReadInputTokens: result.usage?.input_tokens_details?.cached_tokens ?? 0,
|
|
1133
|
+
webSearchCount: calculateWebSearchCount(result)
|
|
1007
1134
|
},
|
|
1008
1135
|
tools: availableTools
|
|
1009
1136
|
});
|
|
@@ -1041,9 +1168,9 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
1041
1168
|
} = extractPosthogParams(body);
|
|
1042
1169
|
const startTime = Date.now();
|
|
1043
1170
|
const originalCreate = super.create.bind(this);
|
|
1044
|
-
const
|
|
1045
|
-
const tempCreate =
|
|
1046
|
-
|
|
1171
|
+
const originalSelfRecord = this;
|
|
1172
|
+
const tempCreate = originalSelfRecord['create'];
|
|
1173
|
+
originalSelfRecord['create'] = originalCreate;
|
|
1047
1174
|
try {
|
|
1048
1175
|
const parentPromise = super.parse(openAIParams, options);
|
|
1049
1176
|
const wrappedPromise = parentPromise.then(async result => {
|
|
@@ -1092,7 +1219,7 @@ let WrappedResponses$1 = class WrappedResponses extends Responses {
|
|
|
1092
1219
|
return wrappedPromise;
|
|
1093
1220
|
} finally {
|
|
1094
1221
|
// Restore our wrapped create method
|
|
1095
|
-
|
|
1222
|
+
originalSelfRecord['create'] = tempCreate;
|
|
1096
1223
|
}
|
|
1097
1224
|
}
|
|
1098
1225
|
};
|
|
@@ -1838,11 +1965,29 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
|
|
|
1838
1965
|
cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
|
|
1839
1966
|
} : {})
|
|
1840
1967
|
};
|
|
1968
|
+
// Calculate web search count based on provider
|
|
1969
|
+
let webSearchCount = 0;
|
|
1970
|
+
if (providerMetadata?.anthropic && typeof providerMetadata.anthropic === 'object' && 'server_tool_use' in providerMetadata.anthropic) {
|
|
1971
|
+
// Anthropic-specific extraction
|
|
1972
|
+
const serverToolUse = providerMetadata.anthropic.server_tool_use;
|
|
1973
|
+
if (serverToolUse && typeof serverToolUse === 'object' && 'web_search_requests' in serverToolUse && typeof serverToolUse.web_search_requests === 'number') {
|
|
1974
|
+
webSearchCount = serverToolUse.web_search_requests;
|
|
1975
|
+
}
|
|
1976
|
+
} else {
|
|
1977
|
+
// For other providers through Vercel, pass available metadata to helper
|
|
1978
|
+
// Note: Vercel abstracts provider responses, so we may not have access to
|
|
1979
|
+
// raw citations/annotations unless Vercel exposes them in usage/metadata
|
|
1980
|
+
webSearchCount = calculateWebSearchCount({
|
|
1981
|
+
usage: result.usage,
|
|
1982
|
+
providerMetadata: providerMetadata
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1841
1985
|
const usage = {
|
|
1842
1986
|
inputTokens: result.usage.inputTokens,
|
|
1843
1987
|
outputTokens: result.usage.outputTokens,
|
|
1844
1988
|
reasoningTokens: result.usage.reasoningTokens,
|
|
1845
1989
|
cacheReadInputTokens: result.usage.cachedInputTokens,
|
|
1990
|
+
webSearchCount,
|
|
1846
1991
|
...additionalTokenValues
|
|
1847
1992
|
};
|
|
1848
1993
|
await sendEventToPosthog({
|
|
@@ -1896,6 +2041,7 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
|
|
|
1896
2041
|
let generatedText = '';
|
|
1897
2042
|
let reasoningText = '';
|
|
1898
2043
|
let usage = {};
|
|
2044
|
+
let providerMetadata = undefined;
|
|
1899
2045
|
const mergedParams = {
|
|
1900
2046
|
...options,
|
|
1901
2047
|
...mapVercelParams(params),
|
|
@@ -1953,12 +2099,10 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
|
|
|
1953
2099
|
});
|
|
1954
2100
|
}
|
|
1955
2101
|
if (chunk.type === 'finish') {
|
|
1956
|
-
|
|
1957
|
-
const additionalTokenValues = {
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
} : {})
|
|
1961
|
-
};
|
|
2102
|
+
providerMetadata = chunk.providerMetadata;
|
|
2103
|
+
const additionalTokenValues = providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'cacheCreationInputTokens' in providerMetadata.anthropic ? {
|
|
2104
|
+
cacheCreationInputTokens: providerMetadata.anthropic.cacheCreationInputTokens
|
|
2105
|
+
} : {};
|
|
1962
2106
|
usage = {
|
|
1963
2107
|
inputTokens: chunk.usage?.inputTokens,
|
|
1964
2108
|
outputTokens: chunk.usage?.outputTokens,
|
|
@@ -2003,6 +2147,28 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
|
|
|
2003
2147
|
role: 'assistant',
|
|
2004
2148
|
content: content.length === 1 && content[0].type === 'text' ? content[0].text : content
|
|
2005
2149
|
}] : [];
|
|
2150
|
+
// Calculate web search count based on provider
|
|
2151
|
+
let webSearchCount = 0;
|
|
2152
|
+
if (providerMetadata && typeof providerMetadata === 'object' && 'anthropic' in providerMetadata && providerMetadata.anthropic && typeof providerMetadata.anthropic === 'object' && 'server_tool_use' in providerMetadata.anthropic) {
|
|
2153
|
+
// Anthropic-specific extraction
|
|
2154
|
+
const serverToolUse = providerMetadata.anthropic.server_tool_use;
|
|
2155
|
+
if (serverToolUse && typeof serverToolUse === 'object' && 'web_search_requests' in serverToolUse && typeof serverToolUse.web_search_requests === 'number') {
|
|
2156
|
+
webSearchCount = serverToolUse.web_search_requests;
|
|
2157
|
+
}
|
|
2158
|
+
} else {
|
|
2159
|
+
// For other providers through Vercel, pass available metadata to helper
|
|
2160
|
+
// Note: Vercel abstracts provider responses, so we may not have access to
|
|
2161
|
+
// raw citations/annotations unless Vercel exposes them in usage/metadata
|
|
2162
|
+
webSearchCount = calculateWebSearchCount({
|
|
2163
|
+
usage: usage,
|
|
2164
|
+
providerMetadata: providerMetadata
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
// Update usage with web search count
|
|
2168
|
+
const finalUsage = {
|
|
2169
|
+
...usage,
|
|
2170
|
+
webSearchCount
|
|
2171
|
+
};
|
|
2006
2172
|
await sendEventToPosthog({
|
|
2007
2173
|
client: phClient,
|
|
2008
2174
|
distinctId: options.posthogDistinctId,
|
|
@@ -2015,7 +2181,7 @@ const createInstrumentationMiddleware = (phClient, model, options) => {
|
|
|
2015
2181
|
baseURL,
|
|
2016
2182
|
params: mergedParams,
|
|
2017
2183
|
httpStatus: 200,
|
|
2018
|
-
usage,
|
|
2184
|
+
usage: finalUsage,
|
|
2019
2185
|
tools: availableTools,
|
|
2020
2186
|
captureImmediate: options.posthogCaptureImmediate
|
|
2021
2187
|
});
|
|
@@ -2101,7 +2267,8 @@ class WrappedMessages extends AnthropicOriginal.Messages {
|
|
|
2101
2267
|
inputTokens: 0,
|
|
2102
2268
|
outputTokens: 0,
|
|
2103
2269
|
cacheCreationInputTokens: 0,
|
|
2104
|
-
cacheReadInputTokens: 0
|
|
2270
|
+
cacheReadInputTokens: 0,
|
|
2271
|
+
webSearchCount: 0
|
|
2105
2272
|
};
|
|
2106
2273
|
if ('tee' in value) {
|
|
2107
2274
|
const [stream1, stream2] = value.tee();
|
|
@@ -2178,9 +2345,14 @@ class WrappedMessages extends AnthropicOriginal.Messages {
|
|
|
2178
2345
|
usage.inputTokens = chunk.message.usage.input_tokens ?? 0;
|
|
2179
2346
|
usage.cacheCreationInputTokens = chunk.message.usage.cache_creation_input_tokens ?? 0;
|
|
2180
2347
|
usage.cacheReadInputTokens = chunk.message.usage.cache_read_input_tokens ?? 0;
|
|
2348
|
+
usage.webSearchCount = chunk.message.usage.server_tool_use?.web_search_requests ?? 0;
|
|
2181
2349
|
}
|
|
2182
2350
|
if ('usage' in chunk) {
|
|
2183
2351
|
usage.outputTokens = chunk.usage.output_tokens ?? 0;
|
|
2352
|
+
// Update web search count if present in delta
|
|
2353
|
+
if (chunk.usage.server_tool_use?.web_search_requests !== undefined) {
|
|
2354
|
+
usage.webSearchCount = chunk.usage.server_tool_use.web_search_requests;
|
|
2355
|
+
}
|
|
2184
2356
|
}
|
|
2185
2357
|
}
|
|
2186
2358
|
const latency = (Date.now() - startTime) / 1000;
|
|
@@ -2257,7 +2429,8 @@ class WrappedMessages extends AnthropicOriginal.Messages {
|
|
|
2257
2429
|
inputTokens: result.usage.input_tokens ?? 0,
|
|
2258
2430
|
outputTokens: result.usage.output_tokens ?? 0,
|
|
2259
2431
|
cacheCreationInputTokens: result.usage.cache_creation_input_tokens ?? 0,
|
|
2260
|
-
cacheReadInputTokens: result.usage.cache_read_input_tokens ?? 0
|
|
2432
|
+
cacheReadInputTokens: result.usage.cache_read_input_tokens ?? 0,
|
|
2433
|
+
webSearchCount: result.usage.server_tool_use?.web_search_requests ?? 0
|
|
2261
2434
|
},
|
|
2262
2435
|
tools: availableTools
|
|
2263
2436
|
});
|
|
@@ -2331,7 +2504,8 @@ class WrappedModels {
|
|
|
2331
2504
|
inputTokens: metadata?.promptTokenCount ?? 0,
|
|
2332
2505
|
outputTokens: metadata?.candidatesTokenCount ?? 0,
|
|
2333
2506
|
reasoningTokens: metadata?.thoughtsTokenCount ?? 0,
|
|
2334
|
-
cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0
|
|
2507
|
+
cacheReadInputTokens: metadata?.cachedContentTokenCount ?? 0,
|
|
2508
|
+
webSearchCount: calculateGoogleWebSearchCount(response)
|
|
2335
2509
|
},
|
|
2336
2510
|
tools: availableTools
|
|
2337
2511
|
});
|
|
@@ -2368,11 +2542,16 @@ class WrappedModels {
|
|
|
2368
2542
|
const accumulatedContent = [];
|
|
2369
2543
|
let usage = {
|
|
2370
2544
|
inputTokens: 0,
|
|
2371
|
-
outputTokens: 0
|
|
2545
|
+
outputTokens: 0,
|
|
2546
|
+
webSearchCount: 0
|
|
2372
2547
|
};
|
|
2373
2548
|
try {
|
|
2374
2549
|
const stream = await this.client.models.generateContentStream(geminiParams);
|
|
2375
2550
|
for await (const chunk of stream) {
|
|
2551
|
+
const chunkWebSearchCount = calculateGoogleWebSearchCount(chunk);
|
|
2552
|
+
if (chunkWebSearchCount > 0 && chunkWebSearchCount > (usage.webSearchCount ?? 0)) {
|
|
2553
|
+
usage.webSearchCount = chunkWebSearchCount;
|
|
2554
|
+
}
|
|
2376
2555
|
// Handle text content
|
|
2377
2556
|
if (chunk.text) {
|
|
2378
2557
|
// Find if we already have a text item to append to
|
|
@@ -2421,7 +2600,8 @@ class WrappedModels {
|
|
|
2421
2600
|
inputTokens: metadata.promptTokenCount ?? 0,
|
|
2422
2601
|
outputTokens: metadata.candidatesTokenCount ?? 0,
|
|
2423
2602
|
reasoningTokens: metadata.thoughtsTokenCount ?? 0,
|
|
2424
|
-
cacheReadInputTokens: metadata.cachedContentTokenCount ?? 0
|
|
2603
|
+
cacheReadInputTokens: metadata.cachedContentTokenCount ?? 0,
|
|
2604
|
+
webSearchCount: usage.webSearchCount
|
|
2425
2605
|
};
|
|
2426
2606
|
}
|
|
2427
2607
|
yield chunk;
|
|
@@ -2444,7 +2624,10 @@ class WrappedModels {
|
|
|
2444
2624
|
baseURL: 'https://generativelanguage.googleapis.com',
|
|
2445
2625
|
params: params,
|
|
2446
2626
|
httpStatus: 200,
|
|
2447
|
-
usage
|
|
2627
|
+
usage: {
|
|
2628
|
+
...usage,
|
|
2629
|
+
webSearchCount: usage.webSearchCount
|
|
2630
|
+
},
|
|
2448
2631
|
tools: availableTools
|
|
2449
2632
|
});
|
|
2450
2633
|
} catch (error) {
|
|
@@ -2587,6 +2770,57 @@ class WrappedModels {
|
|
|
2587
2770
|
return messages;
|
|
2588
2771
|
}
|
|
2589
2772
|
}
|
|
2773
|
+
/**
|
|
2774
|
+
* Detect if Google Search grounding was used in the response.
|
|
2775
|
+
* Gemini bills per request that uses grounding, not per individual query.
|
|
2776
|
+
* Returns 1 if grounding was used, 0 otherwise.
|
|
2777
|
+
*/
|
|
2778
|
+
function calculateGoogleWebSearchCount(response) {
|
|
2779
|
+
if (!response || typeof response !== 'object' || !('candidates' in response)) {
|
|
2780
|
+
return 0;
|
|
2781
|
+
}
|
|
2782
|
+
const candidates = response.candidates;
|
|
2783
|
+
if (!Array.isArray(candidates)) {
|
|
2784
|
+
return 0;
|
|
2785
|
+
}
|
|
2786
|
+
const hasGrounding = candidates.some(candidate => {
|
|
2787
|
+
if (!candidate || typeof candidate !== 'object') {
|
|
2788
|
+
return false;
|
|
2789
|
+
}
|
|
2790
|
+
// Check for grounding metadata
|
|
2791
|
+
if ('groundingMetadata' in candidate && candidate.groundingMetadata) {
|
|
2792
|
+
const metadata = candidate.groundingMetadata;
|
|
2793
|
+
if (typeof metadata === 'object') {
|
|
2794
|
+
// Check if web_search_queries exists and is non-empty
|
|
2795
|
+
if ('webSearchQueries' in metadata && Array.isArray(metadata.webSearchQueries) && metadata.webSearchQueries.length > 0) {
|
|
2796
|
+
return true;
|
|
2797
|
+
}
|
|
2798
|
+
// Check if grounding_chunks exists and is non-empty
|
|
2799
|
+
if ('groundingChunks' in metadata && Array.isArray(metadata.groundingChunks) && metadata.groundingChunks.length > 0) {
|
|
2800
|
+
return true;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
// Check for google search in function calls
|
|
2805
|
+
if ('content' in candidate && candidate.content && typeof candidate.content === 'object') {
|
|
2806
|
+
const content = candidate.content;
|
|
2807
|
+
if ('parts' in content && Array.isArray(content.parts)) {
|
|
2808
|
+
return content.parts.some(part => {
|
|
2809
|
+
if (!part || typeof part !== 'object' || !('functionCall' in part)) {
|
|
2810
|
+
return false;
|
|
2811
|
+
}
|
|
2812
|
+
const functionCall = part.functionCall;
|
|
2813
|
+
if (functionCall && typeof functionCall === 'object' && 'name' in functionCall && typeof functionCall.name === 'string') {
|
|
2814
|
+
return functionCall.name.includes('google_search') || functionCall.name.includes('grounding');
|
|
2815
|
+
}
|
|
2816
|
+
return false;
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
return false;
|
|
2821
|
+
});
|
|
2822
|
+
return hasGrounding ? 1 : 0;
|
|
2823
|
+
}
|
|
2590
2824
|
|
|
2591
2825
|
function getDefaultExportFromCjs (x) {
|
|
2592
2826
|
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
|
|
@@ -3407,6 +3641,9 @@ class LangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
3407
3641
|
if (additionalTokenData.reasoningTokens) {
|
|
3408
3642
|
eventProperties['$ai_reasoning_tokens'] = additionalTokenData.reasoningTokens;
|
|
3409
3643
|
}
|
|
3644
|
+
if (additionalTokenData.webSearchCount !== undefined) {
|
|
3645
|
+
eventProperties['$ai_web_search_count'] = additionalTokenData.webSearchCount;
|
|
3646
|
+
}
|
|
3410
3647
|
// Handle generations/completions
|
|
3411
3648
|
let completions;
|
|
3412
3649
|
if (output.generations && Array.isArray(output.generations)) {
|
|
@@ -3572,6 +3809,47 @@ class LangChainCallbackHandler extends BaseCallbackHandler {
|
|
|
3572
3809
|
} else if (usage.reasoningTokens != null) {
|
|
3573
3810
|
additionalTokenData.reasoningTokens = usage.reasoningTokens;
|
|
3574
3811
|
}
|
|
3812
|
+
// Extract web search counts from various provider formats
|
|
3813
|
+
let webSearchCount;
|
|
3814
|
+
// Priority 1: Exact Count
|
|
3815
|
+
// Check Anthropic format (server_tool_use.web_search_requests)
|
|
3816
|
+
if (usage.server_tool_use?.web_search_requests !== undefined) {
|
|
3817
|
+
webSearchCount = usage.server_tool_use.web_search_requests;
|
|
3818
|
+
}
|
|
3819
|
+
// Priority 2: Binary Detection (1 or 0)
|
|
3820
|
+
// Check for citations array (Perplexity)
|
|
3821
|
+
else if (usage.citations && Array.isArray(usage.citations) && usage.citations.length > 0) {
|
|
3822
|
+
webSearchCount = 1;
|
|
3823
|
+
}
|
|
3824
|
+
// Check for search_results array (Perplexity via OpenRouter)
|
|
3825
|
+
else if (usage.search_results && Array.isArray(usage.search_results) && usage.search_results.length > 0) {
|
|
3826
|
+
webSearchCount = 1;
|
|
3827
|
+
}
|
|
3828
|
+
// Check for search_context_size (Perplexity via OpenRouter)
|
|
3829
|
+
else if (usage.search_context_size) {
|
|
3830
|
+
webSearchCount = 1;
|
|
3831
|
+
}
|
|
3832
|
+
// Check for annotations with url_citation type
|
|
3833
|
+
else if (usage.annotations && Array.isArray(usage.annotations)) {
|
|
3834
|
+
const hasUrlCitation = usage.annotations.some(ann => {
|
|
3835
|
+
return ann && typeof ann === 'object' && 'type' in ann && ann.type === 'url_citation';
|
|
3836
|
+
});
|
|
3837
|
+
if (hasUrlCitation) {
|
|
3838
|
+
webSearchCount = 1;
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
// Check Gemini format (grounding metadata - binary 0 or 1)
|
|
3842
|
+
else if (usage.grounding_metadata?.grounding_support !== undefined || usage.grounding_metadata?.web_search_queries !== undefined) {
|
|
3843
|
+
webSearchCount = 1;
|
|
3844
|
+
}
|
|
3845
|
+
if (webSearchCount !== undefined) {
|
|
3846
|
+
additionalTokenData.webSearchCount = webSearchCount;
|
|
3847
|
+
}
|
|
3848
|
+
// In LangChain, input_tokens is the sum of input and cache read tokens.
|
|
3849
|
+
// Our cost calculation expects them to be separate, for Anthropic.
|
|
3850
|
+
if (parsedUsage.input && additionalTokenData.cacheReadInputTokens) {
|
|
3851
|
+
parsedUsage.input = Math.max(parsedUsage.input - additionalTokenData.cacheReadInputTokens, 0);
|
|
3852
|
+
}
|
|
3575
3853
|
return [parsedUsage.input, parsedUsage.output, additionalTokenData];
|
|
3576
3854
|
}
|
|
3577
3855
|
parseUsage(response) {
|