@juspay/neurolink 12.49.19 → 12.49.20

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.
@@ -28,6 +28,11 @@ import type { NativeGenerateLoopArgs, NativeGenerateLoopResult, StepResult, Step
28
28
  * how a caller forces a tool on step 3 only. A hook that returns no
29
29
  * `toolChoice` defers to the rule. Nothing else on the hook's result is read.
30
30
  *
31
+ * A forced single-tool choice, from the hook or from the turn, that names a
32
+ * tool outside `declaredTools` is warned about and not sent. A hook's choice
33
+ * is then treated as "no override" and the rule decides; the turn's own
34
+ * choice becomes `"auto"`. Without `declaredTools` nothing is checked.
35
+ *
31
36
  * A hook that THROWS is logged and treated as "no override": the steps
32
37
  * already billed are kept and the rule decides. An abort of the turn while
33
38
  * the hook is pending is not a hook failure and is re-thrown. The two are
@@ -32,6 +32,60 @@ const isForcedToolChoice = (choice) => {
32
32
  }
33
33
  return false;
34
34
  };
35
+ /**
36
+ * The tool a choice forces by name, when it forces one. Both wire mappers
37
+ * honour exactly `{ type: "tool", toolName }` with a non-empty name, so
38
+ * that is the only shape that names a function on the wire.
39
+ */
40
+ const forcedToolName = (choice) => {
41
+ if (typeof choice !== "object" || choice === null) {
42
+ return undefined;
43
+ }
44
+ const { type, toolName } = choice;
45
+ return type === "tool" && typeof toolName === "string" && toolName !== ""
46
+ ? toolName
47
+ : undefined;
48
+ };
49
+ /**
50
+ * The name a choice forces when `declaredTools` is known and lacks it, else
51
+ * undefined. Own keys only: the live record may have a null prototype or be
52
+ * keyed by a name such as "constructor".
53
+ */
54
+ const undeclaredForcedToolName = (choice, declaredTools) => {
55
+ if (!declaredTools) {
56
+ return undefined;
57
+ }
58
+ const name = forcedToolName(choice);
59
+ return name !== undefined && !Object.hasOwn(declaredTools, name)
60
+ ? name
61
+ : undefined;
62
+ };
63
+ /**
64
+ * A forced choice for a tool the request does not declare is dropped for the
65
+ * step rather than sent, so a stale hook or a platform-gated tool degrades
66
+ * the turn to the model's own choice instead of failing it with a 400.
67
+ */
68
+ const warnUndeclaredForcedTool = (toolName, step, source) => {
69
+ logger.warn(source === "prepareStep"
70
+ ? `prepareStep forced tool "${toolName}" at step ${step}, but the request does not declare it; ignoring the override for this step`
71
+ : `toolChoice forces tool "${toolName}" at step ${step}, but the request does not declare it; using "auto" for this step`, { toolName, step, source });
72
+ };
73
+ /**
74
+ * The names this request actually carries, in the record shape
75
+ * `declaredTools` takes. The loop sends `args.tools`, built once by the
76
+ * caller; `args.toolsRecord` can hold more (discovery hydrates into it, and
77
+ * this loop never re-declares), so only `tools` answers "is it offered".
78
+ * Null prototype: a tool may be named `__proto__` or `constructor`.
79
+ */
80
+ const declaredToolRecord = (tools) => {
81
+ const declared = Object.create(null);
82
+ for (const tool of tools ?? []) {
83
+ if (typeof tool.name === "string") {
84
+ declared[tool.name] = true;
85
+ }
86
+ }
87
+ return declared;
88
+ };
35
89
  /**
36
90
  * `toolChoiceSteps` is a count of leading steps, so anything but a
37
91
  * non-negative integer is meaningless. Fall back to the default with a WARN
@@ -127,6 +181,11 @@ const abortReason = (signal) => {
127
181
  * how a caller forces a tool on step 3 only. A hook that returns no
128
182
  * `toolChoice` defers to the rule. Nothing else on the hook's result is read.
129
183
  *
184
+ * A forced single-tool choice, from the hook or from the turn, that names a
185
+ * tool outside `declaredTools` is warned about and not sent. A hook's choice
186
+ * is then treated as "no override" and the rule decides; the turn's own
187
+ * choice becomes `"auto"`. Without `declaredTools` nothing is checked.
188
+ *
130
189
  * A hook that THROWS is logged and treated as "no override": the steps
131
190
  * already billed are kept and the rule decides. An abort of the turn while
132
191
  * the hook is pending is not a hook failure and is re-thrown. The two are
@@ -156,16 +215,27 @@ export async function resolveStepToolChoice(input) {
156
215
  }
157
216
  warnIgnoredPrepareStepFields(prepared);
158
217
  if (prepared?.toolChoice !== undefined) {
159
- return prepared.toolChoice;
218
+ const undeclaredPrepared = undeclaredForcedToolName(prepared.toolChoice, input.declaredTools);
219
+ if (undeclaredPrepared === undefined) {
220
+ return prepared.toolChoice;
221
+ }
222
+ warnUndeclaredForcedTool(undeclaredPrepared, input.step, "prepareStep");
160
223
  }
161
224
  }
162
225
  if (input.base === undefined) {
163
226
  return undefined;
164
227
  }
165
- if (!isForcedToolChoice(input.base) ||
166
- input.step < normalizeToolChoiceSteps(input.toolChoiceSteps)) {
228
+ if (!isForcedToolChoice(input.base)) {
229
+ return input.base;
230
+ }
231
+ if (input.step >= normalizeToolChoiceSteps(input.toolChoiceSteps)) {
232
+ return "auto";
233
+ }
234
+ const undeclaredBase = undeclaredForcedToolName(input.base, input.declaredTools);
235
+ if (undeclaredBase === undefined) {
167
236
  return input.base;
168
237
  }
238
+ warnUndeclaredForcedTool(undeclaredBase, input.step, "base");
169
239
  return "auto";
170
240
  }
171
241
  /**
@@ -356,6 +426,7 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
356
426
  steps: stepRecords,
357
427
  maxSteps: args.maxSteps,
358
428
  model: args.modelId,
429
+ declaredTools: declaredToolRecord(args.tools),
359
430
  ...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
360
431
  })
361
432
  : undefined;
@@ -2008,6 +2008,7 @@ export class AnthropicProvider extends BaseProvider {
2008
2008
  steps: stepRecords,
2009
2009
  maxSteps,
2010
2010
  model: modelId,
2011
+ declaredTools: toolsRecord,
2011
2012
  ...(abortSignal ? { abortSignal } : {}),
2012
2013
  })))
2013
2014
  : undefined;
@@ -1741,6 +1741,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
1741
1741
  steps,
1742
1742
  maxSteps,
1743
1743
  model: modelId,
1744
+ declaredTools: toolsRecord,
1744
1745
  ...(abortSignal ? { abortSignal } : {}),
1745
1746
  }), wireNameMaps?.toWire)
1746
1747
  : undefined,
@@ -584,6 +584,10 @@ export type GenerateOptions = {
584
584
  * compel a tool call on every step, and the loop would only end when
585
585
  * `maxSteps` ran out. `"auto"` and `"none"` are applied unchanged on every
586
586
  * step. Vertex, Google AI Studio and Bedrock do not honour `toolChoice`.
587
+ *
588
+ * A named tool that the request does not declare is not sent, because the
589
+ * provider would reject the request: a warning is logged and the step uses
590
+ * `"auto"`.
587
591
  */
588
592
  toolChoice?: ToolChoice<Record<string, Tool>>;
589
593
  /**
@@ -606,7 +610,9 @@ export type GenerateOptions = {
606
610
  * Optional callback that runs before each step in a multi-step generation.
607
611
  *
608
612
  * Honoured field of the result: `toolChoice`, applied to that step only
609
- * and taking precedence over `toolChoice` / `toolChoiceSteps`.
613
+ * and taking precedence over `toolChoice` / `toolChoiceSteps`. A named tool
614
+ * the request does not declare is ignored with a warning, as if the hook had
615
+ * returned no `toolChoice`.
610
616
  *
611
617
  * Not honoured — accepted for source compatibility with the former Vercel
612
618
  * AI SDK `experimental_prepareStep` shape, but ignored by every native
@@ -1799,6 +1805,17 @@ export type StepToolChoiceInput = {
1799
1805
  model: string;
1800
1806
  /** The turn's abort signal; a pending `prepareStep` is released when it fires. */
1801
1807
  abortSignal?: AbortSignal;
1808
+ /**
1809
+ * The tools this request declares, keyed by SDK-side tool name. When given,
1810
+ * a forced single-tool choice naming a tool outside it is not sent: a
1811
+ * provider answers a request that forces a function its own tools list
1812
+ * lacks with a 400. A loop that re-declares hydrated tools in the same
1813
+ * step (the streaming loops) passes the live record, because discovery
1814
+ * hydrates tools into it between steps; a loop that sends a fixed tools
1815
+ * array (the shared generate loop) passes the names of that array.
1816
+ * Absent means the choice is not checked.
1817
+ */
1818
+ declaredTools?: Readonly<Record<string, unknown>>;
1802
1819
  };
1803
1820
  /**
1804
1821
  * Inputs to the shared native generate loop (`core/nativeGenerateLoop.ts`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.49.19",
3
+ "version": "12.49.20",
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": {
@@ -112,6 +112,7 @@
112
112
  "test:agent:cost": "pnpm exec tsx test/continuous-test-suite-agent-cost.ts",
113
113
  "test:adjust-body-after-400": "pnpm exec tsx test/continuous-test-suite-adjust-body-after-400.ts",
114
114
  "test:zod3-schema-native-path": "pnpm exec tsx test/continuous-test-suite-zod3-schema-native-path.ts",
115
+ "test:forced-tool-declared": "pnpm exec tsx test/continuous-test-suite-forced-tool-declared.ts",
115
116
  "test:error-classification-e2e": "pnpm exec tsx test/continuous-test-suite-error-classification-e2e.ts",
116
117
  "test:error-classifier-contract": "pnpm exec tsx test/continuous-test-suite-error-classifier-contract.ts",
117
118
  "test:bedrock-inference-profile": "pnpm exec tsx test/continuous-test-suite-bedrock-inference-profile.ts",