@juspay/neurolink 12.25.1 → 12.25.3

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.
@@ -289,8 +289,10 @@ function drainAccountAdmissionWaiters(accountKey, state) {
289
289
  * removed the waiter's cap — and uncapped arrivals count toward `active`, so
290
290
  * that waiter would otherwise never drain. An uncapped request from an older
291
291
  * snapshot (one that predates a reload adding the cap) or from the same one
292
- * leaves waiters queued: their cap is still current. With no generation there
293
- * is no runtime config store, the cap cannot change, and nothing is admitted.
292
+ * leaves waiters queued: their cap is still current. With no generation,
293
+ * which only direct callers such as test hooks pass (the route passes 0 from
294
+ * its fallback snapshot when no runtime config store is attached), there is
295
+ * nothing to compare against and nothing is admitted.
294
296
  */
295
297
  function admitWaitersOfRemovedCap(accountKey, state, generation) {
296
298
  if (generation === undefined) {
@@ -7068,8 +7070,8 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7068
7070
  });
7069
7071
  }
7070
7072
  const { accounts, enabledAccounts, orderedAccounts, bodyStr, requestStart, toolCount, url, clientHeaders, isClaudeClientRequest, sessionId, } = loadedAccounts;
7071
- // Accounts this request sent an upstream attempt to; one skipped only for
7072
- // its admission cap is not among them.
7073
+ // Accounts this request sent an upstream attempt to or whose token refresh
7074
+ // failed; one skipped only for its admission cap is not among them.
7073
7075
  const attemptedAccountKeys = new Set();
7074
7076
  // Only fill-first over more than one account reads a binding (see
7075
7077
  // selectClaudeProxyAccountOrder), so nothing else records one.
@@ -7233,6 +7235,9 @@ async function handleAnthropicRoutedClaudeRequest(args) {
7233
7235
  !preparedAttempt.finalBodyStr ||
7234
7236
  !preparedAttempt.preparedContext ||
7235
7237
  preparedAttempt.fetchStartMs === undefined) {
7238
+ // A failed token refresh is this account failing the request, so a
7239
+ // session bound to it must not stay there when another one serves.
7240
+ attemptedAccountKeys.add(account.key);
7236
7241
  if (transportPermit.probe) {
7237
7242
  providerTransportCoordinator.reportProbeAbandoned(transportPermit);
7238
7243
  }
@@ -1143,8 +1143,10 @@ export type QueuedAccountAdmission = {
1143
1143
  /** One queued request waiting for per-account admission capacity. */
1144
1144
  export type AccountAdmissionWaiter = {
1145
1145
  capacity: number;
1146
- /** Runtime-config generation of the snapshot the request queued under;
1147
- * absent when no runtime config store publishes one. */
1146
+ /** Runtime-config generation of the snapshot the request queued under.
1147
+ * The route always passes one (0 from its fallback snapshot when no
1148
+ * runtime config store is attached); only direct callers such as test
1149
+ * hooks leave it absent. */
1148
1150
  generation?: number;
1149
1151
  resolve: (lease: AccountAdmissionLease) => void;
1150
1152
  };
@@ -1374,7 +1374,8 @@ export async function buildMultimodalMessagesArray(options, provider, model) {
1374
1374
  // ordinary "transcribe this recording" request — took the text-only branch
1375
1375
  // below, where the collected bytes have nowhere to go and only the metadata
1376
1376
  // summary survives.
1377
- const hasNativeAudio = (inp.nativeAudioFiles?.length ?? 0) > 0 && supportsNativeAudio(provider);
1377
+ const hasNativeAudio = (inp.nativeAudioFiles?.length ?? 0) > 0 &&
1378
+ supportsNativeAudio(provider, model);
1378
1379
  // If no images, PDFs or audio, use standard message building and convert to MultimodalChatMessage[]
1379
1380
  if (!hasImages && !hasPDFs && !hasNativeAudio) {
1380
1381
  // #289: CSV content[] items don't need vision, so they never reach the
@@ -1574,7 +1575,7 @@ async function appendCsvContentToText(csvItems, baseText) {
1574
1575
  /**
1575
1576
  * Convert advanced content format to provider-specific format
1576
1577
  */
1577
- async function convertContentToProviderFormat(content, provider, _model, pdfOptions, audioFiles = [], outputFormat) {
1578
+ async function convertContentToProviderFormat(content, provider, model, pdfOptions, audioFiles = [], outputFormat) {
1578
1579
  const textContent = content.find((c) => c.type === "text");
1579
1580
  const imageContent = content.filter((c) => c.type === "image");
1580
1581
  const pdfContent = content.filter((c) => c.type === "pdf");
@@ -1596,7 +1597,7 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
1596
1597
  // change exists to stop, reached through a different branch. Gated on the
1597
1598
  // provider accepting audio so one that cannot keeps the cheaper plain-text
1598
1599
  // shape rather than an array carrying a part it will ignore.
1599
- const deliversAudio = audioFiles.length > 0 && supportsNativeAudio(provider);
1600
+ const deliversAudio = audioFiles.length > 0 && supportsNativeAudio(provider, model);
1600
1601
  const hasMultimodal = imageContent.length > 0 || pdfContent.length > 0 || deliversAudio;
1601
1602
  // Validate that we have at least some content
1602
1603
  if (!hasMultimodal && !text) {
@@ -1627,7 +1628,7 @@ async function convertContentToProviderFormat(content, provider, _model, pdfOpti
1627
1628
  await enforceAggregatePdfLimits(pdfFiles, provider, {
1628
1629
  trustSuppliedPageCounts: false,
1629
1630
  });
1630
- return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, _model, {
1631
+ return await convertMultimodalToProviderFormat(text, images, pdfFiles, provider, model, {
1631
1632
  audioFiles,
1632
1633
  outputFormat,
1633
1634
  });
@@ -2225,7 +2226,7 @@ mediaOptions = {}) {
2225
2226
  // itself rather than replacing the description — a model asked "how long is
2226
2227
  // this?" keeps the exact answer, and one asked "what is said?" can now
2227
2228
  // answer at all.
2228
- if (audioFiles.length > 0 && supportsNativeAudio(provider)) {
2229
+ if (audioFiles.length > 0 && supportsNativeAudio(provider, model)) {
2229
2230
  for (const audio of audioFiles) {
2230
2231
  // Derived from the trimmed basename so a directory containing a dot
2231
2232
  // (`/srv/v1.2/recording`) cannot be mistaken for the file's extension.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.25.1",
3
+ "version": "12.25.3",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
6
6
  "author": {
@@ -259,7 +259,8 @@
259
259
  "test:docs-mcp": "pnpm exec tsx test/continuous-test-suite-docs-mcp.ts",
260
260
  "test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts",
261
261
  "test:toolfree-guard-overhead": "pnpm exec tsx test/continuous-test-suite-toolfree-guard-overhead.ts",
262
- "test:native-failure-events": "pnpm exec tsx test/continuous-test-suite-native-failure-events.ts"
262
+ "test:native-failure-events": "pnpm exec tsx test/continuous-test-suite-native-failure-events.ts",
263
+ "test:native-audio-model-aware": "pnpm exec tsx test/continuous-test-suite-native-audio-model-aware.ts"
263
264
  },
264
265
  "files": [
265
266
  "dist",