@juspay/neurolink 9.86.0 → 9.86.2

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.
@@ -13,7 +13,7 @@
13
13
  * Nothing here imports from "ai" or "@ai-sdk/*". The whole point of this
14
14
  * module is to be the native replacement for the AI SDK's OpenAI wrapper.
15
15
  */
16
- import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, Tool } from "../types/index.js";
16
+ import type { OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatMessage, OpenAICompatMessageContent, OpenAICompatResponseFormat, OpenAICompatSSEResult, OpenAICompatStreamChunk, OpenAICompatToolChoiceWire, OpenAICompatV3CallToolChoice, OpenAICompatV3CallTools, DeferredUsage, Tool } from "../types/index.js";
17
17
  export declare const stripTrailingSlash: (s: string) => string;
18
18
  export declare const safeStringify: (value: unknown) => string;
19
19
  export declare const stringifyToolInput: (input: unknown) => string;
@@ -38,17 +38,9 @@ export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompa
38
38
  export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void, onReasoningDelta?: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
39
39
  export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
40
40
  export declare const createDeferredAnalytics: () => {
41
- usagePromise: Promise<{
42
- promptTokens: number;
43
- completionTokens: number;
44
- totalTokens: number;
45
- }>;
41
+ usagePromise: Promise<DeferredUsage>;
46
42
  finishPromise: Promise<string>;
47
- resolveUsage: (u: {
48
- promptTokens: number;
49
- completionTokens: number;
50
- totalTokens: number;
51
- }) => void;
43
+ resolveUsage: (u: DeferredUsage) => void;
52
44
  resolveFinish: (reason: string) => void;
53
45
  };
54
46
  export declare const createChunkQueue: () => {
@@ -35,5 +35,10 @@ export declare function loadAccountQuota(accountKey: string): Promise<AccountQuo
35
35
  * Update quota for a single account.
36
36
  * Updates in-memory cache immediately (non-blocking),
37
37
  * then debounces the disk write to every 5 seconds.
38
+ *
39
+ * Loads the persisted file into the cache before the first write so a save
40
+ * after a process restart merges with existing entries instead of rewriting
41
+ * the file with only the accounts used since boot (which silently erased
42
+ * other accounts' snapshots and blinded quota-aware routing to them).
38
43
  */
39
44
  export declare function saveAccountQuota(accountKey: string, quota: AccountQuota): Promise<void>;
@@ -174,10 +174,17 @@ export async function loadAccountQuota(accountKey) {
174
174
  * Update quota for a single account.
175
175
  * Updates in-memory cache immediately (non-blocking),
176
176
  * then debounces the disk write to every 5 seconds.
177
+ *
178
+ * Loads the persisted file into the cache before the first write so a save
179
+ * after a process restart merges with existing entries instead of rewriting
180
+ * the file with only the accounts used since boot (which silently erased
181
+ * other accounts' snapshots and blinded quota-aware routing to them).
177
182
  */
178
183
  export async function saveAccountQuota(accountKey, quota) {
184
+ if (!cacheLoaded) {
185
+ await loadAccountQuotas();
186
+ }
179
187
  memoryCache[accountKey] = quota;
180
- cacheLoaded = true;
181
188
  dirty = true;
182
189
  scheduleFlush();
183
190
  }
@@ -44,18 +44,32 @@ declare function resetEpochToMs(resetEpoch: number | undefined, now: number): nu
44
44
  * allow a couple of jittered same-account retries, then a short cooldown.
45
45
  */
46
46
  declare function planCooldownFor429(quota: AccountQuota | null, retryAfterMs: number, now: number): AccountCooldownPlan;
47
+ /**
48
+ * Seed each account's runtime quota from the persisted snapshots in
49
+ * ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
50
+ * in-memory only, so without this the quota-aware ordering is blind after a
51
+ * proxy restart: all accounts tie, selection falls back to token-store
52
+ * enumeration order, and the first account served becomes self-reinforcing
53
+ * (it alone has data) — starving the others regardless of their resets.
54
+ * Never overwrites fresher in-memory quota; stale disk snapshots degrade
55
+ * gracefully because past reset timestamps are ignored by resetEpochToMs.
56
+ */
57
+ declare function seedRuntimeQuotasFromDisk(accounts: ProxyPassthroughAccount[]): Promise<void>;
47
58
  /**
48
59
  * Order accounts to MAXIMIZE quota utilization (fill-first, smart order):
49
60
  * spend the account whose window refreshes SOONEST first, so its about-to-reset
50
61
  * allowance isn't wasted, then move to accounts with longer-dated resets.
51
62
  *
52
63
  * Priority among usable accounts:
53
- * 1. soonest WEEKLY (7d) reset the scarce, use-it-or-lose-it ceiling
54
- * 2. soonest SESSION (5h) reset
55
- * 3. highest weekly utilization finish off the one closest to done
56
- * Accounts with no quota data yet keep insertion order (stable sort) and sit
57
- * after those with a known soonest reset. Cooling/rejected accounts sort last,
58
- * soonest-back-to-service first, as last resort.
64
+ * 1. no quota data yet probe first: one request reveals its windows and
65
+ * self-corrects the ordering. (Ranking unknowns last would starve them
66
+ * forever: never picked never observed never comparable.)
67
+ * 2. soonest WEEKLY (7d) reset — the scarce, use-it-or-lose-it ceiling
68
+ * 3. soonest SESSION (5h) reset
69
+ * 4. highest weekly utilization finish off the one closest to done
70
+ * 5. configured primary account, then insertion order
71
+ * Cooling/rejected accounts sort last, soonest-back-to-service first, as
72
+ * last resort.
59
73
  */
60
74
  declare function orderAccountsByQuota(accounts: ProxyPassthroughAccount[], now: number): ProxyPassthroughAccount[];
61
75
  /**
@@ -95,6 +109,8 @@ export declare const __testHooks: {
95
109
  planCooldownFor429: typeof planCooldownFor429;
96
110
  orderAccountsByQuota: typeof orderAccountsByQuota;
97
111
  resetEpochToMs: typeof resetEpochToMs;
112
+ seedRuntimeQuotasFromDisk: typeof seedRuntimeQuotasFromDisk;
113
+ getAccountRuntimeState: (key: string) => RuntimeAccountState | undefined;
98
114
  setConfiguredPrimaryAccountKey: (key: string | undefined) => void;
99
115
  getConfiguredPrimaryAccountKey: () => string | undefined;
100
116
  setPrimaryAccountIndex: (index: number) => void;
@@ -13,7 +13,7 @@ import { access, readFile } from "node:fs/promises";
13
13
  import { homedir } from "node:os";
14
14
  import { join } from "node:path";
15
15
  import { buildStableClaudeCodeBillingHeader, CLAUDE_CLI_USER_AGENT, CLAUDE_CODE_OAUTH_BETAS, getOrCreateClaudeCodeIdentity, parseClaudeCodeUserId, } from "../../auth/anthropicOAuth.js";
16
- import { parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
16
+ import { loadAccountQuotas, parseQuotaHeaders, saveAccountQuota, } from "../../proxy/accountQuota.js";
17
17
  import { buildClaudeError, ClaudeStreamSerializer, generateToolUseId, parseClaudeRequest, serializeClaudeResponse, } from "../../proxy/claudeFormat.js";
18
18
  import { buildAnthropicModelsListResponse, buildTranslationOptions, extractText, extractToolArgs, extractUsageFromStreamResult, handleTranslatedJsonRequest, handleTranslatedStreamRequest, hasTranslatedOutput, } from "../../proxy/proxyTranslationEngine.js";
19
19
  import { tracers } from "../../telemetry/tracers.js";
@@ -242,6 +242,30 @@ function maybeCoolFromQuota(state, quota, now) {
242
242
  logger.always(`[proxy] proactively cooling account (${reason}) ~${minutesUntil(clamped, now)}m from success-response quota (status rejected)`);
243
243
  }
244
244
  }
245
+ /**
246
+ * Seed each account's runtime quota from the persisted snapshots in
247
+ * ~/.neurolink/account-quotas.json (keyed by label). Runtime state is
248
+ * in-memory only, so without this the quota-aware ordering is blind after a
249
+ * proxy restart: all accounts tie, selection falls back to token-store
250
+ * enumeration order, and the first account served becomes self-reinforcing
251
+ * (it alone has data) — starving the others regardless of their resets.
252
+ * Never overwrites fresher in-memory quota; stale disk snapshots degrade
253
+ * gracefully because past reset timestamps are ignored by resetEpochToMs.
254
+ */
255
+ async function seedRuntimeQuotasFromDisk(accounts) {
256
+ try {
257
+ const persisted = await loadAccountQuotas();
258
+ for (const account of accounts) {
259
+ const state = getOrCreateRuntimeState(account.key);
260
+ if (!state.quota && persisted[account.label]) {
261
+ state.quota = persisted[account.label];
262
+ }
263
+ }
264
+ }
265
+ catch {
266
+ // Non-fatal: seeding is best-effort; ordering falls back to probe-first.
267
+ }
268
+ }
245
269
  /** Quota-aware selection is on by default; disable with
246
270
  * NEUROLINK_PROXY_QUOTA_ROUTING=off|false|0. Only affects the fill-first
247
271
  * strategy (round-robin keeps strict rotation). */
@@ -260,6 +284,7 @@ function accountSortMetrics(accountKey, now) {
260
284
  const sessionRejected = q?.sessionStatus === "rejected" && sessionReset !== undefined;
261
285
  return {
262
286
  usable: !coolingActive && !weeklyRejected && !sessionRejected,
287
+ hasQuota: !!q,
263
288
  coolingUntil: st?.coolingUntil ?? 0,
264
289
  weeklyReset: weeklyReset ?? Number.POSITIVE_INFINITY,
265
290
  sessionReset: sessionReset ?? Number.POSITIVE_INFINITY,
@@ -272,14 +297,18 @@ function accountSortMetrics(accountKey, now) {
272
297
  * allowance isn't wasted, then move to accounts with longer-dated resets.
273
298
  *
274
299
  * Priority among usable accounts:
275
- * 1. soonest WEEKLY (7d) reset the scarce, use-it-or-lose-it ceiling
276
- * 2. soonest SESSION (5h) reset
277
- * 3. highest weekly utilization finish off the one closest to done
278
- * Accounts with no quota data yet keep insertion order (stable sort) and sit
279
- * after those with a known soonest reset. Cooling/rejected accounts sort last,
280
- * soonest-back-to-service first, as last resort.
300
+ * 1. no quota data yet probe first: one request reveals its windows and
301
+ * self-corrects the ordering. (Ranking unknowns last would starve them
302
+ * forever: never picked never observed never comparable.)
303
+ * 2. soonest WEEKLY (7d) reset — the scarce, use-it-or-lose-it ceiling
304
+ * 3. soonest SESSION (5h) reset
305
+ * 4. highest weekly utilization finish off the one closest to done
306
+ * 5. configured primary account, then insertion order
307
+ * Cooling/rejected accounts sort last, soonest-back-to-service first, as
308
+ * last resort.
281
309
  */
282
310
  function orderAccountsByQuota(accounts, now) {
311
+ const primaryKey = configuredPrimaryAccountKey;
283
312
  return [...accounts].sort((a, b) => {
284
313
  const ma = accountSortMetrics(a.key, now);
285
314
  const mb = accountSortMetrics(b.key, now);
@@ -291,13 +320,22 @@ function orderAccountsByQuota(accounts, now) {
291
320
  const bu = mb.coolingUntil || Number.POSITIVE_INFINITY;
292
321
  return au - bu;
293
322
  }
323
+ if (ma.hasQuota !== mb.hasQuota) {
324
+ return ma.hasQuota ? 1 : -1;
325
+ }
294
326
  if (ma.weeklyReset !== mb.weeklyReset) {
295
327
  return ma.weeklyReset - mb.weeklyReset;
296
328
  }
297
329
  if (ma.sessionReset !== mb.sessionReset) {
298
330
  return ma.sessionReset - mb.sessionReset;
299
331
  }
300
- return mb.weeklyUsed - ma.weeklyUsed;
332
+ if (ma.weeklyUsed !== mb.weeklyUsed) {
333
+ return mb.weeklyUsed - ma.weeklyUsed;
334
+ }
335
+ if (primaryKey && (a.key === primaryKey) !== (b.key === primaryKey)) {
336
+ return a.key === primaryKey ? -1 : 1;
337
+ }
338
+ return 0;
301
339
  });
302
340
  }
303
341
  // ---------------------------------------------------------------------------
@@ -1280,6 +1318,7 @@ async function loadClaudeProxyAccounts(args) {
1280
1318
  state.lastToken = account.token;
1281
1319
  state.lastRefreshToken = account.refreshToken;
1282
1320
  }
1321
+ await seedRuntimeQuotasFromDisk(accounts);
1283
1322
  const enabledAccounts = accounts.filter((account) => {
1284
1323
  return !getOrCreateRuntimeState(account.key).permanentlyDisabled;
1285
1324
  });
@@ -3777,6 +3816,11 @@ export const __testHooks = {
3777
3816
  planCooldownFor429,
3778
3817
  orderAccountsByQuota,
3779
3818
  resetEpochToMs,
3819
+ seedRuntimeQuotasFromDisk,
3820
+ getAccountRuntimeState: (key) => {
3821
+ const state = accountRuntimeState.get(key);
3822
+ return state ? { ...state } : undefined;
3823
+ },
3780
3824
  setConfiguredPrimaryAccountKey: (key) => {
3781
3825
  configuredPrimaryAccountKey = key;
3782
3826
  },
@@ -223,6 +223,11 @@ export type RawUsageObject = {
223
223
  cacheReadInputTokens?: number;
224
224
  cacheCreationTokens?: number;
225
225
  cacheReadTokens?: number;
226
+ cachedInputTokens?: number;
227
+ inputTokenDetails?: {
228
+ cacheReadTokens?: number;
229
+ cacheWriteTokens?: number;
230
+ };
226
231
  prompt_tokens_details?: {
227
232
  cached_tokens?: number;
228
233
  };
@@ -232,6 +237,18 @@ export type RawUsageObject = {
232
237
  thinkingTokens?: number;
233
238
  usage?: RawUsageObject;
234
239
  };
240
+ /**
241
+ * Aggregated usage resolved by a provider's deferred-analytics pair after a
242
+ * multi-step stream loop ends. The cache fields are optional — only providers
243
+ * with prompt caching (Anthropic) populate them.
244
+ */
245
+ export type DeferredUsage = {
246
+ promptTokens: number;
247
+ completionTokens: number;
248
+ totalTokens: number;
249
+ cacheReadTokens?: number;
250
+ cacheCreationTokens?: number;
251
+ };
235
252
  /**
236
253
  * Options for token extraction from raw usage objects.
237
254
  */
@@ -1,4 +1,6 @@
1
- import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../types/index.js";
1
+ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput, VertexAnthropicMessage } from "../types/index.js";
2
+ /** Anthropic allows at most four `cache_control` breakpoints per request. */
3
+ export declare const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
2
4
  /**
3
5
  * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
4
6
  *
@@ -13,3 +15,29 @@ import type { VertexAnthropicCacheInput, VertexAnthropicCacheOutput } from "../t
13
15
  * Pure: the inputs are cloned, never mutated.
14
16
  */
15
17
  export declare function applyVertexAnthropicCacheBreakpoints(input: VertexAnthropicCacheInput): VertexAnthropicCacheOutput;
18
+ /**
19
+ * Count the `cache_control` markers already present on a request. The direct
20
+ * Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
21
+ * placed — MessageBuilder tags the system prompt, GenerationHandler tags the
22
+ * last tool definition, and message content blocks may carry translated
23
+ * AI-SDK markers. Anthropic rejects requests with more than four markers, so
24
+ * any additional history breakpoints must fit in the remaining budget.
25
+ */
26
+ export declare function countAnthropicCacheMarkers(input: {
27
+ system?: string | ReadonlyArray<{
28
+ cache_control?: unknown;
29
+ }> | undefined;
30
+ tools?: ReadonlyArray<{
31
+ cache_control?: unknown;
32
+ }> | undefined;
33
+ messages: ReadonlyArray<VertexAnthropicMessage>;
34
+ }): number;
35
+ /**
36
+ * Rolling history breakpoints for request paths whose stable-prefix markers
37
+ * are managed upstream (direct Anthropic: system via MessageBuilder, last
38
+ * tool via GenerationHandler). Marks the last content block of up to `budget`
39
+ * tail messages; a tail block that already carries a marker is left as-is
40
+ * without consuming budget (it already serves as that breakpoint). Pure —
41
+ * the input array is cloned, never mutated.
42
+ */
43
+ export declare function applyAnthropicHistoryCacheBreakpoints(input: ReadonlyArray<VertexAnthropicMessage>, budget: number): VertexAnthropicMessage[];
@@ -16,7 +16,8 @@
16
16
  */
17
17
  const EPHEMERAL = { type: "ephemeral" };
18
18
  /** Anthropic allows at most four `cache_control` breakpoints per request. */
19
- const MAX_BREAKPOINTS = 4;
19
+ export const ANTHROPIC_MAX_CACHE_BREAKPOINTS = 4;
20
+ const MAX_BREAKPOINTS = ANTHROPIC_MAX_CACHE_BREAKPOINTS;
20
21
  /**
21
22
  * Annotate a native Vertex+Claude request with prompt-cache breakpoints.
22
23
  *
@@ -60,6 +61,58 @@ export function applyVertexAnthropicCacheBreakpoints(input) {
60
61
  }
61
62
  return { system, tools, messages };
62
63
  }
64
+ /**
65
+ * Count the `cache_control` markers already present on a request. The direct
66
+ * Anthropic path (AI-SDK pipeline) arrives with markers the upstream layers
67
+ * placed — MessageBuilder tags the system prompt, GenerationHandler tags the
68
+ * last tool definition, and message content blocks may carry translated
69
+ * AI-SDK markers. Anthropic rejects requests with more than four markers, so
70
+ * any additional history breakpoints must fit in the remaining budget.
71
+ */
72
+ export function countAnthropicCacheMarkers(input) {
73
+ let count = 0;
74
+ if (Array.isArray(input.system)) {
75
+ count += input.system.filter((b) => b.cache_control).length;
76
+ }
77
+ if (input.tools) {
78
+ count += input.tools.filter((t) => t.cache_control).length;
79
+ }
80
+ for (const message of input.messages) {
81
+ if (Array.isArray(message.content)) {
82
+ count += message.content.filter((b) => b.cache_control).length;
83
+ }
84
+ }
85
+ return count;
86
+ }
87
+ /**
88
+ * Rolling history breakpoints for request paths whose stable-prefix markers
89
+ * are managed upstream (direct Anthropic: system via MessageBuilder, last
90
+ * tool via GenerationHandler). Marks the last content block of up to `budget`
91
+ * tail messages; a tail block that already carries a marker is left as-is
92
+ * without consuming budget (it already serves as that breakpoint). Pure —
93
+ * the input array is cloned, never mutated.
94
+ */
95
+ export function applyAnthropicHistoryCacheBreakpoints(input, budget) {
96
+ const messages = input.map((m) => ({ ...m }));
97
+ let remaining = Math.max(0, Math.min(budget, MAX_BREAKPOINTS));
98
+ for (let i = messages.length - 1; i >= 0 && remaining > 0; i--) {
99
+ if (lastContentBlockHasMarker(messages[i])) {
100
+ continue;
101
+ }
102
+ if (markLastContentBlock(messages, i)) {
103
+ remaining--;
104
+ }
105
+ }
106
+ return messages;
107
+ }
108
+ /** True when the last content block of a message already carries `cache_control`. */
109
+ function lastContentBlockHasMarker(message) {
110
+ if (!Array.isArray(message.content) || message.content.length === 0) {
111
+ return false;
112
+ }
113
+ const last = message.content[message.content.length - 1];
114
+ return !!last.cache_control;
115
+ }
63
116
  /**
64
117
  * Place a cache breakpoint on the last content block of `messages[i]`.
65
118
  * Anthropic attaches `cache_control` to a content block, not the message
@@ -30,12 +30,17 @@ export declare function extractTotalTokens(usage: RawUsageObject, input: number,
30
30
  export declare function extractReasoningTokens(usage: RawUsageObject): number | undefined;
31
31
  /**
32
32
  * Extract cache creation token count from various provider formats
33
- * Supports: cacheCreationInputTokens, cacheCreationTokens
33
+ * Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
34
+ * normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
35
+ * direct-Anthropic path reports through generateText).
34
36
  */
35
37
  export declare function extractCacheCreationTokens(usage: RawUsageObject): number | undefined;
36
38
  /**
37
39
  * Extract cache read token count from various provider formats
38
- * Supports: cacheReadInputTokens, cacheReadTokens
40
+ * Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
41
+ * shapes — flat `cachedInputTokens` and nested
42
+ * `inputTokenDetails.cacheReadTokens` (the only shapes the native
43
+ * direct-Anthropic path reports through generateText).
39
44
  */
40
45
  export declare function extractCacheReadTokens(usage: RawUsageObject): number | undefined;
41
46
  /**
@@ -74,7 +74,9 @@ export function extractReasoningTokens(usage) {
74
74
  }
75
75
  /**
76
76
  * Extract cache creation token count from various provider formats
77
- * Supports: cacheCreationInputTokens, cacheCreationTokens
77
+ * Supports: cacheCreationInputTokens, cacheCreationTokens, and the ai@6
78
+ * normalized `inputTokenDetails.cacheWriteTokens` (the only shape the native
79
+ * direct-Anthropic path reports through generateText).
78
80
  */
79
81
  export function extractCacheCreationTokens(usage) {
80
82
  if (typeof usage.cacheCreationInputTokens === "number" &&
@@ -85,11 +87,18 @@ export function extractCacheCreationTokens(usage) {
85
87
  usage.cacheCreationTokens > 0) {
86
88
  return usage.cacheCreationTokens;
87
89
  }
90
+ if (typeof usage.inputTokenDetails?.cacheWriteTokens === "number" &&
91
+ usage.inputTokenDetails.cacheWriteTokens > 0) {
92
+ return usage.inputTokenDetails.cacheWriteTokens;
93
+ }
88
94
  return undefined;
89
95
  }
90
96
  /**
91
97
  * Extract cache read token count from various provider formats
92
- * Supports: cacheReadInputTokens, cacheReadTokens
98
+ * Supports: cacheReadInputTokens, cacheReadTokens, and the ai@6 normalized
99
+ * shapes — flat `cachedInputTokens` and nested
100
+ * `inputTokenDetails.cacheReadTokens` (the only shapes the native
101
+ * direct-Anthropic path reports through generateText).
93
102
  */
94
103
  export function extractCacheReadTokens(usage) {
95
104
  if (typeof usage.cacheReadInputTokens === "number" &&
@@ -99,6 +108,14 @@ export function extractCacheReadTokens(usage) {
99
108
  if (typeof usage.cacheReadTokens === "number" && usage.cacheReadTokens > 0) {
100
109
  return usage.cacheReadTokens;
101
110
  }
111
+ if (typeof usage.cachedInputTokens === "number" &&
112
+ usage.cachedInputTokens > 0) {
113
+ return usage.cachedInputTokens;
114
+ }
115
+ if (typeof usage.inputTokenDetails?.cacheReadTokens === "number" &&
116
+ usage.inputTokenDetails.cacheReadTokens > 0) {
117
+ return usage.inputTokenDetails.cacheReadTokens;
118
+ }
102
119
  return undefined;
103
120
  }
104
121
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "9.86.0",
3
+ "version": "9.86.2",
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": {