@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -0
  3. package/dist/browser/neurolink.min.js +342 -342
  4. package/dist/cli/commands/proxy.js +5 -0
  5. package/dist/cli/factories/commandFactory.d.ts +2 -1
  6. package/dist/cli/factories/commandFactory.js +36 -16
  7. package/dist/cli/utils/audioPlayer.d.ts +37 -0
  8. package/dist/cli/utils/audioPlayer.js +138 -0
  9. package/dist/core/modules/GenerationHandler.js +7 -1
  10. package/dist/index.d.ts +6 -1
  11. package/dist/index.js +14 -6
  12. package/dist/lib/core/modules/GenerationHandler.js +7 -1
  13. package/dist/lib/index.d.ts +6 -1
  14. package/dist/lib/index.js +14 -6
  15. package/dist/lib/providers/googleAiStudio.js +25 -4
  16. package/dist/lib/providers/googleNativeGemini3.js +22 -2
  17. package/dist/lib/providers/googleVertex.js +28 -4
  18. package/dist/lib/proxy/proxyDispatcher.d.ts +5 -0
  19. package/dist/lib/proxy/proxyDispatcher.js +61 -0
  20. package/dist/lib/types/common.d.ts +3 -0
  21. package/dist/lib/types/providers.d.ts +8 -0
  22. package/dist/lib/utils/pricing.js +144 -26
  23. package/dist/lib/utils/systemMessages.d.ts +29 -0
  24. package/dist/lib/utils/systemMessages.js +45 -0
  25. package/dist/lib/utils/tokenUtils.d.ts +11 -0
  26. package/dist/lib/utils/tokenUtils.js +33 -2
  27. package/dist/providers/googleAiStudio.js +25 -4
  28. package/dist/providers/googleNativeGemini3.js +22 -2
  29. package/dist/providers/googleVertex.js +28 -4
  30. package/dist/proxy/proxyDispatcher.d.ts +5 -0
  31. package/dist/proxy/proxyDispatcher.js +60 -0
  32. package/dist/types/common.d.ts +3 -0
  33. package/dist/types/providers.d.ts +8 -0
  34. package/dist/utils/pricing.js +144 -26
  35. package/dist/utils/systemMessages.d.ts +29 -0
  36. package/dist/utils/systemMessages.js +44 -0
  37. package/dist/utils/tokenUtils.d.ts +11 -0
  38. package/dist/utils/tokenUtils.js +33 -2
  39. package/package.json +3 -2
@@ -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 { rawResponseParts, stepFunctionCalls, inputTokens, outputTokens };
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 { rawResponseParts, stepFunctionCalls, inputTokens, outputTokens };
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: totalInputTokens,
1911
+ input: adjustedInputTokens,
1903
1912
  output: totalOutputTokens,
1904
- total: totalInputTokens + totalOutputTokens,
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: totalInputTokens,
2822
+ input: adjustedInputTokens,
2802
2823
  output: totalOutputTokens,
2803
- total: totalInputTokens + totalOutputTokens,
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,5 @@
1
+ /**
2
+ * Install the tuned global undici dispatcher. Idempotent — safe to call once at
3
+ * proxy startup. No-op when `NEUROLINK_PROXY_KEEPALIVE` is off/false/0.
4
+ */
5
+ export declare function configureProxyKeepAliveDispatcher(): void;
@@ -0,0 +1,60 @@
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
+ }
@@ -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": { input: 3.0 / 1_000_000, output: 15.0 / 1_000_000 },
91
- "claude-3-haiku": { input: 0.25 / 1_000_000, output: 1.25 / 1_000_000 },
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
- "gpt-5.4": { input: 2.5 / 1_000_000, output: 15.0 / 1_000_000 },
142
- "gpt-5.2": { input: 1.75 / 1_000_000, output: 14.0 / 1_000_000 },
143
- "gpt-5.1": { input: 0.625 / 1_000_000, output: 5.0 / 1_000_000 },
144
- "gpt-5.1-codex": { input: 1.25 / 1_000_000, output: 10.0 / 1_000_000 },
145
- "gpt-5": { input: 1.25 / 1_000_000, output: 10.0 / 1_000_000 },
146
- "gpt-5-mini": { input: 0.25 / 1_000_000, output: 2.0 / 1_000_000 },
147
- "gpt-5-nano": { input: 0.05 / 1_000_000, output: 0.4 / 1_000_000 },
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": { input: 2.0 / 1_000_000, output: 8.0 / 1_000_000 },
150
- "gpt-4.1-mini": { input: 0.4 / 1_000_000, output: 1.6 / 1_000_000 },
151
- "gpt-4.1-nano": { input: 0.1 / 1_000_000, output: 0.4 / 1_000_000 },
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": { input: 2.5 / 1_000_000, output: 10.0 / 1_000_000 },
154
- "gpt-4o-mini": { input: 0.15 / 1_000_000, output: 0.6 / 1_000_000 },
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: { input: 2.0 / 1_000_000, output: 8.0 / 1_000_000 },
157
- "o3-mini": { input: 1.1 / 1_000_000, output: 4.4 / 1_000_000 },
158
- "o4-mini": { input: 1.1 / 1_000_000, output: 4.4 / 1_000_000 },
159
- o1: { input: 15.0 / 1_000_000, output: 60.0 / 1_000_000 },
160
- "o1-mini": { input: 0.55 / 1_000_000, output: 2.2 / 1_000_000 },
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": { input: 0.3 / 1_000_000, output: 2.5 / 1_000_000 },
201
- "gemini-2.5-pro": { input: 1.25 / 1_000_000, output: 10.0 / 1_000_000 },
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": { input: 0.15 / 1_000_000, output: 0.6 / 1_000_000 },
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": { input: 1.25 / 1_000_000, output: 5.0 / 1_000_000 },
214
- "gemini-1.5-flash": { input: 0.075 / 1_000_000, output: 0.3 / 1_000_000 },
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 && rates.cacheRead) {
598
- cost += usage.cacheReadTokens * rates.cacheRead;
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,44 @@
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
+ }
@@ -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
- const input = extractInputTokens(usage);
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
- const cacheReadTokens = extractCacheReadTokens(usage);
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.81.3",
3
+ "version": "9.83.0",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applications with 21+ providers: OpenAI, Anthropic, Google AI Studio, Google Vertex, AWS Bedrock, Azure OpenAI, Mistral, LiteLLM, SageMaker, Hugging Face, Ollama, OpenAI-compatible, OpenRouter, DeepSeek, NVIDIA NIM, LM Studio, llama.cpp, plus voice (OpenAI TTS, ElevenLabs, Deepgram, Azure Speech).",
6
6
  "author": {
@@ -135,9 +135,10 @@
135
135
  "test:unit:vitest": "pnpm exec vitest run test/toolRouting.test.ts",
136
136
  "test:tool-routing": "pnpm run test:unit:vitest && npx tsx test/continuous-test-suite-tool-routing.ts",
137
137
  "test:classifier-router": "npx tsx test/continuous-test-suite-classifier-router.ts",
138
+ "test:system-messages:vitest": "pnpm exec vitest run test/systemMessages.test.ts",
138
139
  "test:tool-routing-semantic:vitest": "pnpm exec vitest run test/toolRoutingSemantic.test.ts",
139
140
  "test:tool-routing-semantic": "pnpm run test:tool-routing-semantic:vitest && npx tsx test/continuous-test-suite-tool-routing-semantic.ts",
140
- "test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:tool-routing-semantic:vitest && pnpm run test:anthropic-tools-policy && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop",
141
+ "test:unit": "pnpm run test:envguard && pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:bash && pnpm run test:mcp:limits && pnpm run test:mcp:spans && pnpm run test:autoresearch:redis && pnpm run test:unit:vitest && pnpm run test:tool-routing-cli:vitest && pnpm run test:tool-dedup:vitest && pnpm run test:model-pool:vitest && pnpm run test:system-messages:vitest && pnpm run test:tool-routing-semantic:vitest && pnpm run test:anthropic-tools-policy && pnpm run test:anthropic-multimodal && pnpm run test:excel-interop",
141
142
  "// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit)": "",
142
143
  "test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
143
144
  "// CI tier — product output (image/video/TTS/PPT) — costs $$ per run": "",