@juspay/neurolink 12.14.15 → 12.14.17

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.
@@ -31,7 +31,7 @@ import { attachStreamCancel, cancelStream, releaseIterator, } from "../utils/str
31
31
  import { TimeoutError as AsyncTimeoutError, withTimeoutFn, } from "../utils/async/withTimeout.js";
32
32
  import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
33
33
  import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
34
- import { getKeyCount, getKeysAsString } from "../utils/transformationUtils.js";
34
+ import { getKeyCount, getKeysAsString, transformToolExecutions, } from "../utils/transformationUtils.js";
35
35
  import { ToolExecutionRecorder } from "./toolExecutionRecorder.js";
36
36
  import { TTS_ERROR_CODES, TTSProcessor } from "../utils/ttsProcessor.js";
37
37
  import { executeVideoAnalysis, hasVideoFrames, } from "../utils/videoAnalysisProcessor.js";
@@ -48,6 +48,7 @@ import { Utilities } from "./modules/Utilities.js";
48
48
  import { generateOnceNative } from "../utils/nativeSingleShot.js";
49
49
  import { validateExecutionControl } from "../utils/parameterValidation.js";
50
50
  import { extractTokenUsage } from "../utils/tokenUtils.js";
51
+ import { preserveLiveStreamAccessors } from "../utils/streamResultAccessors.js";
51
52
  /**
52
53
  * Read the consumer-facing lifecycle callbacks buried inside a request's
53
54
  * middleware blob. The parameter is `unknown` on purpose: request options
@@ -523,10 +524,14 @@ export class BaseProvider {
523
524
  cancelStream(activeStream);
524
525
  releaseIterator(activeIterator);
525
526
  });
526
- return {
527
+ // A naked spread reads (and thereby snapshots) every enumerable getter
528
+ // on `result` — including a provider's lazily-populated `toolsUsed` /
529
+ // `toolExecutions` — before the consumer has pulled a single chunk.
530
+ // Re-applying the original accessor descriptors keeps those fields live.
531
+ return preserveLiveStreamAccessors(result, {
527
532
  ...result,
528
533
  stream: wrapped,
529
- };
534
+ });
530
535
  }
531
536
  /**
532
537
  * The eager half of the retired-model stream fallback, for providers whose
@@ -688,7 +693,12 @@ export class BaseProvider {
688
693
  cancelStream(originalStream);
689
694
  releaseIterator(upstreamIterator);
690
695
  });
691
- return { ...result, stream: wrappedStream };
696
+ // See the comment in withStreamModelFallback above: this spread must not
697
+ // be allowed to freeze a provider's lazy toolsUsed/toolExecutions getters.
698
+ return preserveLiveStreamAccessors(result, {
699
+ ...result,
700
+ stream: wrappedStream,
701
+ });
692
702
  }
693
703
  /**
694
704
  * Fire the consumer-supplied onError callback before throwing. Used in
@@ -891,7 +901,19 @@ export class BaseProvider {
891
901
  }
892
902
  : undefined;
893
903
  // Create a synthetic stream from the generate result that simulates progressive delivery
894
- return {
904
+ //
905
+ // toolsUsed/toolExecutions: generate()'s result carries both once the
906
+ // tool round trip settles, but a plain field list (like every other
907
+ // field above) would silently drop them from the fake-streaming
908
+ // StreamResult. toolsUsed overlaps generate()'s and StreamResult's
909
+ // declared types exactly, so it is assigned directly; toolExecutions
910
+ // does not (GenerateResult carries ToolExecutionRecord[], StreamResult
911
+ // declares ToolExecutionSummary[] but every other live producer of this
912
+ // field actually returns transformToolExecutions()'s
913
+ // {name,input,output,duration} shape) — attached via defineProperty,
914
+ // the same bypass `createMCPStream()` uses, rather than a literal that
915
+ // would fail the structural check or need a banned double-cast.
916
+ return Object.defineProperty({
895
917
  stream: this.createFakeStreamingOutput(result, options, onTTSComplete),
896
918
  usage: result?.usage,
897
919
  provider: result?.provider,
@@ -916,7 +938,13 @@ export class BaseProvider {
916
938
  evaluation: result?.evaluation,
917
939
  audio,
918
940
  ttsMetadata,
919
- };
941
+ toolsUsed: result?.toolsUsed,
942
+ }, "toolExecutions", {
943
+ value: transformToolExecutions(result?.toolExecutions),
944
+ enumerable: true,
945
+ configurable: true,
946
+ writable: true,
947
+ });
920
948
  }
921
949
  catch (error) {
922
950
  logger.error(`Fake streaming fallback failed for ${this.providerName}:`, error);
@@ -11,8 +11,12 @@ export const vertexManifest = {
11
11
  aliases: [],
12
12
  contextWindow: 1048576,
13
13
  maxOutputTokens: 64000,
14
- vision: false,
15
- functionCalling: false,
14
+ // Measured against live Vertex, every served model in this project —
15
+ // gemini-2.5-flash / -pro / -flash-lite, claude-sonnet-4-6 and
16
+ // claude-sonnet-4-5@20250929 — accepted image and PDF input and
17
+ // returned tool calls. Declaring false here contradicted the wire.
18
+ vision: true,
19
+ functionCalling: true,
16
20
  },
17
21
  },
18
22
  };
package/dist/neurolink.js CHANGED
@@ -82,6 +82,7 @@ import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecy
82
82
  import { interleaveTTSStream } from "./utils/ttsStream.js";
83
83
  import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
84
84
  import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
85
+ import { pickLiveStreamDescriptors, preserveLiveStreamAccessors, } from "./utils/streamResultAccessors.js";
85
86
  import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "./utils/json/coerce.js";
86
87
  // Factory processing imports
87
88
  import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
@@ -6873,10 +6874,17 @@ Current user's request: ${currentInput}`;
6873
6874
  // the inner loop. We only get here if the loop didn't return.
6874
6875
  throw new Error(`[NeuroLink.stream] iteration fallback exhausted ${maxAttempts} attempts`);
6875
6876
  })();
6876
- return {
6877
+ // `result`'s toolsUsed/toolExecutions may be live getters (openai-compat
6878
+ // providers back them with a getter over an array still being populated
6879
+ // by a background loop) — a naked spread here would read them once, now,
6880
+ // and freeze whatever they held at that instant. preserveLiveStreamAccessors
6881
+ // re-applies the original getter descriptors onto the rebuilt object, the
6882
+ // same fix already applied to withStreamModelFallback/
6883
+ // wrapStreamWithLifecycleCallbacks in baseProvider.ts.
6884
+ return preserveLiveStreamAccessors(result, {
6877
6885
  ...result,
6878
6886
  stream: wrappedStream,
6879
- };
6887
+ });
6880
6888
  }
6881
6889
  async executeStreamRequest(options) {
6882
6890
  // Dynamic argument resolution — resolve any function-valued options before downstream use
@@ -7567,7 +7575,12 @@ Current user's request: ${currentInput}`;
7567
7575
  factoryResult,
7568
7576
  sessionId: enhancedOptions.context?.sessionId,
7569
7577
  });
7570
- const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, metadata: providerStreamMetadata, } = await this.createMCPStream(enhancedOptions);
7578
+ // Kept whole (not just destructured) so its toolsUsed/toolExecutions
7579
+ // descriptors can be re-applied onto the final StreamResult near the
7580
+ // bottom of this method, without ever reading — and thereby freezing —
7581
+ // a getter-based provider's value here.
7582
+ const mcpStreamOutcome = await this.createMCPStream(enhancedOptions);
7583
+ const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, metadata: providerStreamMetadata, } = mcpStreamOutcome;
7571
7584
  let streamedTTSResult;
7572
7585
  const { stream: incrementalStream, ttsMetadata: streamTtsMetadata } = await this.createIncrementalTTSStream({
7573
7586
  stream: mcpStream,
@@ -7934,6 +7947,7 @@ Current user's request: ${currentInput}`;
7934
7947
  error: metadata.error,
7935
7948
  events: eventSequence,
7936
7949
  providerMetadata: providerStreamMetadata,
7950
+ toolTelemetrySource: mcpStreamOutcome,
7937
7951
  });
7938
7952
  }
7939
7953
  catch (error) {
@@ -8731,7 +8745,11 @@ Current user's request: ${currentInput}`;
8731
8745
  throw streamConsumeErr;
8732
8746
  }
8733
8747
  })();
8734
- return {
8748
+ // toolsUsed/toolExecutions are attached via defineProperties (not
8749
+ // listed as plain fields above) so a getter-based provider result
8750
+ // keeps resolving lazily through this boundary instead of being
8751
+ // read — and frozen — once, right here, before any chunk is drained.
8752
+ return Object.defineProperties({
8735
8753
  stream: wrappedStream,
8736
8754
  provider: poolStreamProviderName,
8737
8755
  usage: poolStreamResult.usage,
@@ -8741,7 +8759,10 @@ Current user's request: ${currentInput}`;
8741
8759
  toolResults: poolStreamResult.toolResults ?? [],
8742
8760
  analytics: poolStreamResult.analytics,
8743
8761
  metadata: poolStreamResult.metadata,
8744
- };
8762
+ }, pickLiveStreamDescriptors(poolStreamResult, [
8763
+ "toolsUsed",
8764
+ "toolExecutions",
8765
+ ]));
8745
8766
  }
8746
8767
  catch (poolStreamError) {
8747
8768
  if (isAbortError(poolStreamError)) {
@@ -8785,7 +8806,11 @@ Current user's request: ${currentInput}`;
8785
8806
  provider: providerName,
8786
8807
  systemPromptPassedLength: enhancedSystemPrompt.length,
8787
8808
  });
8788
- return {
8809
+ // toolsUsed/toolExecutions are attached via defineProperties, not listed
8810
+ // as plain fields: see the identical comment on the ModelPool branch
8811
+ // above — a plain `toolsUsed: streamResult.toolsUsed` here would invoke
8812
+ // and freeze a getter-based provider's value before any chunk is drained.
8813
+ return Object.defineProperties({
8789
8814
  stream: streamResult.stream,
8790
8815
  provider: providerName,
8791
8816
  usage: streamResult.usage,
@@ -8800,7 +8825,7 @@ Current user's request: ${currentInput}`;
8800
8825
  // the loop finishes — snapshotting fields here would freeze them as
8801
8826
  // undefined before the stream is drained.
8802
8827
  metadata: streamResult.metadata,
8803
- };
8828
+ }, pickLiveStreamDescriptors(streamResult, ["toolsUsed", "toolExecutions"]));
8804
8829
  }
8805
8830
  /**
8806
8831
  * Process stream result
@@ -8831,7 +8856,7 @@ Current user's request: ${currentInput}`;
8831
8856
  * Create stream response
8832
8857
  */
8833
8858
  createStreamResponse(streamResult, stream, config) {
8834
- return {
8859
+ return Object.defineProperties({
8835
8860
  stream,
8836
8861
  provider: config.providerName,
8837
8862
  model: config.options.model,
@@ -8850,7 +8875,12 @@ Current user's request: ${currentInput}`;
8850
8875
  guardrailsBlocked: config.guardrailsBlocked,
8851
8876
  error: config.error,
8852
8877
  }),
8853
- };
8878
+ }, config.toolTelemetrySource
8879
+ ? pickLiveStreamDescriptors(config.toolTelemetrySource, [
8880
+ "toolsUsed",
8881
+ "toolExecutions",
8882
+ ])
8883
+ : {});
8854
8884
  }
8855
8885
  /**
8856
8886
  * Handle stream error with fallback
@@ -264,14 +264,30 @@ export class AmazonBedrockProvider extends BaseProvider {
264
264
  provider: this.getProviderName(),
265
265
  ...(finishReason !== undefined && { finishReason }),
266
266
  ...(rawFinishReason !== undefined && { rawFinishReason }),
267
- toolsUsed: toolExecutions.map((execution) => execution.name),
267
+ // `toolsUsed` means RAN, not "was asked for". `runAgenticLoop` records
268
+ // every dispatch it performed, including the ones that never reached a
269
+ // tool: a name the model invented (TOOL_NOT_FOUND), one the breaker had
270
+ // already given up on (TOOL_PERMANENTLY_FAILED), and one that threw.
271
+ // Exactly those carry `error`; a call that reached the tool and got a
272
+ // value back does not. That is the same line `nativeGenerateLoop` draws
273
+ // — its `toolsUsed.push(name)` sits inside the try, after the guarded
274
+ // call returns — so filtering here makes Bedrock's generate path report
275
+ // what every other generate path reports, instead of inflating
276
+ // analytics, cost attribution and audit with calls that never ran.
277
+ //
278
+ // `toolCalls` and `toolExecutions` stay complete on purpose: they are
279
+ // the record of what the turn ATTEMPTED, and a failed attempt is
280
+ // precisely what a caller debugging a turn needs to see.
281
+ toolsUsed: toolExecutions
282
+ .filter((execution) => execution.error === undefined)
283
+ .map((execution) => execution.name),
268
284
  toolCalls: toolExecutions.map((execution) => ({
269
285
  toolCallId: execution.id,
270
286
  toolName: execution.name,
271
287
  args: execution.input,
272
288
  })),
273
289
  toolExecutions: resolveToolExecutionRecords(options, transformToolExecutions(toolExecutions)),
274
- enhancedWithTools: toolExecutions.length > 0,
290
+ enhancedWithTools: toolExecutions.some((execution) => execution.error === undefined),
275
291
  };
276
292
  }
277
293
  async conversationLoop(options) {
@@ -983,7 +999,15 @@ export class AmazonBedrockProvider extends BaseProvider {
983
999
  // alone left every streamed Bedrock turn reporting no tool calls,
984
1000
  // however many it made.
985
1001
  toolExecutions: outcome.result?.toolExecutions ?? [],
986
- toolsUsed: (outcome.result?.toolExecutions ?? []).map((execution) => execution.name),
1002
+ // Same "ran, not asked for" rule as the generate path above.
1003
+ // `toolCallCount` still derives from the unfiltered
1004
+ // `toolExecutions`, which is correct — a dispatch that failed was
1005
+ // still a call — so this list is a fallback today rather than an
1006
+ // observable surface. It is filtered anyway so that the moment
1007
+ // anything does surface it, it is not surfacing the same lie.
1008
+ toolsUsed: (outcome.result?.toolExecutions ?? [])
1009
+ .filter((execution) => execution.error === undefined)
1010
+ .map((execution) => execution.name),
987
1011
  }, Date.now() - startTime, {
988
1012
  requestId: `bedrock-stream-${Date.now()}`,
989
1013
  streamingMode: true,
@@ -77,6 +77,11 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
77
77
  let cacheWrite5mTokens = 0;
78
78
  let cacheWrite1hTokens = 0;
79
79
  let stepOutputTokens = 0;
80
+ // Thinking tokens are a SUBSET of output_tokens (Anthropic bills them as
81
+ // part of the same total), so this is tracked separately for
82
+ // observability and never added into outputTokens above.
83
+ let reasoningTokens = 0;
84
+ let stepReasoningTokens = 0;
80
85
  // Anthropic closes every complete message with `message_stop`. Its
81
86
  // absence is the only signal that the response ended early, because the
82
87
  // content blocks that DID arrive are syntactically complete.
@@ -96,6 +101,9 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
96
101
  const startOutput = usage?.output_tokens ?? 0;
97
102
  outputTokens += startOutput - stepOutputTokens;
98
103
  stepOutputTokens = startOutput;
104
+ const startReasoning = usage?.output_tokens_details?.thinking_tokens ?? 0;
105
+ reasoningTokens += startReasoning - stepReasoningTokens;
106
+ stepReasoningTokens = startReasoning;
99
107
  // Anthropic reports cache reads/writes separately from input_tokens
100
108
  // on this same event; without these the stream drops all cache
101
109
  // accounting.
@@ -186,6 +194,10 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
186
194
  const cumulative = event.usage?.output_tokens ?? stepOutputTokens;
187
195
  outputTokens += cumulative - stepOutputTokens;
188
196
  stepOutputTokens = cumulative;
197
+ const cumulativeReasoning = event.usage?.output_tokens_details?.thinking_tokens ??
198
+ stepReasoningTokens;
199
+ reasoningTokens += cumulativeReasoning - stepReasoningTokens;
200
+ stepReasoningTokens = cumulativeReasoning;
189
201
  continue;
190
202
  }
191
203
  if (event.type === "message_stop") {
@@ -327,6 +339,7 @@ async function readAnthropicStep({ params, client, channel, stepSignal, requestT
327
339
  // in that tier".
328
340
  ...(cacheWrite5mTokens ? { cacheWrite5mTokens } : {}),
329
341
  ...(cacheWrite1hTokens ? { cacheWrite1hTokens } : {}),
342
+ ...(reasoningTokens ? { reasoningTokens } : {}),
330
343
  },
331
344
  rawStopReason,
332
345
  raw: blocks,
@@ -68,7 +68,11 @@ export declare function buildNativeConfig(options: {
68
68
  temperature?: number;
69
69
  maxTokens?: number;
70
70
  systemPrompt?: string;
71
- /** Model id, used for the registry-driven sampling-param strip. */
71
+ /**
72
+ * Model id, used for the registry-driven sampling-param strip and to
73
+ * pick the right thinkingConfig wire shape (thinkingLevel for Gemini 3,
74
+ * a translated numeric thinkingBudget for Gemini 2.5).
75
+ */
72
76
  model?: string;
73
77
  thinkingConfig?: ThinkingConfig;
74
78
  /**
@@ -435,8 +435,15 @@ export function buildNativeConfig(options, toolsConfig) {
435
435
  if (options.systemPrompt) {
436
436
  config.systemInstruction = options.systemPrompt;
437
437
  }
438
- // Add thinking config for Gemini 3
439
- const nativeThinkingConfig = createNativeThinkingConfig(options.thinkingConfig);
438
+ // Add thinking config. `options.model` is the resolved model id (both
439
+ // call sites in googleAiStudio/client.ts pass their effective `modelName`
440
+ // in) — createNativeThinkingConfig picks the wire shape from it: Gemini 3
441
+ // gets thinkingLevel passthrough, Gemini 2.5 gets a translated numeric
442
+ // thinkingBudget (see thinkingConfig.ts). Mirrors the Vertex call sites
443
+ // in googleVertex/client.ts, which this native path was previously
444
+ // missing — Google AI Studio's identical native SDK rejects
445
+ // thinkingLevel on Gemini 2.5 exactly like Vertex does.
446
+ const nativeThinkingConfig = createNativeThinkingConfig(options.thinkingConfig, options.model);
440
447
  if (nativeThinkingConfig) {
441
448
  config.thinkingConfig = nativeThinkingConfig;
442
449
  }
@@ -202,6 +202,43 @@ export declare class GoogleVertexProvider extends BaseProvider {
202
202
  * Execute stream using native @anthropic-ai/vertex-sdk for Claude models on Vertex AI
203
203
  * This bypasses @ai-sdk/google-vertex completely and uses Anthropic's native SDK
204
204
  */
205
+ /**
206
+ * Extended thinking for Claude-on-Vertex.
207
+ *
208
+ * The direct Anthropic provider has always translated `thinkingConfig` into
209
+ * the Messages API's `thinking` field (anthropic/client.ts). This path never
210
+ * did — it built the request without one — so `thinkingConfig` was accepted
211
+ * and silently ignored here: no error, `usage.reasoning` 0, no reasoning
212
+ * content. Measured against the raw @anthropic-ai/vertex-sdk with the same
213
+ * project, region, credentials and model, `thinking` is supported and
214
+ * returns a real thinking block, so the capability was ours to send.
215
+ *
216
+ * `thinkingLevel` is deliberately not mapped to a budget: the option's
217
+ * contract reserves `budgetTokens` for Anthropic models and `thinkingLevel`
218
+ * for Gemini 3, and inventing a conversion here would be guessing at the
219
+ * caller's intent.
220
+ *
221
+ * The budget must leave room inside `max_tokens` for an answer, so it is
222
+ * clamped below the ceiling rather than passed through — an over-large
223
+ * budget is a 400 from the API.
224
+ *
225
+ * `useFinalResultTool` (schema / structured-output mode) always pairs with
226
+ * a forced `tool_choice:{type:"any"}` in the request built right after this
227
+ * call — Anthropic hard-rejects `thinking` combined with a `tool_choice`
228
+ * that forces tool use: a live 400, "Thinking may not be enabled when
229
+ * tool_choice forces tool use." So `thinking` is omitted entirely in that
230
+ * mode, preserving the prior (pre-thinking-fix) behaviour of silently
231
+ * ignoring `thinkingConfig` for schema calls instead of hard-failing them —
232
+ * mirrors the same guard already applied to the OAuth proxy path in
233
+ * `src/lib/proxy/oauthFetch.ts` (`delete parsed.thinking` when
234
+ * `tool_choice.type` is `"any"` or `"tool"`). The step/context-cap
235
+ * finalization backstop reuses this same omission for free since it spreads
236
+ * `...requestParams`. `tool_choice:{type:"none"}` (the separate
237
+ * tools-disabled backstop, used only outside schema mode) does not force
238
+ * tool use — it forces the opposite — so it is not gated here, consistent
239
+ * with oauthFetch.ts leaving `"none"` alone too.
240
+ */
241
+ private buildClaudeThinkingParam;
205
242
  private executeNativeAnthropicStream;
206
243
  /**
207
244
  * Execute generate using native @anthropic-ai/vertex-sdk for Claude models on Vertex AI
@@ -366,6 +403,17 @@ export declare class GoogleVertexProvider extends BaseProvider {
366
403
  /**
367
404
  * Get model suggestions when a model is not found
368
405
  */
406
+ /**
407
+ * Candidate model ids for a "model not available" error.
408
+ *
409
+ * This list is static. It cannot know what the caller's project and region
410
+ * actually serve, so it must not claim to: it previously announced
411
+ * "always available" and included the requested model itself, which
412
+ * produced errors that recommended the very id that had just failed —
413
+ * `gemini-3-pro-preview-11-2025` suggesting `gemini-3-pro-preview-11-2025`.
414
+ * The requested model is now filtered out, and the wording says these are
415
+ * ids to try rather than ids known to work.
416
+ */
369
417
  private getModelSuggestions;
370
418
  /**
371
419
  * Generate an embedding for `text` using Vertex via @google/genai.