@juspay/neurolink 9.81.3 → 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 +18 -0
- package/README.md +4 -0
- package/dist/browser/neurolink.min.js +342 -342
- package/dist/cli/commands/proxy.js +5 -0
- package/dist/cli/factories/commandFactory.d.ts +2 -1
- package/dist/cli/factories/commandFactory.js +36 -16
- package/dist/cli/utils/audioPlayer.d.ts +37 -0
- package/dist/cli/utils/audioPlayer.js +138 -0
- package/dist/core/modules/GenerationHandler.js +7 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -6
- package/dist/lib/core/modules/GenerationHandler.js +7 -1
- package/dist/lib/index.d.ts +6 -1
- package/dist/lib/index.js +14 -6
- 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/proxy/proxyDispatcher.d.ts +5 -0
- package/dist/lib/proxy/proxyDispatcher.js +61 -0
- 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/systemMessages.d.ts +29 -0
- package/dist/lib/utils/systemMessages.js +45 -0
- 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/proxy/proxyDispatcher.d.ts +5 -0
- package/dist/proxy/proxyDispatcher.js +60 -0
- 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/systemMessages.d.ts +29 -0
- package/dist/utils/systemMessages.js +44 -0
- package/dist/utils/tokenUtils.d.ts +11 -0
- package/dist/utils/tokenUtils.js +33 -2
- package/package.json +3 -2
|
@@ -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),
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tuned global undici dispatcher for the proxy's upstream forwards.
|
|
3
|
+
*
|
|
4
|
+
* The Claude passthrough (`claudeProxyRoutes.ts`) forwards every request to
|
|
5
|
+
* Anthropic via the global `fetch` → undici global dispatcher. undici keep-alives
|
|
6
|
+
* by default, but with a ~4s idle timeout: between Claude Code turns (the user
|
|
7
|
+
* reads output, a tool runs, the model thinks) the idle socket closes, so the
|
|
8
|
+
* next request opens a brand-new TCP connection. Under sustained use that is a
|
|
9
|
+
* high rate of short-lived outbound flows.
|
|
10
|
+
*
|
|
11
|
+
* On hosts running a socket content-filter (CFIL) — e.g. SentinelOne or
|
|
12
|
+
* GlobalProtect network extensions — every new flow allocates per-flow kernel
|
|
13
|
+
* state, so a high flow rate amplifies any leak in that path. Reusing connections
|
|
14
|
+
* cuts the flow rate sharply, so we install a dispatcher with a longer keep-alive
|
|
15
|
+
* and a bounded, reused connection pool.
|
|
16
|
+
*
|
|
17
|
+
* Everything is overridable via env so it can be tuned (or disabled) per host:
|
|
18
|
+
* NEUROLINK_PROXY_KEEPALIVE=off disable; keep undici defaults
|
|
19
|
+
* NEUROLINK_PROXY_KEEPALIVE_MS idle keep-alive timeout (default 30000)
|
|
20
|
+
* NEUROLINK_PROXY_KEEPALIVE_MAX_MS keep-alive upper bound (default 600000)
|
|
21
|
+
* NEUROLINK_PROXY_MAX_CONNECTIONS max pooled connections per origin (default 64)
|
|
22
|
+
*/
|
|
23
|
+
import { Agent, setGlobalDispatcher } from "undici";
|
|
24
|
+
import { logger } from "../utils/logger.js";
|
|
25
|
+
function readPositiveInt(name, fallback) {
|
|
26
|
+
const raw = process.env[name];
|
|
27
|
+
if (!raw) {
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32
|
+
}
|
|
33
|
+
let configured = false;
|
|
34
|
+
/**
|
|
35
|
+
* Install the tuned global undici dispatcher. Idempotent — safe to call once at
|
|
36
|
+
* proxy startup. No-op when `NEUROLINK_PROXY_KEEPALIVE` is off/false/0.
|
|
37
|
+
*/
|
|
38
|
+
export function configureProxyKeepAliveDispatcher() {
|
|
39
|
+
if (configured) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
configured = true;
|
|
43
|
+
const toggle = (process.env.NEUROLINK_PROXY_KEEPALIVE ?? "").toLowerCase();
|
|
44
|
+
if (toggle === "off" || toggle === "false" || toggle === "0") {
|
|
45
|
+
logger.debug("[proxy] keep-alive dispatcher disabled via env");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const keepAliveTimeout = readPositiveInt("NEUROLINK_PROXY_KEEPALIVE_MS", 30_000);
|
|
49
|
+
const keepAliveMaxTimeout = readPositiveInt("NEUROLINK_PROXY_KEEPALIVE_MAX_MS", 600_000);
|
|
50
|
+
const connections = readPositiveInt("NEUROLINK_PROXY_MAX_CONNECTIONS", 64);
|
|
51
|
+
setGlobalDispatcher(new Agent({
|
|
52
|
+
keepAliveTimeout,
|
|
53
|
+
keepAliveMaxTimeout,
|
|
54
|
+
connections,
|
|
55
|
+
pipelining: 1,
|
|
56
|
+
}));
|
|
57
|
+
logger.debug(`[proxy] tuned undici dispatcher installed ` +
|
|
58
|
+
`(keepAliveTimeout=${keepAliveTimeout}ms, ` +
|
|
59
|
+
`keepAliveMaxTimeout=${keepAliveMaxTimeout}ms, connections=${connections})`);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=proxyDispatcher.js.map
|
|
@@ -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;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ModelMessage, SystemModelMessage } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Partition a built message array into system messages and the rest, so the
|
|
4
|
+
* system prompt can ride `generateText`'s top-level `system` option instead of
|
|
5
|
+
* the `messages` array.
|
|
6
|
+
*
|
|
7
|
+
* The AI SDK deprecates system-role entries inside `messages` (warns by default
|
|
8
|
+
* from ai@6.0.170 via the `allowSystemInMessages` option, rejected by default
|
|
9
|
+
* in v7, flagged as a prompt-injection risk). The `system` option accepts full
|
|
10
|
+
* SystemModelMessage objects, so per-message providerOptions (e.g. the anthropic
|
|
11
|
+
* cacheControl breakpoint set by buildMessagesArray) survive the move. See
|
|
12
|
+
* issue #1024.
|
|
13
|
+
*
|
|
14
|
+
* Order is preserved within both partitions and the input is not mutated.
|
|
15
|
+
*
|
|
16
|
+
* Guard: the partition only runs when it leaves a non-empty `messages` array.
|
|
17
|
+
* If every message is a system message (a system-only priming call) — or there
|
|
18
|
+
* are no system messages at all — the original array is returned untouched with
|
|
19
|
+
* `system: undefined`. The AI SDK rejects an empty `messages` array, and a
|
|
20
|
+
* system-only call has no hoisted form (the SDK also requires a non-system
|
|
21
|
+
* message), so that degenerate case is deliberately left on the old
|
|
22
|
+
* system-in-`messages` path rather than made to throw — it keeps whatever
|
|
23
|
+
* behaviour it had before #1024. The normal system-plus-conversation path is
|
|
24
|
+
* always hoisted.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractSystemMessages(messages: ModelMessage[]): {
|
|
27
|
+
system: SystemModelMessage[] | undefined;
|
|
28
|
+
messages: ModelMessage[];
|
|
29
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partition a built message array into system messages and the rest, so the
|
|
3
|
+
* system prompt can ride `generateText`'s top-level `system` option instead of
|
|
4
|
+
* the `messages` array.
|
|
5
|
+
*
|
|
6
|
+
* The AI SDK deprecates system-role entries inside `messages` (warns by default
|
|
7
|
+
* from ai@6.0.170 via the `allowSystemInMessages` option, rejected by default
|
|
8
|
+
* in v7, flagged as a prompt-injection risk). The `system` option accepts full
|
|
9
|
+
* SystemModelMessage objects, so per-message providerOptions (e.g. the anthropic
|
|
10
|
+
* cacheControl breakpoint set by buildMessagesArray) survive the move. See
|
|
11
|
+
* issue #1024.
|
|
12
|
+
*
|
|
13
|
+
* Order is preserved within both partitions and the input is not mutated.
|
|
14
|
+
*
|
|
15
|
+
* Guard: the partition only runs when it leaves a non-empty `messages` array.
|
|
16
|
+
* If every message is a system message (a system-only priming call) — or there
|
|
17
|
+
* are no system messages at all — the original array is returned untouched with
|
|
18
|
+
* `system: undefined`. The AI SDK rejects an empty `messages` array, and a
|
|
19
|
+
* system-only call has no hoisted form (the SDK also requires a non-system
|
|
20
|
+
* message), so that degenerate case is deliberately left on the old
|
|
21
|
+
* system-in-`messages` path rather than made to throw — it keeps whatever
|
|
22
|
+
* behaviour it had before #1024. The normal system-plus-conversation path is
|
|
23
|
+
* always hoisted.
|
|
24
|
+
*/
|
|
25
|
+
export function extractSystemMessages(messages) {
|
|
26
|
+
const system = [];
|
|
27
|
+
const rest = [];
|
|
28
|
+
for (const message of messages) {
|
|
29
|
+
if (message.role === "system") {
|
|
30
|
+
system.push(message);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
rest.push(message);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Only hoist when a non-empty messages array remains. A system-only call (or
|
|
37
|
+
// a no-system array) passes through unchanged: hoisting would empty
|
|
38
|
+
// `messages`, which the SDK rejects, and a system-only call has no compliant
|
|
39
|
+
// hoisted form.
|
|
40
|
+
if (system.length === 0 || rest.length === 0) {
|
|
41
|
+
return { system: undefined, messages };
|
|
42
|
+
}
|
|
43
|
+
return { system, messages: rest };
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=systemMessages.js.map
|
|
@@ -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),
|