@juspay/neurolink 9.82.0 → 9.83.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/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +253 -253
- package/dist/lib/providers/googleAiStudio.js +25 -4
- package/dist/lib/providers/googleNativeGemini3.js +22 -2
- package/dist/lib/providers/googleVertex.js +28 -4
- package/dist/lib/types/common.d.ts +3 -0
- package/dist/lib/types/providers.d.ts +8 -0
- package/dist/lib/utils/pricing.js +144 -26
- package/dist/lib/utils/tokenUtils.d.ts +11 -0
- package/dist/lib/utils/tokenUtils.js +33 -2
- package/dist/providers/googleAiStudio.js +25 -4
- package/dist/providers/googleNativeGemini3.js +22 -2
- package/dist/providers/googleVertex.js +28 -4
- package/dist/types/common.d.ts +3 -0
- package/dist/types/providers.d.ts +8 -0
- package/dist/utils/pricing.js +144 -26
- package/dist/utils/tokenUtils.d.ts +11 -0
- package/dist/utils/tokenUtils.js +33 -2
- package/package.json +1 -1
|
@@ -584,6 +584,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
584
584
|
let lastStepText = "";
|
|
585
585
|
let totalInputTokens = 0;
|
|
586
586
|
let totalOutputTokens = 0;
|
|
587
|
+
let totalCacheReadTokens = 0;
|
|
587
588
|
let step = 0;
|
|
588
589
|
let completedWithFinalAnswer = false;
|
|
589
590
|
const failedTools = new Map();
|
|
@@ -614,6 +615,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
614
615
|
const chunkResult = await collectStreamChunksIncremental(rawStream, channel);
|
|
615
616
|
totalInputTokens += chunkResult.inputTokens;
|
|
616
617
|
totalOutputTokens += chunkResult.outputTokens;
|
|
618
|
+
totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
|
|
617
619
|
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
618
620
|
// If no function calls, this was the final step — channel
|
|
619
621
|
// already received all text parts incrementally.
|
|
@@ -698,13 +700,23 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
698
700
|
span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
|
|
699
701
|
span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
|
|
700
702
|
span.setAttribute(ATTR.GEN_AI_FINISH_REASON, hitStepLimitWithoutFinalAnswer ? "max_steps" : "stop");
|
|
703
|
+
// Gemini promptTokenCount is OVERLAPPING: it already includes
|
|
704
|
+
// cachedContentTokenCount. Subtract once here so calculateCost
|
|
705
|
+
// bills the cached portion at the cheaper cacheRead rate without
|
|
706
|
+
// double-counting. Total billable tokens are conserved.
|
|
707
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
701
708
|
analyticsResolve({
|
|
702
709
|
provider: this.providerName,
|
|
703
710
|
model: modelName,
|
|
704
711
|
tokenUsage: {
|
|
705
|
-
input:
|
|
712
|
+
input: adjustedInputTokens,
|
|
706
713
|
output: totalOutputTokens,
|
|
707
|
-
total:
|
|
714
|
+
total: adjustedInputTokens +
|
|
715
|
+
totalCacheReadTokens +
|
|
716
|
+
totalOutputTokens,
|
|
717
|
+
...(totalCacheReadTokens > 0
|
|
718
|
+
? { cacheReadTokens: totalCacheReadTokens }
|
|
719
|
+
: {}),
|
|
708
720
|
},
|
|
709
721
|
requestDuration: responseTime,
|
|
710
722
|
timestamp: new Date().toISOString(),
|
|
@@ -831,6 +843,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
831
843
|
let lastStepText = "";
|
|
832
844
|
let totalInputTokens = 0;
|
|
833
845
|
let totalOutputTokens = 0;
|
|
846
|
+
let totalCacheReadTokens = 0;
|
|
834
847
|
const allToolCalls = [];
|
|
835
848
|
const toolExecutions = [];
|
|
836
849
|
let step = 0;
|
|
@@ -856,6 +869,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
856
869
|
const chunkResult = await collectStreamChunks(stream);
|
|
857
870
|
totalInputTokens += chunkResult.inputTokens;
|
|
858
871
|
totalOutputTokens += chunkResult.outputTokens;
|
|
872
|
+
totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
|
|
859
873
|
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
860
874
|
// If no function calls, we're done
|
|
861
875
|
if (chunkResult.stepFunctionCalls.length === 0) {
|
|
@@ -927,14 +941,21 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
927
941
|
// analytics / evaluation / tracing stay attached. The native AI
|
|
928
942
|
// Studio generate path bypasses BaseProvider.generate(), so
|
|
929
943
|
// skipping enhanceResult would silently drop those features.
|
|
944
|
+
// Gemini promptTokenCount is OVERLAPPING (already includes
|
|
945
|
+
// cachedContentTokenCount). Subtract once so the cached portion is
|
|
946
|
+
// billed at the cheaper cacheRead rate without double-counting.
|
|
947
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
930
948
|
const baseResult = {
|
|
931
949
|
content: finalText,
|
|
932
950
|
provider: this.providerName,
|
|
933
951
|
model: modelName,
|
|
934
952
|
usage: {
|
|
935
|
-
input:
|
|
953
|
+
input: adjustedInputTokens,
|
|
936
954
|
output: totalOutputTokens,
|
|
937
|
-
total:
|
|
955
|
+
total: adjustedInputTokens + totalCacheReadTokens + totalOutputTokens,
|
|
956
|
+
...(totalCacheReadTokens > 0
|
|
957
|
+
? { cacheReadTokens: totalCacheReadTokens }
|
|
958
|
+
: {}),
|
|
938
959
|
},
|
|
939
960
|
responseTime,
|
|
940
961
|
toolsUsed: allToolCalls.map((tc) => tc.toolName),
|
|
@@ -514,6 +514,7 @@ export async function collectStreamChunks(stream) {
|
|
|
514
514
|
const stepFunctionCalls = [];
|
|
515
515
|
let inputTokens = 0;
|
|
516
516
|
let outputTokens = 0;
|
|
517
|
+
let cacheReadTokens = 0;
|
|
517
518
|
for await (const chunk of stream) {
|
|
518
519
|
// Extract raw parts from candidates FIRST
|
|
519
520
|
// This avoids using chunk.text which triggers SDK warning when
|
|
@@ -533,9 +534,18 @@ export async function collectStreamChunks(stream) {
|
|
|
533
534
|
if (usage) {
|
|
534
535
|
inputTokens = Math.max(inputTokens, usage.promptTokenCount || 0);
|
|
535
536
|
outputTokens = Math.max(outputTokens, usage.candidatesTokenCount || 0);
|
|
537
|
+
// cachedContentTokenCount is OVERLAPPING (a subset already inside
|
|
538
|
+
// promptTokenCount). Surface it so the call site subtracts once.
|
|
539
|
+
cacheReadTokens = Math.max(cacheReadTokens, usage.cachedContentTokenCount || 0);
|
|
536
540
|
}
|
|
537
541
|
}
|
|
538
|
-
return {
|
|
542
|
+
return {
|
|
543
|
+
rawResponseParts,
|
|
544
|
+
stepFunctionCalls,
|
|
545
|
+
inputTokens,
|
|
546
|
+
outputTokens,
|
|
547
|
+
cacheReadTokens,
|
|
548
|
+
};
|
|
539
549
|
}
|
|
540
550
|
/**
|
|
541
551
|
* Create a push-based text channel that bridges a background producer
|
|
@@ -627,6 +637,7 @@ export async function collectStreamChunksIncremental(stream, channel) {
|
|
|
627
637
|
const stepFunctionCalls = [];
|
|
628
638
|
let inputTokens = 0;
|
|
629
639
|
let outputTokens = 0;
|
|
640
|
+
let cacheReadTokens = 0;
|
|
630
641
|
for await (const chunk of stream) {
|
|
631
642
|
const chunkRecord = chunk;
|
|
632
643
|
const candidates = chunkRecord.candidates;
|
|
@@ -648,9 +659,18 @@ export async function collectStreamChunksIncremental(stream, channel) {
|
|
|
648
659
|
if (usage) {
|
|
649
660
|
inputTokens = Math.max(inputTokens, usage.promptTokenCount || 0);
|
|
650
661
|
outputTokens = Math.max(outputTokens, usage.candidatesTokenCount || 0);
|
|
662
|
+
// cachedContentTokenCount is OVERLAPPING (a subset already inside
|
|
663
|
+
// promptTokenCount). Surface it so the call site subtracts once.
|
|
664
|
+
cacheReadTokens = Math.max(cacheReadTokens, usage.cachedContentTokenCount || 0);
|
|
651
665
|
}
|
|
652
666
|
}
|
|
653
|
-
return {
|
|
667
|
+
return {
|
|
668
|
+
rawResponseParts,
|
|
669
|
+
stepFunctionCalls,
|
|
670
|
+
inputTokens,
|
|
671
|
+
outputTokens,
|
|
672
|
+
cacheReadTokens,
|
|
673
|
+
};
|
|
654
674
|
}
|
|
655
675
|
/**
|
|
656
676
|
* Extract the thoughtSignature token from raw response parts.
|
|
@@ -1325,6 +1325,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1325
1325
|
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
1326
1326
|
let totalInputTokens = 0;
|
|
1327
1327
|
let totalOutputTokens = 0;
|
|
1328
|
+
let totalCacheReadTokens = 0;
|
|
1328
1329
|
// Track text parts as they arrive from the SDK so the returned async
|
|
1329
1330
|
// iterable yields multiple chunks instead of a single buffered chunk.
|
|
1330
1331
|
// The CLI's chunk-count smoke test asserts > 1 stream chunks for any
|
|
@@ -1440,6 +1441,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1440
1441
|
totalInputTokens = usageMetadata.promptTokenCount;
|
|
1441
1442
|
// Feed the context guard the REAL prompt size of this call.
|
|
1442
1443
|
contextGuard.noteUsage(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
|
|
1444
|
+
// cachedContentTokenCount is OVERLAPPING (a subset already inside
|
|
1445
|
+
// promptTokenCount). Clamp to the prompt count so a later uncached
|
|
1446
|
+
// step resets it to 0 instead of leaving a stale cached value.
|
|
1447
|
+
totalCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
|
|
1443
1448
|
}
|
|
1444
1449
|
// Take the latest candidatesTokenCount (accumulates through chunks)
|
|
1445
1450
|
if (usageMetadata.candidatesTokenCount !== undefined &&
|
|
@@ -1891,6 +1896,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1891
1896
|
// Filter out final_result from tool calls as it's an internal pattern
|
|
1892
1897
|
const externalToolCalls = allToolCalls.filter((tc) => tc.toolName !== "final_result");
|
|
1893
1898
|
const externalToolExecutions = toolExecutions.filter((te) => te.name !== "final_result");
|
|
1899
|
+
// Gemini promptTokenCount is OVERLAPPING (already includes
|
|
1900
|
+
// cachedContentTokenCount). Subtract once so the cached portion is billed at
|
|
1901
|
+
// the cheaper cacheRead rate without double-counting; total is conserved.
|
|
1902
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
1894
1903
|
const result = {
|
|
1895
1904
|
stream: createTextStream(),
|
|
1896
1905
|
provider: this.providerName,
|
|
@@ -1899,9 +1908,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
1899
1908
|
stopReason,
|
|
1900
1909
|
rawFinishReason: lastFinishReason,
|
|
1901
1910
|
usage: {
|
|
1902
|
-
input:
|
|
1911
|
+
input: adjustedInputTokens,
|
|
1903
1912
|
output: totalOutputTokens,
|
|
1904
|
-
total:
|
|
1913
|
+
total: adjustedInputTokens + totalCacheReadTokens + totalOutputTokens,
|
|
1914
|
+
...(totalCacheReadTokens > 0 && {
|
|
1915
|
+
cacheReadTokens: totalCacheReadTokens,
|
|
1916
|
+
}),
|
|
1905
1917
|
},
|
|
1906
1918
|
toolCalls: externalToolCalls.map((tc) => ({
|
|
1907
1919
|
toolName: tc.toolName,
|
|
@@ -2254,6 +2266,7 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2254
2266
|
// promptTokenCount is typically in the final chunk, candidatesTokenCount accumulates
|
|
2255
2267
|
let totalInputTokens = 0;
|
|
2256
2268
|
let totalOutputTokens = 0;
|
|
2269
|
+
let totalCacheReadTokens = 0;
|
|
2257
2270
|
// Abort scaffolding (mirrors executeNativeAnthropicStream). The native
|
|
2258
2271
|
// Gemini SDK cancels via config.abortSignal, so drive an internal
|
|
2259
2272
|
// AbortController: the caller's signal and the turn clock's watchdogs
|
|
@@ -2359,6 +2372,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2359
2372
|
totalInputTokens = usageMetadata.promptTokenCount;
|
|
2360
2373
|
// Feed the context guard the REAL prompt size of this call.
|
|
2361
2374
|
contextGuard.noteUsage(usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount ?? 0);
|
|
2375
|
+
// cachedContentTokenCount is OVERLAPPING (a subset already inside
|
|
2376
|
+
// promptTokenCount). Clamp to the prompt count so a later uncached
|
|
2377
|
+
// step resets it to 0 instead of leaving a stale cached value.
|
|
2378
|
+
totalCacheReadTokens = Math.min(usageMetadata.cachedContentTokenCount ?? 0, usageMetadata.promptTokenCount);
|
|
2362
2379
|
}
|
|
2363
2380
|
// Take the latest candidatesTokenCount (accumulates through chunks)
|
|
2364
2381
|
if (usageMetadata.candidatesTokenCount !== undefined &&
|
|
@@ -2789,6 +2806,10 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2789
2806
|
const externalToolCalls = allToolCalls.filter((tc) => tc.toolName !== "final_result");
|
|
2790
2807
|
const externalToolExecutions = toolExecutions.filter((te) => te.name !== "final_result");
|
|
2791
2808
|
// Build EnhancedGenerateResult
|
|
2809
|
+
// Gemini promptTokenCount is OVERLAPPING (already includes
|
|
2810
|
+
// cachedContentTokenCount). Subtract once so the cached portion is billed at
|
|
2811
|
+
// the cheaper cacheRead rate without double-counting; total is conserved.
|
|
2812
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
2792
2813
|
const result = {
|
|
2793
2814
|
content: finalText,
|
|
2794
2815
|
provider: this.providerName,
|
|
@@ -2798,9 +2819,12 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
2798
2819
|
rawFinishReason: lastFinishReason,
|
|
2799
2820
|
stepsUsed: step,
|
|
2800
2821
|
usage: {
|
|
2801
|
-
input:
|
|
2822
|
+
input: adjustedInputTokens,
|
|
2802
2823
|
output: totalOutputTokens,
|
|
2803
|
-
total:
|
|
2824
|
+
total: adjustedInputTokens + totalCacheReadTokens + totalOutputTokens,
|
|
2825
|
+
...(totalCacheReadTokens > 0 && {
|
|
2826
|
+
cacheReadTokens: totalCacheReadTokens,
|
|
2827
|
+
}),
|
|
2804
2828
|
},
|
|
2805
2829
|
responseTime,
|
|
2806
2830
|
toolsUsed: externalToolCalls.map((tc) => tc.toolName),
|
|
@@ -223,6 +223,9 @@ export type RawUsageObject = {
|
|
|
223
223
|
cacheReadInputTokens?: number;
|
|
224
224
|
cacheCreationTokens?: number;
|
|
225
225
|
cacheReadTokens?: number;
|
|
226
|
+
prompt_tokens_details?: {
|
|
227
|
+
cached_tokens?: number;
|
|
228
|
+
};
|
|
226
229
|
reasoningTokens?: number;
|
|
227
230
|
reasoning?: number;
|
|
228
231
|
reasoning_tokens?: number;
|
|
@@ -1597,6 +1597,14 @@ export type CollectedChunkResult = {
|
|
|
1597
1597
|
stepFunctionCalls: NativeFunctionCall[];
|
|
1598
1598
|
inputTokens: number;
|
|
1599
1599
|
outputTokens: number;
|
|
1600
|
+
/**
|
|
1601
|
+
* Gemini cached-content tokens (overlapping: included in promptTokenCount).
|
|
1602
|
+
* Surfaced so the call site can subtract from input and bill at cacheRead
|
|
1603
|
+
* rate. Subtraction happens at the call site, not in the collector.
|
|
1604
|
+
*/
|
|
1605
|
+
cacheReadTokens?: number;
|
|
1606
|
+
/** Cache creation tokens (symmetry; Gemini does not emit this). */
|
|
1607
|
+
cacheCreationTokens?: number;
|
|
1600
1608
|
};
|
|
1601
1609
|
/** Push-based text channel for incremental streaming. */
|
|
1602
1610
|
export type TextChannel = {
|
|
@@ -87,8 +87,18 @@ const PRICING = {
|
|
|
87
87
|
cacheRead: 1.5 / 1_000_000,
|
|
88
88
|
cacheCreation: 18.75 / 1_000_000,
|
|
89
89
|
},
|
|
90
|
-
"claude-3-sonnet": {
|
|
91
|
-
|
|
90
|
+
"claude-3-sonnet": {
|
|
91
|
+
input: 3.0 / 1_000_000,
|
|
92
|
+
output: 15.0 / 1_000_000,
|
|
93
|
+
cacheRead: 0.3 / 1_000_000,
|
|
94
|
+
cacheCreation: 3.75 / 1_000_000,
|
|
95
|
+
},
|
|
96
|
+
"claude-3-haiku": {
|
|
97
|
+
input: 0.25 / 1_000_000,
|
|
98
|
+
output: 1.25 / 1_000_000,
|
|
99
|
+
cacheRead: 0.025 / 1_000_000,
|
|
100
|
+
cacheCreation: 0.3125 / 1_000_000,
|
|
101
|
+
},
|
|
92
102
|
},
|
|
93
103
|
// Google Vertex AI — Claude models on Vertex (same pricing, @ date suffix)
|
|
94
104
|
vertex: {
|
|
@@ -138,26 +148,95 @@ const PRICING = {
|
|
|
138
148
|
// OpenAI — updated March 2026
|
|
139
149
|
openai: {
|
|
140
150
|
// GPT-5.x family
|
|
141
|
-
|
|
142
|
-
"gpt-5.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"gpt-5
|
|
151
|
+
// cacheRead = 0.25x input (cached input tokens; no separate cacheCreation).
|
|
152
|
+
"gpt-5.4": {
|
|
153
|
+
input: 2.5 / 1_000_000,
|
|
154
|
+
output: 15.0 / 1_000_000,
|
|
155
|
+
cacheRead: 0.625 / 1_000_000,
|
|
156
|
+
},
|
|
157
|
+
"gpt-5.2": {
|
|
158
|
+
input: 1.75 / 1_000_000,
|
|
159
|
+
output: 14.0 / 1_000_000,
|
|
160
|
+
cacheRead: 0.4375 / 1_000_000,
|
|
161
|
+
},
|
|
162
|
+
"gpt-5.1": {
|
|
163
|
+
input: 0.625 / 1_000_000,
|
|
164
|
+
output: 5.0 / 1_000_000,
|
|
165
|
+
cacheRead: 0.15625 / 1_000_000,
|
|
166
|
+
},
|
|
167
|
+
"gpt-5.1-codex": {
|
|
168
|
+
input: 1.25 / 1_000_000,
|
|
169
|
+
output: 10.0 / 1_000_000,
|
|
170
|
+
cacheRead: 0.3125 / 1_000_000,
|
|
171
|
+
},
|
|
172
|
+
"gpt-5": {
|
|
173
|
+
input: 1.25 / 1_000_000,
|
|
174
|
+
output: 10.0 / 1_000_000,
|
|
175
|
+
cacheRead: 0.3125 / 1_000_000,
|
|
176
|
+
},
|
|
177
|
+
"gpt-5-mini": {
|
|
178
|
+
input: 0.25 / 1_000_000,
|
|
179
|
+
output: 2.0 / 1_000_000,
|
|
180
|
+
cacheRead: 0.0625 / 1_000_000,
|
|
181
|
+
},
|
|
182
|
+
"gpt-5-nano": {
|
|
183
|
+
input: 0.05 / 1_000_000,
|
|
184
|
+
output: 0.4 / 1_000_000,
|
|
185
|
+
cacheRead: 0.0125 / 1_000_000,
|
|
186
|
+
},
|
|
148
187
|
// GPT-4.1 family
|
|
149
|
-
"gpt-4.1": {
|
|
150
|
-
|
|
151
|
-
|
|
188
|
+
"gpt-4.1": {
|
|
189
|
+
input: 2.0 / 1_000_000,
|
|
190
|
+
output: 8.0 / 1_000_000,
|
|
191
|
+
cacheRead: 0.5 / 1_000_000,
|
|
192
|
+
},
|
|
193
|
+
"gpt-4.1-mini": {
|
|
194
|
+
input: 0.4 / 1_000_000,
|
|
195
|
+
output: 1.6 / 1_000_000,
|
|
196
|
+
cacheRead: 0.1 / 1_000_000,
|
|
197
|
+
},
|
|
198
|
+
"gpt-4.1-nano": {
|
|
199
|
+
input: 0.1 / 1_000_000,
|
|
200
|
+
output: 0.4 / 1_000_000,
|
|
201
|
+
cacheRead: 0.025 / 1_000_000,
|
|
202
|
+
},
|
|
152
203
|
// GPT-4o family
|
|
153
|
-
"gpt-4o": {
|
|
154
|
-
|
|
204
|
+
"gpt-4o": {
|
|
205
|
+
input: 2.5 / 1_000_000,
|
|
206
|
+
output: 10.0 / 1_000_000,
|
|
207
|
+
cacheRead: 0.625 / 1_000_000,
|
|
208
|
+
},
|
|
209
|
+
"gpt-4o-mini": {
|
|
210
|
+
input: 0.15 / 1_000_000,
|
|
211
|
+
output: 0.6 / 1_000_000,
|
|
212
|
+
cacheRead: 0.0375 / 1_000_000,
|
|
213
|
+
},
|
|
155
214
|
// o-series reasoning
|
|
156
|
-
o3: {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
215
|
+
o3: {
|
|
216
|
+
input: 2.0 / 1_000_000,
|
|
217
|
+
output: 8.0 / 1_000_000,
|
|
218
|
+
cacheRead: 0.5 / 1_000_000,
|
|
219
|
+
},
|
|
220
|
+
"o3-mini": {
|
|
221
|
+
input: 1.1 / 1_000_000,
|
|
222
|
+
output: 4.4 / 1_000_000,
|
|
223
|
+
cacheRead: 0.275 / 1_000_000,
|
|
224
|
+
},
|
|
225
|
+
"o4-mini": {
|
|
226
|
+
input: 1.1 / 1_000_000,
|
|
227
|
+
output: 4.4 / 1_000_000,
|
|
228
|
+
cacheRead: 0.275 / 1_000_000,
|
|
229
|
+
},
|
|
230
|
+
o1: {
|
|
231
|
+
input: 15.0 / 1_000_000,
|
|
232
|
+
output: 60.0 / 1_000_000,
|
|
233
|
+
cacheRead: 3.75 / 1_000_000,
|
|
234
|
+
},
|
|
235
|
+
"o1-mini": {
|
|
236
|
+
input: 0.55 / 1_000_000,
|
|
237
|
+
output: 2.2 / 1_000_000,
|
|
238
|
+
cacheRead: 0.1375 / 1_000_000,
|
|
239
|
+
},
|
|
161
240
|
// Legacy
|
|
162
241
|
"gpt-4-turbo": { input: 10.0 / 1_000_000, output: 30.0 / 1_000_000 },
|
|
163
242
|
"gpt-4": { input: 30.0 / 1_000_000, output: 60.0 / 1_000_000 },
|
|
@@ -166,52 +245,83 @@ const PRICING = {
|
|
|
166
245
|
// Google (Gemini) — updated March 2026
|
|
167
246
|
google: {
|
|
168
247
|
// Gemini 3.1 family (all require -preview suffix)
|
|
248
|
+
// cacheRead = 0.25x input (cached content tokens; explicit-cache storage is
|
|
249
|
+
// billed separately by TTL/time, so no per-token cacheCreation rate here).
|
|
169
250
|
"gemini-3.1-pro-preview": {
|
|
170
251
|
input: 2.0 / 1_000_000,
|
|
171
252
|
output: 12.0 / 1_000_000,
|
|
253
|
+
cacheRead: 0.5 / 1_000_000,
|
|
172
254
|
},
|
|
173
255
|
"gemini-3.1-flash-lite-preview": {
|
|
174
256
|
input: 0.25 / 1_000_000,
|
|
175
257
|
output: 1.5 / 1_000_000,
|
|
258
|
+
cacheRead: 0.0625 / 1_000_000,
|
|
176
259
|
},
|
|
177
260
|
"gemini-3.1-flash-image-preview": {
|
|
178
261
|
input: 0.5 / 1_000_000,
|
|
179
262
|
output: 3.0 / 1_000_000,
|
|
263
|
+
cacheRead: 0.125 / 1_000_000,
|
|
180
264
|
},
|
|
181
265
|
"gemini-3.1-pro-preview-customtools": {
|
|
182
266
|
input: 2.0 / 1_000_000,
|
|
183
267
|
output: 12.0 / 1_000_000,
|
|
268
|
+
cacheRead: 0.5 / 1_000_000,
|
|
184
269
|
},
|
|
185
270
|
// Gemini 3 family
|
|
186
271
|
"gemini-3-flash-preview": {
|
|
187
272
|
input: 0.5 / 1_000_000,
|
|
188
273
|
output: 3.0 / 1_000_000,
|
|
274
|
+
cacheRead: 0.125 / 1_000_000,
|
|
189
275
|
},
|
|
190
276
|
"gemini-3-pro-image-preview": {
|
|
191
277
|
input: 2.0 / 1_000_000,
|
|
192
278
|
output: 12.0 / 1_000_000,
|
|
279
|
+
cacheRead: 0.5 / 1_000_000,
|
|
193
280
|
},
|
|
194
281
|
/** @deprecated SHUT DOWN March 9, 2026. Migrate to gemini-3.1-pro-preview. */
|
|
195
282
|
"gemini-3-pro-preview": {
|
|
196
283
|
input: 2.0 / 1_000_000,
|
|
197
284
|
output: 12.0 / 1_000_000,
|
|
285
|
+
cacheRead: 0.5 / 1_000_000,
|
|
198
286
|
},
|
|
199
287
|
// Gemini 2.5 family
|
|
200
|
-
"gemini-2.5-flash": {
|
|
201
|
-
|
|
288
|
+
"gemini-2.5-flash": {
|
|
289
|
+
input: 0.3 / 1_000_000,
|
|
290
|
+
output: 2.5 / 1_000_000,
|
|
291
|
+
cacheRead: 0.075 / 1_000_000,
|
|
292
|
+
},
|
|
293
|
+
"gemini-2.5-pro": {
|
|
294
|
+
input: 1.25 / 1_000_000,
|
|
295
|
+
output: 10.0 / 1_000_000,
|
|
296
|
+
cacheRead: 0.3125 / 1_000_000,
|
|
297
|
+
},
|
|
202
298
|
"gemini-2.5-flash-lite": {
|
|
203
299
|
input: 0.1 / 1_000_000,
|
|
204
300
|
output: 0.4 / 1_000_000,
|
|
301
|
+
cacheRead: 0.025 / 1_000_000,
|
|
205
302
|
},
|
|
206
303
|
// Gemini 2.0 family (deprecated June 2026)
|
|
207
|
-
"gemini-2.0-flash": {
|
|
304
|
+
"gemini-2.0-flash": {
|
|
305
|
+
input: 0.15 / 1_000_000,
|
|
306
|
+
output: 0.6 / 1_000_000,
|
|
307
|
+
cacheRead: 0.0375 / 1_000_000,
|
|
308
|
+
},
|
|
208
309
|
"gemini-2.0-flash-lite": {
|
|
209
310
|
input: 0.075 / 1_000_000,
|
|
210
311
|
output: 0.3 / 1_000_000,
|
|
312
|
+
cacheRead: 0.01875 / 1_000_000,
|
|
211
313
|
},
|
|
212
314
|
// Gemini 1.5 family
|
|
213
|
-
"gemini-1.5-pro": {
|
|
214
|
-
|
|
315
|
+
"gemini-1.5-pro": {
|
|
316
|
+
input: 1.25 / 1_000_000,
|
|
317
|
+
output: 5.0 / 1_000_000,
|
|
318
|
+
cacheRead: 0.3125 / 1_000_000,
|
|
319
|
+
},
|
|
320
|
+
"gemini-1.5-flash": {
|
|
321
|
+
input: 0.075 / 1_000_000,
|
|
322
|
+
output: 0.3 / 1_000_000,
|
|
323
|
+
cacheRead: 0.01875 / 1_000_000,
|
|
324
|
+
},
|
|
215
325
|
},
|
|
216
326
|
// Mistral AI
|
|
217
327
|
mistral: {
|
|
@@ -594,8 +704,16 @@ export function calculateCost(provider, model, usage) {
|
|
|
594
704
|
let cost = 0;
|
|
595
705
|
cost += (usage.input || 0) * rates.input;
|
|
596
706
|
cost += (usage.output || 0) * rates.output;
|
|
597
|
-
if (usage.cacheReadTokens
|
|
598
|
-
|
|
707
|
+
if (usage.cacheReadTokens) {
|
|
708
|
+
// Price cache reads at the discounted cacheRead rate when one exists;
|
|
709
|
+
// otherwise fall back to the full input rate. The overlapping extractor
|
|
710
|
+
// (tokenUtils) moves cached tokens out of `input` into `cacheReadTokens`
|
|
711
|
+
// for OpenAI/Gemini-family providers, so without this fallback a provider
|
|
712
|
+
// that reports cached tokens but has no cacheRead rate would bill those
|
|
713
|
+
// tokens at $0 (silent undercharge). Falling back to the input rate keeps
|
|
714
|
+
// total cost identical to pre-split billing for unpriced providers, while
|
|
715
|
+
// priced providers still get the cheaper cacheRead rate.
|
|
716
|
+
cost += usage.cacheReadTokens * (rates.cacheRead ?? rates.input);
|
|
599
717
|
}
|
|
600
718
|
if (usage.cacheCreationTokens && rates.cacheCreation) {
|
|
601
719
|
cost += usage.cacheCreationTokens * rates.cacheCreation;
|
|
@@ -38,6 +38,17 @@ export declare function extractCacheCreationTokens(usage: RawUsageObject): numbe
|
|
|
38
38
|
* Supports: cacheReadInputTokens, cacheReadTokens
|
|
39
39
|
*/
|
|
40
40
|
export declare function extractCacheReadTokens(usage: RawUsageObject): number | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Extract cache read token count from the OVERLAPPING-convention nested path
|
|
43
|
+
* used by OpenAI / DeepSeek / NIM / OpenAI-compatible providers:
|
|
44
|
+
* `usage.prompt_tokens_details.cached_tokens`.
|
|
45
|
+
*
|
|
46
|
+
* Unlike {@link extractCacheReadTokens} (non-overlapping Anthropic/Vertex
|
|
47
|
+
* convention where cache tokens are reported SEPARATELY from input), the value
|
|
48
|
+
* returned here is a SUBSET already included in `prompt_tokens`. Callers that
|
|
49
|
+
* use this value MUST subtract it from `input` to avoid double-counting.
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractCachedInputTokensOverlapping(usage: RawUsageObject): number | undefined;
|
|
41
52
|
/**
|
|
42
53
|
* Calculate cache savings percentage
|
|
43
54
|
*
|
|
@@ -101,6 +101,23 @@ export function extractCacheReadTokens(usage) {
|
|
|
101
101
|
}
|
|
102
102
|
return undefined;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Extract cache read token count from the OVERLAPPING-convention nested path
|
|
106
|
+
* used by OpenAI / DeepSeek / NIM / OpenAI-compatible providers:
|
|
107
|
+
* `usage.prompt_tokens_details.cached_tokens`.
|
|
108
|
+
*
|
|
109
|
+
* Unlike {@link extractCacheReadTokens} (non-overlapping Anthropic/Vertex
|
|
110
|
+
* convention where cache tokens are reported SEPARATELY from input), the value
|
|
111
|
+
* returned here is a SUBSET already included in `prompt_tokens`. Callers that
|
|
112
|
+
* use this value MUST subtract it from `input` to avoid double-counting.
|
|
113
|
+
*/
|
|
114
|
+
export function extractCachedInputTokensOverlapping(usage) {
|
|
115
|
+
const cached = usage.prompt_tokens_details?.cached_tokens;
|
|
116
|
+
if (typeof cached === "number" && cached > 0) {
|
|
117
|
+
return cached;
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
104
121
|
/**
|
|
105
122
|
* Calculate cache savings percentage
|
|
106
123
|
*
|
|
@@ -149,13 +166,27 @@ export function extractTokenUsage(result, options = {}) {
|
|
|
149
166
|
// Handle nested usage object (some providers wrap usage in a usage property)
|
|
150
167
|
const usage = result.usage && typeof result.usage === "object" ? result.usage : result;
|
|
151
168
|
// Extract base token counts
|
|
152
|
-
|
|
169
|
+
let input = extractInputTokens(usage);
|
|
153
170
|
const output = extractOutputTokens(usage);
|
|
154
171
|
const total = extractTotalTokens(usage, input, output);
|
|
155
172
|
// Extract optional token fields
|
|
156
173
|
const reasoning = extractReasoningTokens(usage);
|
|
157
174
|
const cacheCreationTokens = extractCacheCreationTokens(usage);
|
|
158
|
-
|
|
175
|
+
let cacheReadTokens = extractCacheReadTokens(usage);
|
|
176
|
+
// Overlapping-convention fallback (OpenAI/DeepSeek/NIM/OpenAI-compatible):
|
|
177
|
+
// when no non-overlapping cache-read field was present, look for the nested
|
|
178
|
+
// `prompt_tokens_details.cached_tokens`. That value is a SUBSET already
|
|
179
|
+
// included in `input`, so we must subtract it from `input` to avoid
|
|
180
|
+
// double-counting (and to let calculateCost apply the cheaper cacheRead rate
|
|
181
|
+
// to the cached portion). Only do this when cached <= input so a malformed
|
|
182
|
+
// response can never produce negative input or inflate the total.
|
|
183
|
+
if (cacheReadTokens === undefined) {
|
|
184
|
+
const overlappingCached = extractCachedInputTokensOverlapping(usage);
|
|
185
|
+
if (overlappingCached !== undefined && overlappingCached <= input) {
|
|
186
|
+
cacheReadTokens = overlappingCached;
|
|
187
|
+
input = Math.max(0, input - overlappingCached);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
159
190
|
// Calculate cache savings if enabled
|
|
160
191
|
const cacheSavingsPercent = calculateCacheSavings
|
|
161
192
|
? calculateCacheSavingsPercent(cacheReadTokens, input)
|
|
@@ -584,6 +584,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
584
584
|
let lastStepText = "";
|
|
585
585
|
let totalInputTokens = 0;
|
|
586
586
|
let totalOutputTokens = 0;
|
|
587
|
+
let totalCacheReadTokens = 0;
|
|
587
588
|
let step = 0;
|
|
588
589
|
let completedWithFinalAnswer = false;
|
|
589
590
|
const failedTools = new Map();
|
|
@@ -614,6 +615,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
614
615
|
const chunkResult = await collectStreamChunksIncremental(rawStream, channel);
|
|
615
616
|
totalInputTokens += chunkResult.inputTokens;
|
|
616
617
|
totalOutputTokens += chunkResult.outputTokens;
|
|
618
|
+
totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
|
|
617
619
|
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
618
620
|
// If no function calls, this was the final step — channel
|
|
619
621
|
// already received all text parts incrementally.
|
|
@@ -698,13 +700,23 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
698
700
|
span.setAttribute(ATTR.GEN_AI_INPUT_TOKENS, totalInputTokens);
|
|
699
701
|
span.setAttribute(ATTR.GEN_AI_OUTPUT_TOKENS, totalOutputTokens);
|
|
700
702
|
span.setAttribute(ATTR.GEN_AI_FINISH_REASON, hitStepLimitWithoutFinalAnswer ? "max_steps" : "stop");
|
|
703
|
+
// Gemini promptTokenCount is OVERLAPPING: it already includes
|
|
704
|
+
// cachedContentTokenCount. Subtract once here so calculateCost
|
|
705
|
+
// bills the cached portion at the cheaper cacheRead rate without
|
|
706
|
+
// double-counting. Total billable tokens are conserved.
|
|
707
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
701
708
|
analyticsResolve({
|
|
702
709
|
provider: this.providerName,
|
|
703
710
|
model: modelName,
|
|
704
711
|
tokenUsage: {
|
|
705
|
-
input:
|
|
712
|
+
input: adjustedInputTokens,
|
|
706
713
|
output: totalOutputTokens,
|
|
707
|
-
total:
|
|
714
|
+
total: adjustedInputTokens +
|
|
715
|
+
totalCacheReadTokens +
|
|
716
|
+
totalOutputTokens,
|
|
717
|
+
...(totalCacheReadTokens > 0
|
|
718
|
+
? { cacheReadTokens: totalCacheReadTokens }
|
|
719
|
+
: {}),
|
|
708
720
|
},
|
|
709
721
|
requestDuration: responseTime,
|
|
710
722
|
timestamp: new Date().toISOString(),
|
|
@@ -831,6 +843,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
831
843
|
let lastStepText = "";
|
|
832
844
|
let totalInputTokens = 0;
|
|
833
845
|
let totalOutputTokens = 0;
|
|
846
|
+
let totalCacheReadTokens = 0;
|
|
834
847
|
const allToolCalls = [];
|
|
835
848
|
const toolExecutions = [];
|
|
836
849
|
let step = 0;
|
|
@@ -856,6 +869,7 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
856
869
|
const chunkResult = await collectStreamChunks(stream);
|
|
857
870
|
totalInputTokens += chunkResult.inputTokens;
|
|
858
871
|
totalOutputTokens += chunkResult.outputTokens;
|
|
872
|
+
totalCacheReadTokens += chunkResult.cacheReadTokens ?? 0;
|
|
859
873
|
const stepText = extractTextFromParts(chunkResult.rawResponseParts);
|
|
860
874
|
// If no function calls, we're done
|
|
861
875
|
if (chunkResult.stepFunctionCalls.length === 0) {
|
|
@@ -927,14 +941,21 @@ export class GoogleAIStudioProvider extends BaseProvider {
|
|
|
927
941
|
// analytics / evaluation / tracing stay attached. The native AI
|
|
928
942
|
// Studio generate path bypasses BaseProvider.generate(), so
|
|
929
943
|
// skipping enhanceResult would silently drop those features.
|
|
944
|
+
// Gemini promptTokenCount is OVERLAPPING (already includes
|
|
945
|
+
// cachedContentTokenCount). Subtract once so the cached portion is
|
|
946
|
+
// billed at the cheaper cacheRead rate without double-counting.
|
|
947
|
+
const adjustedInputTokens = Math.max(0, totalInputTokens - totalCacheReadTokens);
|
|
930
948
|
const baseResult = {
|
|
931
949
|
content: finalText,
|
|
932
950
|
provider: this.providerName,
|
|
933
951
|
model: modelName,
|
|
934
952
|
usage: {
|
|
935
|
-
input:
|
|
953
|
+
input: adjustedInputTokens,
|
|
936
954
|
output: totalOutputTokens,
|
|
937
|
-
total:
|
|
955
|
+
total: adjustedInputTokens + totalCacheReadTokens + totalOutputTokens,
|
|
956
|
+
...(totalCacheReadTokens > 0
|
|
957
|
+
? { cacheReadTokens: totalCacheReadTokens }
|
|
958
|
+
: {}),
|
|
938
959
|
},
|
|
939
960
|
responseTime,
|
|
940
961
|
toolsUsed: allToolCalls.map((tc) => tc.toolName),
|