@juspay/neurolink 12.47.0 → 12.47.1

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.
@@ -19,6 +19,7 @@ import { handleBedrockSetup } from "./setup-bedrock.js";
19
19
  import { handleGCPSetup } from "./setup-gcp.js";
20
20
  import { handleHuggingFaceSetup } from "./setup-huggingface.js";
21
21
  import { handleMistralSetup } from "./setup-mistral.js";
22
+ import { OpenRouterModels, } from "../../constants/enums.js";
22
23
  import { PROVIDER_DESCRIPTORS_BY_NAME } from "../../factories/providerDescriptors.js";
23
24
  import { createCohereConfig, createDeepSeekConfig, createIdeogramConfig, createJinaConfig, createNvidiaNimConfig, createOpenAICompatibleConfig, createRecraftConfig, createReplicateConfig, createStabilityConfig, createVoyageConfig, createTypeSafeConfig, createLayaConfig, createXorConfig, createPerplexityDeciderConfig, createCloudflareClefConfig, satisfiesFallbacks, } from "../../utils/providerConfig.js";
24
25
  import { getCatalogJsonEntries, buildCatalogConfigOptions, } from "../../providers/catalog/loader.js";
@@ -571,13 +572,13 @@ async function handleOpenRouterSetup() {
571
572
  logger.always(chalk.cyan(" export OPENROUTER_API_KEY=your_api_key_here"));
572
573
  logger.always("");
573
574
  logger.always(chalk.yellow("Step 3: Test the configuration"));
574
- logger.always(chalk.cyan(' neurolink generate "Hello!" --provider openrouter --model google/gemini-2.0-flash-exp:free'));
575
+ logger.always(chalk.cyan(` neurolink generate "Hello!" --provider openrouter --model ${OpenRouterModels.GEMINI_2_5_FLASH}`));
575
576
  logger.always("");
576
577
  logger.always(chalk.green("Available models include:"));
577
- logger.always(" • anthropic/claude-3.5-sonnet - Best for analysis");
578
- logger.always(" • openai/gpt-4o - Industry standard");
579
- logger.always(" • google/gemini-2.0-flash-exp:free - Free tier");
580
- logger.always(" • meta-llama/llama-3.1-70b-instruct - Open source");
578
+ logger.always(` • ${OpenRouterModels.CLAUDE_SONNET_4_6} - Best for analysis`);
579
+ logger.always(` • ${OpenRouterModels.GPT_4O} - Industry standard`);
580
+ logger.always(` • ${OpenRouterModels.GEMINI_2_5_FLASH} - Fast and efficient`);
581
+ logger.always(` • ${OpenRouterModels.LLAMA_3_1_70B} - Open source`);
581
582
  logger.always("");
582
583
  logger.always(chalk.gray("See all models at: https://openrouter.ai/models"));
583
584
  }
@@ -46,12 +46,66 @@ function getCopilotEnvPath() {
46
46
  function getProfileCandidates() {
47
47
  return [".zshrc", ".zprofile", ".bashrc", ".bash_profile", ".profile"].map((name) => join(homedir(), name));
48
48
  }
49
+ /** A command that is `source`/`.` of a path ending in exactly `copilot-env.sh`, optionally after `then`/`else`/`do`. */
50
+ const ENV_SCRIPT_SOURCE_COMMAND = /^\s*(?:(?:then|else|do)\s+)?(?:source|\.)\s+(["']?)(?:[^\s"']*\/)?copilot-env\.sh\1(?:\s|$)/;
51
+ /**
52
+ * One profile line as the commands it runs, comment dropped. Quote-aware, so a
53
+ * `;` or `#` inside quotes neither splits nor starts a comment. Heredocs,
54
+ * multi-line strings and line continuations are not modelled, so a source line
55
+ * inside one is still counted, and a source built with `eval` is missed. Both
56
+ * are rare; this guards against the common mentions (a comment, an `echo`, a
57
+ * file-existence test, a `.bak` name) that suppressed the note before.
58
+ */
59
+ function splitProfileCommands(line) {
60
+ const commands = [];
61
+ let current = "";
62
+ let quote = null;
63
+ for (let i = 0; i < line.length; i++) {
64
+ const char = line[i];
65
+ if (quote) {
66
+ current += char;
67
+ if (quote === '"' && char === "\\") {
68
+ current += line[i + 1] ?? "";
69
+ i++;
70
+ }
71
+ else if (char === quote) {
72
+ quote = null;
73
+ }
74
+ continue;
75
+ }
76
+ if (char === "\\") {
77
+ current += char + (line[i + 1] ?? "");
78
+ i++;
79
+ continue;
80
+ }
81
+ if (char === '"' || char === "'") {
82
+ quote = char;
83
+ current += char;
84
+ continue;
85
+ }
86
+ if (char === "#" &&
87
+ (current === "" || /\s/.test(current[current.length - 1]))) {
88
+ break;
89
+ }
90
+ if (char === ";" || char === "&" || char === "|") {
91
+ commands.push(current);
92
+ current = "";
93
+ continue;
94
+ }
95
+ current += char;
96
+ }
97
+ commands.push(current);
98
+ return commands;
99
+ }
49
100
  /** Whether any shell profile already sources the generated script. */
50
101
  async function isEnvScriptSourced() {
51
102
  const fs = await import("fs");
52
103
  for (const profile of getProfileCandidates()) {
53
104
  try {
54
- if (fs.readFileSync(profile, "utf8").includes("copilot-env.sh")) {
105
+ if (fs
106
+ .readFileSync(profile, "utf8")
107
+ .split(/\r?\n/)
108
+ .some((line) => splitProfileCommands(line).some((command) => ENV_SCRIPT_SOURCE_COMMAND.test(command)))) {
55
109
  return true;
56
110
  }
57
111
  }
@@ -122,6 +122,11 @@ export const MODEL_CONTEXT_WINDOWS = {
122
122
  "claude-fable-5-1": 1_000_000,
123
123
  // Claude 5 (mid 2026) — 1M context window
124
124
  "claude-sonnet-5": 1_000_000,
125
+ "claude-opus-5": 1_000_000,
126
+ "claude-fable-5": 1_000_000,
127
+ // Claude 4.7 / 4.8 — 1M context window (platform.claude.com/docs/en/models)
128
+ "claude-opus-4-8": 1_000_000,
129
+ "claude-opus-4-7": 1_000_000,
125
130
  // Claude 4.6 (Feb 2026) — 1M context window
126
131
  "claude-opus-4-6": 1_000_000,
127
132
  "claude-sonnet-4-6": 1_000_000,
@@ -220,6 +225,13 @@ export const MODEL_CONTEXT_WINDOWS = {
220
225
  "claude-sonnet-5-5": 1_000_000,
221
226
  "claude-fable-5-1": 1_000_000,
222
227
  "claude-sonnet-5": 1_000_000,
228
+ // Placed before the 200K `claude-opus-4` prefix key below: the lookup takes
229
+ // the first prefix that matches, so a dated `claude-opus-4-8@...` id would
230
+ // otherwise land on 200K.
231
+ "claude-opus-5": 1_000_000,
232
+ "claude-fable-5": 1_000_000,
233
+ "claude-opus-4-8": 1_000_000,
234
+ "claude-opus-4-7": 1_000_000,
223
235
  "claude-opus-4-6": 1_000_000,
224
236
  "claude-sonnet-4-6": 1_000_000,
225
237
  "claude-sonnet-4-5": 200_000,
@@ -13,15 +13,7 @@
13
13
  */
14
14
  import type { Tool, ToolExecutionGuards } from "../types/index.js";
15
15
  /**
16
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
- * discovered tools into the live record between steps; without this refresh
19
- * they stay invisible to the rest of the turn and every call dies as
20
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
- * `toolsConfig` by reference — and returns true when anything was added.
22
- */
23
- /**
24
- * Everything a native Gemini loop wraps around a tool call that the shared
16
+ * Everything a native provider loop wraps around a tool call that the shared
25
17
  * engine does not do itself.
26
18
  *
27
19
  * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
@@ -13,15 +13,7 @@
13
13
  */
14
14
  import { raceWithAbort, withTimeout } from "../utils/async/index.js";
15
15
  /**
16
- * Mid-turn tool sync for the native Gemini loops that build their snapshot
17
- * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
18
- * discovered tools into the live record between steps; without this refresh
19
- * they stay invisible to the rest of the turn and every call dies as
20
- * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
21
- * `toolsConfig` by reference — and returns true when anything was added.
22
- */
23
- /**
24
- * Everything a native Gemini loop wraps around a tool call that the shared
16
+ * Everything a native provider loop wraps around a tool call that the shared
25
17
  * engine does not do itself.
26
18
  *
27
19
  * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
@@ -22,6 +22,17 @@ export function resolveCredentialKey(providerName) {
22
22
  const normalized = providerName.toLowerCase();
23
23
  return (ProviderFactory.getDescriptor(normalized)?.credentialsKey ?? normalized);
24
24
  }
25
+ /**
26
+ * registerProvider() takes a class as well as a factory (ProviderConstructor),
27
+ * and calling a class without `new` throws. A prototype test cannot tell them
28
+ * apart, because every non-arrow function's prototype points back at itself,
29
+ * so the source text decides. A factory is therefore never constructed and
30
+ * never retried after it throws.
31
+ */
32
+ function isClassConstructor(candidate) {
33
+ return (typeof candidate === "function" &&
34
+ /^class[\s{]/.test(Function.prototype.toString.call(candidate)));
35
+ }
25
36
  /**
26
37
  * True Factory Pattern implementation for AI Providers
27
38
  * Uses registration-based approach to eliminate switch statements
@@ -113,7 +124,11 @@ export class ProviderFactory {
113
124
  if (typeof registration.constructor !== "function") {
114
125
  throw new Error(`Invalid constructor for provider ${providerName}: not a function`);
115
126
  }
116
- const factoryResult = registration.constructor(model, resolvedProviderName, sdk, region, scopedCredentials);
127
+ // A factory keeps being called as a method of the registration, as it
128
+ // always was; only a class is constructed.
129
+ const factoryResult = isClassConstructor(registration.constructor)
130
+ ? new registration.constructor(model, resolvedProviderName, sdk, region, scopedCredentials)
131
+ : registration.constructor(model, resolvedProviderName, sdk, region, scopedCredentials);
117
132
  const result = factoryResult instanceof Promise ? await factoryResult : factoryResult;
118
133
  return result;
119
134
  }
@@ -827,7 +827,7 @@ export class FileReferenceRegistry {
827
827
  extractedText = await this.extractWordText(buffer, ref);
828
828
  break;
829
829
  case "pptx":
830
- extractedText = await this.extractPptxText(buffer);
830
+ extractedText = await this.extractPptxText(buffer, ref.filename);
831
831
  break;
832
832
  case "video":
833
833
  extractedText = await this.extractVideoContent(buffer, ref);
@@ -992,13 +992,13 @@ export class FileReferenceRegistry {
992
992
  /**
993
993
  * Extract text from a PowerPoint file using PptxProcessor.
994
994
  */
995
- async extractPptxText(buffer) {
995
+ async extractPptxText(buffer, filename) {
996
996
  try {
997
997
  const { PptxProcessor } = await import("../processors/document/PptxProcessor.js");
998
998
  return await PptxProcessor.extractText(buffer);
999
999
  }
1000
1000
  catch (err) {
1001
- logger.warn(`[FileReferenceRegistry] PPTX extraction failed: ${err instanceof Error ? err.message : String(err)}`);
1001
+ logger.warn(`[FileReferenceRegistry] PPTX extraction failed for "${filename}": ${err instanceof Error ? err.message : String(err)}`);
1002
1002
  return null;
1003
1003
  }
1004
1004
  }
package/dist/neurolink.js CHANGED
@@ -8640,11 +8640,22 @@ Current user's request: ${currentInput}`;
8640
8640
  }
8641
8641
  yield fallbackChunk;
8642
8642
  }
8643
+ // Read after the drain: a background-loop provider (native Vertex+Claude)
8644
+ // fills these while the stream is consumed, so the values taken before
8645
+ // the loop are still empty for it. Without this, a fallback that ran
8646
+ // tools reports none on the result and in the end-of-turn events, and
8647
+ // the 0-output gate below counts tool calls that had not appeared yet.
8648
+ const finalToolCalls = fallbackResult.toolCalls ?? [];
8649
+ const finalToolResults = fallbackResult.toolResults ?? [];
8643
8650
  if (fallbackRealOutputChunks === 0 &&
8644
- fallbackToolCalls.length === 0 &&
8645
- fallbackToolResults.length === 0) {
8651
+ finalToolCalls.length === 0 &&
8652
+ finalToolResults.length === 0) {
8646
8653
  throw new Error(`Fallback provider ${fallbackRoute.provider} also returned 0 real output chunks (chunkCount=${fallbackChunkCount}, sentinel-only or empty)`);
8647
8654
  }
8655
+ streamState.toolCalls = finalToolCalls;
8656
+ streamState.toolResults = finalToolResults;
8657
+ streamState.finishReason =
8658
+ fallbackResult.finishReason ?? streamState.finishReason;
8648
8659
  // Fallback succeeded - likely guardrails blocked primary
8649
8660
  metadata.fallbackProvider = fallbackRoute.provider;
8650
8661
  metadata.fallbackModel = fallbackRoute.model;
@@ -660,6 +660,7 @@ export class VideoProcessor extends BaseFileProcessor {
660
660
  */
661
661
  async probeVideoWithFfmpeg(filePath) {
662
662
  let report;
663
+ let exitFailure;
663
664
  try {
664
665
  const { stderr } = await runFfmpeg(["-hide_banner", "-i", filePath, "-t", "0", "-f", "null", "-"], { timeoutMs: VIDEO_CONFIG.FFPROBE_TIMEOUT_MS });
665
666
  report = stderr;
@@ -668,18 +669,29 @@ export class VideoProcessor extends BaseFileProcessor {
668
669
  // A non-zero exit still carries the stream report when the failure came
669
670
  // after the input was opened.
670
671
  const stderr = error.stderr;
672
+ const message = error instanceof Error ? error.message : String(error);
671
673
  if (typeof stderr !== "string") {
672
674
  return {
673
675
  success: false,
674
- error: `ffmpeg probe failed: ${error instanceof Error ? error.message : String(error)}`,
676
+ error: `ffmpeg probe failed: ${message}`,
675
677
  };
676
678
  }
677
679
  report = stderr;
680
+ exitFailure = message;
678
681
  }
679
682
  const data = this.parseFfmpegInputReport(report);
680
- return data
681
- ? { success: true, data }
682
- : { success: false, error: "ffmpeg reported no duration" };
683
+ if (data) {
684
+ return { success: true, data };
685
+ }
686
+ // Keep ffmpeg's own words when it failed as well as found no duration:
687
+ // a file it cannot read at all is the common case, and the reason is the
688
+ // only thing the warning that follows can tell the reader.
689
+ return {
690
+ success: false,
691
+ error: exitFailure
692
+ ? `ffmpeg reported no duration: ${exitFailure}`
693
+ : "ffmpeg reported no duration",
694
+ };
683
695
  }
684
696
  /**
685
697
  * Parse the input section of an ffmpeg banner: `Duration:`, `bitrate:` and
@@ -52,6 +52,15 @@ export declare function buildNativeToolDeclarations(tools: Record<string, Tool>,
52
52
  * duplicate side effects the moment the model repeats itself.
53
53
  */
54
54
  export declare function buildDedupedEngineTools(declarations: NativeToolDeclarationsResult | undefined, tools: Record<string, Tool> | undefined, guards?: ToolExecutionGuards): NonNullable<AgenticLoopOptions["tools"]>;
55
+ /**
56
+ * Mid-turn tool sync for the native Gemini loops that build their snapshot
57
+ * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
58
+ * discovered tools into the live record between steps; without this refresh
59
+ * they stay invisible to the rest of the turn and every call dies as
60
+ * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
61
+ * `toolsConfig` by reference — and returns the original names of the tools it
62
+ * added (empty when nothing was added).
63
+ */
55
64
  export declare function refreshNativeToolDeclarations(liveTools: Record<string, Tool> | undefined, current: NativeToolDeclarationsResult): string[];
56
65
  /**
57
66
  * Build the native @google/genai config object shared by stream and generate.
@@ -381,6 +381,15 @@ export function buildDedupedEngineTools(declarations, tools, guards) {
381
381
  }
382
382
  return engineTools;
383
383
  }
384
+ /**
385
+ * Mid-turn tool sync for the native Gemini loops that build their snapshot
386
+ * via buildNativeToolDeclarations. `search_tools` (tools.discovery) hydrates
387
+ * discovered tools into the live record between steps; without this refresh
388
+ * they stay invisible to the rest of the turn and every call dies as
389
+ * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
390
+ * `toolsConfig` by reference — and returns the original names of the tools it
391
+ * added (empty when nothing was added).
392
+ */
384
393
  export function refreshNativeToolDeclarations(liveTools, current) {
385
394
  if (!liveTools) {
386
395
  return [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.47.0",
3
+ "version": "12.47.1",
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": {