@juspay/neurolink 11.15.9 → 11.16.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.
@@ -16,7 +16,8 @@
16
16
  * and original -> sanitized when writing `functionResponse` parts. Neither the
17
17
  * engine nor any other adapter needs to know sanitization happened.
18
18
  */
19
- import { collectStreamChunksIncremental, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
19
+ import { resolveLiveTool } from "../tools/toolDiscovery.js";
20
+ import { collectStreamChunksIncremental, guardToolExecutor, extractTextFromParts, mapGeminiFinishReason, pushModelResponseToHistory, refreshNativeToolDeclarations, } from "../providers/googleNativeGemini3/utils.js";
20
21
  export function createGeminiLoopAdapter(config) {
21
22
  /**
22
23
  * Sanitized wire name -> the name the caller registered. Rebuilt per step
@@ -87,17 +88,34 @@ export function createGeminiLoopAdapter(config) {
87
88
  }
88
89
  : {}),
89
90
  resolveToolOnMiss: (name) => {
90
- const tool = config.liveTools?.[name];
91
+ // resolveLiveTool, NOT a plain record lookup. A deferred-catalog tool is
92
+ // not a key on the record at all — it lives behind a symbol-keyed
93
+ // resolver — so `liveTools[name]` returns undefined for exactly the
94
+ // tools this hook exists to hydrate, and the model gets TOOL_NOT_FOUND
95
+ // for a tool it was told about.
96
+ const tool = resolveLiveTool(config.liveTools, name);
91
97
  const execute = tool?.execute;
92
98
  if (!execute) {
93
99
  return undefined;
94
100
  }
95
- // Wrapped because the hook types `opts` as `unknown`, and a function
96
- // declaring a narrower options type is not assignable to one accepting
97
- // `unknown`. One assertion at the boundary, never a double assertion.
98
- return {
99
- execute: async (args, opts) => execute(args, opts),
100
- };
101
+ // Registered into the turn's DedupExecuteMap and taken back through
102
+ // `.get()`, so a hydrated tool gets the same per-turn result cache as a
103
+ // declared one. A tool the model just discovered is the one most likely
104
+ // to be re-requested with identical arguments.
105
+ const executeMap = config.declarations?.executeMap;
106
+ let resolved = execute;
107
+ if (executeMap) {
108
+ executeMap.set(name, execute);
109
+ resolved = executeMap.get(name) ?? execute;
110
+ }
111
+ const guarded = config.toolGuards
112
+ ? guardToolExecutor(name, resolved, config.toolGuards)
113
+ : // Wrapped because the hook types `opts` as `unknown`, and a function
114
+ // declaring a narrower options type is not assignable to one
115
+ // accepting `unknown`. One assertion at the boundary, never a double
116
+ // assertion.
117
+ async (args, opts) => resolved(args, opts);
118
+ return { execute: guarded };
101
119
  },
102
120
  async executeStep(request, channel, signal) {
103
121
  const rawStream = await config.sendStep(request.raw, signal);
@@ -87,9 +87,19 @@ async function dispatchStepTools(params) {
87
87
  // a deferred-catalog placeholder is exactly that shape, and it is
88
88
  // precisely what hydration exists to resolve.
89
89
  const declaredTool = tools?.[call.name];
90
- const tool = declaredTool?.execute
91
- ? declaredTool
92
- : (adapter.resolveToolOnMiss?.(call.name) ?? declaredTool);
90
+ let tool = declaredTool;
91
+ if (!declaredTool?.execute) {
92
+ const hydrated = adapter.resolveToolOnMiss?.(call.name);
93
+ if (hydrated) {
94
+ tool = hydrated;
95
+ // It resolves NOW, so any strikes standing against this name were
96
+ // recorded while it genuinely did not resolve — snapshot artifacts of
97
+ // a deferred catalog, not failures of a tool that exists. Leaving them
98
+ // in place would disable the tool at the exact moment it became
99
+ // usable.
100
+ failedTools.delete(call.name);
101
+ }
102
+ }
93
103
  if (!tool?.execute) {
94
104
  const output = breaker
95
105
  ? {
@@ -91,6 +91,22 @@ export declare function buildNativeToolDeclarations(tools: Record<string, Tool>,
91
91
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
92
92
  * `toolsConfig` by reference — and returns true when anything was added.
93
93
  */
94
+ /**
95
+ * Everything a native Gemini loop wraps around a tool call that the shared
96
+ * engine does not do itself.
97
+ *
98
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
99
+ * abort is observed the moment it fires rather than after the tool settles,
100
+ * and the timeout still bounds a tool that neither settles nor honours its
101
+ * signal. The progress pings bracket the await because the stall watchdog is
102
+ * a whole-turn interval comparing wall-clock against the last progress mark —
103
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
104
+ *
105
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
106
+ * one declared up front; keeping this inline made the discovered tool the one
107
+ * executor in the system that ran raw.
108
+ */
109
+ export declare function guardToolExecutor(name: string, execute: NonNullable<Tool["execute"]>, guards: GeminiToolExecutionGuards): (args: Record<string, unknown>, opts: unknown) => Promise<unknown>;
94
110
  /**
95
111
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
96
112
  * DedupExecuteMap.
@@ -456,6 +456,41 @@ export function buildNativeToolDeclarations(tools, reservedNames) {
456
456
  * TOOL_NOT_FOUND. Mutates the snapshot in place — the request config holds
457
457
  * `toolsConfig` by reference — and returns true when anything was added.
458
458
  */
459
+ /**
460
+ * Everything a native Gemini loop wraps around a tool call that the shared
461
+ * engine does not do itself.
462
+ *
463
+ * Order matters. `raceWithAbort` sits INSIDE `withTimeout` so a turn-level
464
+ * abort is observed the moment it fires rather than after the tool settles,
465
+ * and the timeout still bounds a tool that neither settles nor honours its
466
+ * signal. The progress pings bracket the await because the stall watchdog is
467
+ * a whole-turn interval comparing wall-clock against the last progress mark —
468
+ * without them a legitimately slow tool reads as a stalled turn and is killed.
469
+ *
470
+ * Exported because a tool hydrated MID-TURN has to be wrapped the same way as
471
+ * one declared up front; keeping this inline made the discovered tool the one
472
+ * executor in the system that ran raw.
473
+ */
474
+ export function guardToolExecutor(name, execute, guards) {
475
+ return async (args, opts) => {
476
+ const call = () => Promise.resolve(execute(args, opts));
477
+ guards.onProgress?.();
478
+ try {
479
+ const raced = guards.abortSignal
480
+ ? raceWithAbort(call(), guards.abortSignal)
481
+ : call();
482
+ return await (guards.toolTimeoutMs === undefined
483
+ ? raced
484
+ : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
485
+ }
486
+ finally {
487
+ // In `finally`, not after a successful await: a tool that times out or
488
+ // throws has still consumed real time, and skipping the mark there would
489
+ // leave the watchdog measuring from before the call.
490
+ guards.onProgress?.();
491
+ }
492
+ };
493
+ }
459
494
  /**
460
495
  * Build the tool record handed to `runAgenticLoop`, routed through the turn's
461
496
  * DedupExecuteMap.
@@ -487,29 +522,9 @@ export function buildDedupedEngineTools(declarations, tools, guards) {
487
522
  * mark — without them a legitimately slow tool reads as a stalled turn and
488
523
  * gets killed.
489
524
  */
490
- const guard = (name, execute) => {
491
- return async (args, opts) => {
492
- const call = () => Promise.resolve(execute(args, opts));
493
- if (!guards) {
494
- return call();
495
- }
496
- guards.onProgress?.();
497
- try {
498
- const raced = guards.abortSignal
499
- ? raceWithAbort(call(), guards.abortSignal)
500
- : call();
501
- return await (guards.toolTimeoutMs === undefined
502
- ? raced
503
- : withTimeout(raced, guards.toolTimeoutMs, `Tool "${name}" execution timed out after ${guards.toolTimeoutMs}ms`));
504
- }
505
- finally {
506
- // In `finally`, not after a successful await: a tool that times out or
507
- // throws has still consumed real time, and skipping the mark there
508
- // would leave the watchdog measuring from before the call.
509
- guards.onProgress?.();
510
- }
511
- };
512
- };
525
+ const guard = (name, execute) => guards
526
+ ? guardToolExecutor(name, execute, guards)
527
+ : async (args, opts) => execute(args, opts);
513
528
  if (declarations) {
514
529
  for (const [safeName, originalName] of declarations.originalNameMap) {
515
530
  const execute = declarations.executeMap.get(safeName);
@@ -373,9 +373,19 @@ const hasGoogleCredentials = () => {
373
373
  process.env.GOOGLE_AUTH_PRIVATE_KEY));
374
374
  };
375
375
  // Create Anthropic-specific Vertex settings for native @anthropic-ai/vertex-sdk
376
- const createVertexAnthropicSettings = async (region, timeoutMs) => {
376
+ const createVertexAnthropicSettings = async (region, timeoutMs, direct, baseURL) => {
377
377
  const location = region || getVertexLocation();
378
- const project = getVertexProjectId();
378
+ // Express-style auth carries its own credentials, so the ADC-derived project
379
+ // is neither available nor needed; asking for it would throw before the
380
+ // request is ever built. It cannot be EMPTY either — the SDK rejects a
381
+ // falsy projectId outright ("No projectId was given and it could not be
382
+ // resolved from credentials") — so a configured project is used when there
383
+ // is one and a placeholder stands in otherwise. The value only ever appears
384
+ // in the request path, which an endpoint reached this way is expected to
385
+ // route on its own.
386
+ const project = direct
387
+ ? direct.projectId?.trim() || "express"
388
+ : getVertexProjectId();
379
389
  return {
380
390
  projectId: project,
381
391
  region: location,
@@ -384,6 +394,26 @@ const createVertexAnthropicSettings = async (region, timeoutMs) => {
384
394
  // bound (a 429 with retry-after: 8549 sleeps 2.4h per retry, invisible
385
395
  // to fallback orchestration). Retries are the orchestrator's job.
386
396
  maxRetries: 0,
397
+ // Outside the express branch too: an endpoint override is about WHERE the
398
+ // request goes, not how it is authenticated, so a caller using ADC against
399
+ // a gateway needs it just as much.
400
+ ...(baseURL ? { baseURL } : {}),
401
+ ...(direct
402
+ ? {
403
+ // The token goes on the request directly. `accessToken` on the SDK's
404
+ // own options looks like it should do this and does not — the client
405
+ // stores it and never reads it for auth, so prepareOptions() still
406
+ // awaits Application Default Credentials and the call fails with a
407
+ // credentials error that names nothing useful. `authClient` is the
408
+ // option the SDK actually consults.
409
+ authClient: {
410
+ getRequestHeaders: async () => ({
411
+ Authorization: `Bearer ${direct.apiKey}`,
412
+ }),
413
+ projectId: null,
414
+ },
415
+ }
416
+ : {}),
387
417
  };
388
418
  };
389
419
  // Helper function to determine if a model is an Anthropic model
@@ -1583,6 +1613,13 @@ export class GoogleVertexProvider extends BaseProvider {
1583
1613
  classifyResultFailure: (output) => extractToolFailureText(output) ?? undefined,
1584
1614
  },
1585
1615
  liveTools: options.tools ?? {},
1616
+ // A tool hydrated mid-turn gets the SAME bound, abort race and stall
1617
+ // ping as one declared up front — see guardToolExecutor.
1618
+ toolGuards: {
1619
+ toolTimeoutMs: toolExecTimeoutMs,
1620
+ abortSignal: effectiveSignal,
1621
+ onProgress: () => turnClock.noteProgress(),
1622
+ },
1586
1623
  ...(declarations ? { declarations } : {}),
1587
1624
  ...(useFinalResultTool
1588
1625
  ? {
@@ -2371,6 +2408,13 @@ export class GoogleVertexProvider extends BaseProvider {
2371
2408
  classifyResultFailure: (output) => extractToolFailureText(output) ?? undefined,
2372
2409
  },
2373
2410
  liveTools: options.tools ?? {},
2411
+ // A tool hydrated mid-turn gets the SAME bound, abort race and stall
2412
+ // ping as one declared up front — see guardToolExecutor.
2413
+ toolGuards: {
2414
+ toolTimeoutMs: toolExecTimeoutMs,
2415
+ abortSignal: effectiveSignal,
2416
+ onProgress: () => turnClock.noteProgress(),
2417
+ },
2374
2418
  ...(declarations ? { declarations } : {}),
2375
2419
  ...(useFinalResultTool
2376
2420
  ? {
@@ -2844,7 +2888,20 @@ export class GoogleVertexProvider extends BaseProvider {
2844
2888
  */
2845
2889
  async createAnthropicVertexClient(timeoutMs) {
2846
2890
  const mod = await getAnthropicVertexModule();
2847
- const settings = await createVertexAnthropicSettings(this.location, timeoutMs);
2891
+ const expressApiKey = this.resolveExpressApiKey();
2892
+ const directBaseURL = this.baseURL?.trim() || process.env.GOOGLE_VERTEX_BASE_URL?.trim();
2893
+ const settings = await createVertexAnthropicSettings(this.location, timeoutMs, expressApiKey
2894
+ ? {
2895
+ apiKey: expressApiKey,
2896
+ ...(this.projectId ? { projectId: this.projectId } : {}),
2897
+ }
2898
+ : undefined, directBaseURL);
2899
+ // One assertion, at the one place the shapes genuinely differ. The SDK
2900
+ // declares `authClient` as its full `AuthClient` (24-plus members) while
2901
+ // `prepareOptions()` only ever calls `getRequestHeaders()` and reads
2902
+ // `projectId`. Naming the narrow surface in our own type and widening it
2903
+ // here is the honest version of that gap; the alternative is standing up a
2904
+ // real AuthClient to satisfy a contract the SDK does not exercise.
2848
2905
  const client = new mod.AnthropicVertex(settings);
2849
2906
  // The vertex SDK eagerly starts Google ADC resolution in its constructor
2850
2907
  // (`this._authClientPromise = this._auth.getClient()`) and only awaits it
@@ -326,6 +326,16 @@ export type GeminiLoopAdapterCoreConfig = {
326
326
  * step with that step's real token counts.
327
327
  */
328
328
  noteUsage?: (inputTokens: number, outputTokens: number) => void;
329
+ /**
330
+ * The same guards `buildDedupedEngineTools` wraps declared tools in, applied
331
+ * to one hydrated mid-turn.
332
+ *
333
+ * Without this a tool discovered during a turn is the ONE executor that runs
334
+ * raw: no per-turn dedup, no execution timeout, no stall-clock ping. That is
335
+ * the opposite of what discovery is for — the tool the model just found is
336
+ * the one most likely to be called repeatedly with the same arguments.
337
+ */
338
+ toolGuards?: GeminiToolExecutionGuards;
329
339
  /**
330
340
  * Name of the terminal structured-output tool when one is in play. A call
331
341
  * to it ends the turn: its arguments ARE the answer, so it is reported as
@@ -1052,6 +1052,18 @@ export type GoogleVertexProviderSettings = {
1052
1052
  * Anthropic Vertex AI settings for Claude models on Vertex
1053
1053
  * Used with @anthropic-ai/vertex-sdk
1054
1054
  */
1055
+ /**
1056
+ * The two members `@anthropic-ai/vertex-sdk` actually uses off an auth client.
1057
+ *
1058
+ * Its declared `AuthClient` is far wider, but `prepareOptions()` only ever
1059
+ * awaits `getRequestHeaders()` and reads `projectId` (client.js:109-111).
1060
+ * Naming that narrow surface is what lets a caller supply a token directly
1061
+ * instead of standing up Application Default Credentials.
1062
+ */
1063
+ export type VertexAnthropicAuthClient = {
1064
+ getRequestHeaders: () => Promise<Record<string, string>>;
1065
+ projectId?: string | null;
1066
+ };
1055
1067
  export type AnthropicVertexSettings = {
1056
1068
  /** Google Cloud project ID */
1057
1069
  projectId: string;
@@ -1061,6 +1073,22 @@ export type AnthropicVertexSettings = {
1061
1073
  timeout?: number;
1062
1074
  /** SDK-internal retry budget (transport retries are the orchestrator's job) */
1063
1075
  maxRetries?: number;
1076
+ /**
1077
+ * Endpoint override. The SDK derives
1078
+ * `https://${region}-aiplatform.googleapis.com/v1` by default; a gateway or
1079
+ * a compatible endpoint is reached by setting this instead.
1080
+ */
1081
+ baseURL?: string;
1082
+ /**
1083
+ * Supply the request credentials directly, bypassing Application Default
1084
+ * Credentials.
1085
+ *
1086
+ * Note that `accessToken` on the SDK's own options does NOT do this: the
1087
+ * client stores it and never reads it for auth, so `prepareOptions()` still
1088
+ * awaits ADC and a token-only caller fails with a credentials error that
1089
+ * names nothing useful. `authClient` is the option that actually works.
1090
+ */
1091
+ authClient?: VertexAnthropicAuthClient;
1064
1092
  };
1065
1093
  /**
1066
1094
  * OpenAI-compatible models endpoint response structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "11.15.9",
3
+ "version": "11.16.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {
@@ -22,9 +22,8 @@
22
22
  "url": "https://github.com/sponsors/juspay"
23
23
  },
24
24
  "engines": {
25
- "node": ">=20.19.0",
26
- "npm": ">=10.0.0",
27
- "pnpm": ">=8.0.0"
25
+ "node": ">=22.0.0",
26
+ "pnpm": ">=10.0.0"
28
27
  },
29
28
  "scripts": {
30
29
  "dev": "vite dev",
@@ -211,7 +210,8 @@
211
210
  "test:sagemaker-streaming": "tsx test/continuous-test-suite-sagemaker-streaming.ts",
212
211
  "test:anthropic-loop-characterization": "tsx test/continuous-test-suite-anthropic-loop-characterization.ts",
213
212
  "test:aistudio-loop-characterization": "tsx test/continuous-test-suite-aistudio-loop-characterization.ts",
214
- "test:docs-mcp": "npx tsx test/continuous-test-suite-docs-mcp.ts"
213
+ "test:docs-mcp": "npx tsx test/continuous-test-suite-docs-mcp.ts",
214
+ "test:vertex-claude-characterization": "tsx test/continuous-test-suite-vertex-claude-characterization.ts"
215
215
  },
216
216
  "files": [
217
217
  "dist",
@@ -443,7 +443,7 @@
443
443
  "koa": "^3.1.1",
444
444
  "koa-bodyparser": "^4.4.1",
445
445
  "livekit-server-sdk": "^2.15.4",
446
- "mammoth": "^1.11.0",
446
+ "mammoth": "1.12.0",
447
447
  "mediabunny": "^1.40.1",
448
448
  "music-metadata": "^11.11.2",
449
449
  "pdf-parse": "^2.4.5",
@@ -459,9 +459,7 @@
459
459
  "@changesets/cli": "^2.29.8",
460
460
  "@electric-sql/pglite": "^0.4.6",
461
461
  "@eslint/js": "^10.0.1",
462
- "@juspay/hippocampus": ">=0.1.7",
463
462
  "@opentelemetry/api": "^1.9.0",
464
- "@opentelemetry/sdk-trace-base": "^2.6.0",
465
463
  "@opentelemetry/sdk-trace-node": "^2.6.0",
466
464
  "@semantic-release/changelog": "^6.0.3",
467
465
  "@semantic-release/commit-analyzer": "^13.0.1",
@@ -480,7 +478,7 @@
480
478
  "@types/koa": "^3.0.1",
481
479
  "@types/koa-bodyparser": "^4.3.13",
482
480
  "@types/koa__cors": "^5.0.1",
483
- "@types/node": "^25.3.3",
481
+ "@types/node": "^22.20.0",
484
482
  "@types/react": "^19.2.10",
485
483
  "@types/tar-stream": "^3.1.4",
486
484
  "@types/ws": "^8.18.1",
@@ -570,7 +568,6 @@
570
568
  "esbuild",
571
569
  "protobufjs",
572
570
  "puppeteer",
573
- "sqlite3",
574
571
  "ffmpeg-static",
575
572
  "sharp"
576
573
  ],