@juspay/neurolink 12.32.1 → 12.32.2

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.
@@ -450,6 +450,21 @@ export class BaseProvider {
450
450
  const sourceIterator = source[Symbol.asyncIterator]();
451
451
  let activeStream = source;
452
452
  let activeIterator = sourceIterator;
453
+ // The StreamResult whose non-stream fields (model, provider, metadata,
454
+ // analytics, tool*) the caller should see. Starts as the first attempt
455
+ // and is reassigned to a fallback candidate's own result the moment that
456
+ // candidate actually commits real output (see the retry loop below) —
457
+ // never to a candidate that failed before producing anything. Read by
458
+ // the live getters built at the bottom of this function, so a caller
459
+ // that reads a field AFTER a fallback takes over sees the attempt that
460
+ // actually served the stream, not the abandoned one.
461
+ let activeResult = result;
462
+ // Some provider results expose getter-only properties. The forwarding
463
+ // result below must still be writable because NeuroLink's stream pipeline
464
+ // assigns final fields after constructing it. Keep assignment overrides
465
+ // scoped to the result that was active when the assignment happened, so a
466
+ // value written to a rejected attempt cannot shadow a later retry.
467
+ const assignedValues = new WeakMap();
453
468
  const iterableOf = (iterator) => ({
454
469
  [Symbol.asyncIterator]: () => iterator,
455
470
  });
@@ -526,7 +541,13 @@ export class BaseProvider {
526
541
  retryHeld.push(chunk);
527
542
  continue;
528
543
  }
529
- retryCommitted = true;
544
+ if (!retryCommitted) {
545
+ retryCommitted = true;
546
+ // This candidate is now the attempt serving the stream:
547
+ // every metadata/analytics/tool field the caller reads must
548
+ // resolve from here, never from the abandoned first attempt.
549
+ activeResult = retry;
550
+ }
530
551
  while (retryHeld.length > 0) {
531
552
  yield retryHeld.shift();
532
553
  }
@@ -567,14 +588,65 @@ export class BaseProvider {
567
588
  cancelStream(activeStream);
568
589
  releaseIterator(activeIterator);
569
590
  });
570
- // A naked spread reads (and thereby snapshots) every enumerable getter
571
- // on `result` — including a provider's lazily-populated `toolsUsed` /
572
- // `toolExecutions` — before the consumer has pulled a single chunk.
573
- // Re-applying the original accessor descriptors keeps those fields live.
574
- return preserveLiveStreamAccessors(result, {
575
- ...result,
591
+ // A naked spread would read (and thereby snapshot) every field on
592
+ // `result` immediately — before the generator above has even started —
593
+ // permanently freezing the first attempt's values. Define every declared
594
+ // StreamResult field instead of only the keys present on `result`: retry
595
+ // providers may expose optional fields the rejected attempt omitted.
596
+ const target = {
576
597
  stream: wrapped,
577
- });
598
+ };
599
+ const resultKeys = [
600
+ "knowledge",
601
+ "agentModeVersion",
602
+ "provider",
603
+ "model",
604
+ "usage",
605
+ "finishReason",
606
+ "stopReason",
607
+ "rawFinishReason",
608
+ "toolCalls",
609
+ "toolResults",
610
+ "toolEvents",
611
+ "toolExecutions",
612
+ "toolsUsed",
613
+ "metadata",
614
+ "analytics",
615
+ "evaluation",
616
+ "events",
617
+ "workflow",
618
+ "transcription",
619
+ "audio",
620
+ "ttsMetadata",
621
+ ];
622
+ for (const key of resultKeys) {
623
+ Object.defineProperty(target, key, {
624
+ enumerable: true,
625
+ configurable: true,
626
+ get: () => {
627
+ const assigned = assignedValues.get(activeResult);
628
+ return assigned?.has(key) ? assigned.get(key) : activeResult[key];
629
+ },
630
+ set: (value) => {
631
+ const descriptor = Object.getOwnPropertyDescriptor(activeResult, key);
632
+ if (descriptor?.set) {
633
+ descriptor.set.call(activeResult, value);
634
+ return;
635
+ }
636
+ if (!descriptor || "value" in descriptor) {
637
+ Reflect.set(activeResult, key, value);
638
+ return;
639
+ }
640
+ let assigned = assignedValues.get(activeResult);
641
+ if (!assigned) {
642
+ assigned = new Map();
643
+ assignedValues.set(activeResult, assigned);
644
+ }
645
+ assigned.set(key, value);
646
+ },
647
+ });
648
+ }
649
+ return target;
578
650
  }
579
651
  /**
580
652
  * The eager half of the retired-model stream fallback, for providers whose
package/dist/neurolink.js CHANGED
@@ -83,7 +83,7 @@ import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecy
83
83
  import { interleaveTTSStream } from "./utils/ttsStream.js";
84
84
  import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
85
85
  import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
86
- import { pickLiveStreamDescriptors, preserveLiveStreamAccessors, } from "./utils/streamResultAccessors.js";
86
+ import { preserveLiveStreamAccessors } from "./utils/streamResultAccessors.js";
87
87
  import { coerceJsonToSchema, recoverScalarRoot, schemaAccepts, } from "./utils/json/coerce.js";
88
88
  // Factory processing imports
89
89
  import { createCleanStreamOptions, enhanceTextGenerationOptions, processFactoryOptions, processStreamingFactoryOptions, validateFactoryConfig, } from "./utils/factoryProcessing.js";
@@ -7828,7 +7828,8 @@ Current user's request: ${currentInput}`;
7828
7828
  // bottom of this method, without ever reading — and thereby freezing —
7829
7829
  // a getter-based provider's value here.
7830
7830
  const mcpStreamOutcome = await this.createMCPStream(enhancedOptions);
7831
- const { stream: mcpStream, provider: providerName, usage: streamUsage, model: streamModel, finishReason: streamFinishReason, toolCalls: streamToolCalls, toolResults: streamToolResults, analytics: streamAnalytics, metadata: providerStreamMetadata, } = mcpStreamOutcome;
7831
+ const { stream: mcpStream } = mcpStreamOutcome;
7832
+ const providerName = mcpStreamOutcome.provider ?? "unknown";
7832
7833
  let streamedTTSResult;
7833
7834
  const { stream: incrementalStream, ttsMetadata: streamTtsMetadata } = await this.createIncrementalTTSStream({
7834
7835
  stream: mcpStream,
@@ -7841,9 +7842,9 @@ Current user's request: ${currentInput}`;
7841
7842
  });
7842
7843
  ttsMetadataSink?.(streamTtsMetadata);
7843
7844
  const streamState = {
7844
- finishReason: streamFinishReason ?? "stop",
7845
- toolCalls: streamToolCalls,
7846
- toolResults: streamToolResults,
7845
+ finishReason: mcpStreamOutcome.finishReason ?? "stop",
7846
+ toolCalls: mcpStreamOutcome.toolCalls ?? [],
7847
+ toolResults: mcpStreamOutcome.toolResults ?? [],
7847
7848
  };
7848
7849
  streamSpan.setAttribute(ATTR.NL_PROVIDER, providerName || "unknown");
7849
7850
  let accumulatedContent = "";
@@ -7926,7 +7927,7 @@ Current user's request: ${currentInput}`;
7926
7927
  // not a provider failure, and retrying it on another provider
7927
7928
  // spends that provider's tokens to exceed a budget the caller set.
7928
7929
  const cappedByCallerBudget = enhancedOptions.fallbackOnMaxSteps === false &&
7929
- providerStreamMetadata?.stopReason === "step-cap";
7930
+ mcpStreamOutcome.metadata?.stopReason === "step-cap";
7930
7931
  if (realOutputChunks === 0 &&
7931
7932
  !cappedByCallerBudget &&
7932
7933
  !metadata.fallbackAttempted &&
@@ -7949,10 +7950,10 @@ Current user's request: ${currentInput}`;
7949
7950
  yield* incrementalFallback;
7950
7951
  }
7951
7952
  ttsResolver?.(streamedTTSResult);
7952
- resolvedUsage = streamUsage;
7953
- if (!resolvedUsage && streamAnalytics) {
7953
+ resolvedUsage = mcpStreamOutcome.usage;
7954
+ if (!resolvedUsage && mcpStreamOutcome.analytics) {
7954
7955
  try {
7955
- const resolved = await Promise.resolve(streamAnalytics);
7956
+ const resolved = await Promise.resolve(mcpStreamOutcome.analytics);
7956
7957
  if (resolved?.tokenUsage) {
7957
7958
  resolvedUsage = resolved.tokenUsage;
7958
7959
  }
@@ -7961,11 +7962,16 @@ Current user's request: ${currentInput}`;
7961
7962
  // non-blocking
7962
7963
  }
7963
7964
  }
7965
+ if (resolvedUsage) {
7966
+ mcpStreamOutcome.usage = resolvedUsage;
7967
+ }
7964
7968
  self.emitter.emit("stream:complete", {
7965
7969
  type: "stream:complete",
7966
7970
  content: accumulatedContent,
7967
7971
  provider: metadata.fallbackProvider ?? providerName,
7968
- model: metadata.fallbackModel ?? streamModel ?? enhancedOptions.model,
7972
+ model: metadata.fallbackModel ??
7973
+ mcpStreamOutcome.model ??
7974
+ enhancedOptions.model,
7969
7975
  finishReason: streamState.finishReason ?? "stop",
7970
7976
  prompt: enhancedOptions.input?.text ||
7971
7977
  enhancedOptions.prompt,
@@ -8047,7 +8053,7 @@ Current user's request: ${currentInput}`;
8047
8053
  try {
8048
8054
  const finalProvider = metadata.fallbackProvider ?? providerName ?? "unknown";
8049
8055
  const finalModel = metadata.fallbackModel ??
8050
- streamModel ??
8056
+ mcpStreamOutcome.model ??
8051
8057
  enhancedOptions.model ??
8052
8058
  "unknown";
8053
8059
  const finalFinishReason = streamError
@@ -8139,7 +8145,7 @@ Current user's request: ${currentInput}`;
8139
8145
  streamResult.toolCalls = streamState.toolCalls;
8140
8146
  streamResult.toolResults = streamState.toolResults;
8141
8147
  if (!streamResult.usage) {
8142
- streamResult.usage = streamUsage;
8148
+ streamResult.usage = mcpStreamOutcome.usage;
8143
8149
  }
8144
8150
  if (!streamResult.analytics) {
8145
8151
  // CRITICAL: do NOT `await` a Promise-typed analytics here. Bedrock
@@ -8155,8 +8161,7 @@ Current user's request: ${currentInput}`;
8155
8161
  // Promise<AnalyticsData>` — the cast widens the local
8156
8162
  // `processStreamResult` return type which only declares the
8157
8163
  // resolved shape.
8158
- streamResult.analytics =
8159
- streamAnalytics;
8164
+ streamResult.analytics = mcpStreamOutcome.analytics;
8160
8165
  }
8161
8166
  // The cost listener wants a resolved cost, but if the provider gave
8162
8167
  // us a Promise (Bedrock) we can't tax the stream's return path on it.
@@ -8183,8 +8188,7 @@ Current user's request: ${currentInput}`;
8183
8188
  guardrailsBlocked: metadata.guardrailsBlocked,
8184
8189
  error: metadata.error,
8185
8190
  events: eventSequence,
8186
- providerMetadata: providerStreamMetadata,
8187
- toolTelemetrySource: mcpStreamOutcome,
8191
+ sourceResult: mcpStreamOutcome,
8188
8192
  });
8189
8193
  }
8190
8194
  catch (error) {
@@ -8986,24 +8990,14 @@ Current user's request: ${currentInput}`;
8986
8990
  throw streamConsumeErr;
8987
8991
  }
8988
8992
  })();
8989
- // toolsUsed/toolExecutions are attached via defineProperties (not
8990
- // listed as plain fields above) so a getter-based provider result
8991
- // keeps resolving lazily through this boundary instead of being
8992
- // read — and frozen — once, right here, before any chunk is drained.
8993
- return Object.defineProperties({
8993
+ return preserveLiveStreamAccessors(poolStreamResult, {
8994
+ ...poolStreamResult,
8994
8995
  stream: wrappedStream,
8995
8996
  provider: poolStreamProviderName,
8996
- usage: poolStreamResult.usage,
8997
8997
  model: poolStreamResult.model || poolStreamModel,
8998
- finishReason: poolStreamResult.finishReason,
8999
8998
  toolCalls: poolStreamResult.toolCalls ?? [],
9000
8999
  toolResults: poolStreamResult.toolResults ?? [],
9001
- analytics: poolStreamResult.analytics,
9002
- metadata: poolStreamResult.metadata,
9003
- }, pickLiveStreamDescriptors(poolStreamResult, [
9004
- "toolsUsed",
9005
- "toolExecutions",
9006
- ]));
9000
+ });
9007
9001
  }
9008
9002
  catch (poolStreamError) {
9009
9003
  if (isAbortError(poolStreamError)) {
@@ -9048,26 +9042,17 @@ Current user's request: ${currentInput}`;
9048
9042
  provider: providerName,
9049
9043
  systemPromptPassedLength: enhancedSystemPrompt.length,
9050
9044
  });
9051
- // toolsUsed/toolExecutions are attached via defineProperties, not listed
9052
- // as plain fields: see the identical comment on the ModelPool branch
9053
- // above — a plain `toolsUsed: streamResult.toolsUsed` here would invoke
9054
- // and freeze a getter-based provider's value before any chunk is drained.
9055
- return Object.defineProperties({
9056
- stream: streamResult.stream,
9045
+ // Preserve every live descriptor from the provider result. Internal model
9046
+ // fallback can switch the descriptor's backing result only after iteration
9047
+ // starts, so eagerly rebuilding selected fields here would re-freeze the
9048
+ // rejected attempt before the caller drains a chunk.
9049
+ return preserveLiveStreamAccessors(streamResult, {
9050
+ ...streamResult,
9057
9051
  provider: providerName,
9058
- usage: streamResult.usage,
9059
9052
  model: streamResult.model || options.model,
9060
- finishReason: streamResult.finishReason,
9061
9053
  toolCalls: streamResult.toolCalls ?? [],
9062
9054
  toolResults: streamResult.toolResults ?? [],
9063
- analytics: streamResult.analytics,
9064
- // Pass the provider's metadata object THROUGH BY REFERENCE: native
9065
- // background-loop streams (Vertex Gemini/Claude) resolve
9066
- // finishReason/stopReason/rawFinishReason/stepsUsed onto it only when
9067
- // the loop finishes — snapshotting fields here would freeze them as
9068
- // undefined before the stream is drained.
9069
- metadata: streamResult.metadata,
9070
- }, pickLiveStreamDescriptors(streamResult, ["toolsUsed", "toolExecutions"]));
9055
+ });
9071
9056
  }
9072
9057
  /**
9073
9058
  * Process stream result
@@ -9098,10 +9083,20 @@ Current user's request: ${currentInput}`;
9098
9083
  * Create stream response
9099
9084
  */
9100
9085
  createStreamResponse(streamResult, stream, config) {
9101
- return Object.defineProperties({
9086
+ const source = config.sourceResult;
9087
+ const responseMetadata = {
9088
+ streamId: config.streamId,
9089
+ startTime: config.startTime,
9090
+ responseTime: config.responseTime,
9091
+ fallback: config.fallback || false,
9092
+ guardrailsBlocked: config.guardrailsBlocked,
9093
+ error: config.error,
9094
+ };
9095
+ const response = {
9096
+ ...(source ?? {}),
9102
9097
  stream,
9103
- provider: config.providerName,
9104
- model: config.options.model,
9098
+ provider: source?.provider ?? config.providerName,
9099
+ model: source?.model ?? config.options.model,
9105
9100
  usage: streamResult.usage,
9106
9101
  finishReason: streamResult.finishReason,
9107
9102
  toolCalls: streamResult.toolCalls,
@@ -9109,20 +9104,37 @@ Current user's request: ${currentInput}`;
9109
9104
  analytics: streamResult.analytics,
9110
9105
  evaluation: streamResult.evaluation,
9111
9106
  events: config.events && config.events.length > 0 ? config.events : undefined,
9112
- metadata: Object.assign(config.providerMetadata ?? {}, {
9113
- streamId: config.streamId,
9114
- startTime: config.startTime,
9115
- responseTime: config.responseTime,
9116
- fallback: config.fallback || false,
9117
- guardrailsBlocked: config.guardrailsBlocked,
9118
- error: config.error,
9119
- }),
9120
- }, config.toolTelemetrySource
9121
- ? pickLiveStreamDescriptors(config.toolTelemetrySource, [
9122
- "toolsUsed",
9123
- "toolExecutions",
9124
- ])
9125
- : {});
9107
+ metadata: source ? undefined : Object.assign({}, responseMetadata),
9108
+ };
9109
+ if (!source) {
9110
+ return response;
9111
+ }
9112
+ // NeuroLink grades the turn's terminal state itself (a normalized
9113
+ // finishReason, stopReason for aborts and time limits), so those fields
9114
+ // stay plain values here. Copying the provider's getter for them would
9115
+ // report the raw vendor reason, e.g. Anthropic's end_turn, instead.
9116
+ const gradedTerminalFields = new Set([
9117
+ "finishReason",
9118
+ "stopReason",
9119
+ "rawFinishReason",
9120
+ ]);
9121
+ for (const key of Object.getOwnPropertyNames(source)) {
9122
+ const descriptor = Object.getOwnPropertyDescriptor(source, key);
9123
+ if (!gradedTerminalFields.has(key) &&
9124
+ descriptor &&
9125
+ typeof descriptor.get === "function") {
9126
+ Object.defineProperty(response, key, descriptor);
9127
+ }
9128
+ }
9129
+ Object.defineProperty(response, "metadata", {
9130
+ enumerable: true,
9131
+ configurable: true,
9132
+ get: () => Object.assign(source.metadata ?? {}, responseMetadata),
9133
+ set: (value) => {
9134
+ source.metadata = value;
9135
+ },
9136
+ });
9137
+ return response;
9126
9138
  }
9127
9139
  /**
9128
9140
  * Handle stream error with fallback
@@ -905,6 +905,12 @@ export class GoogleVertexProvider extends BaseProvider {
905
905
  ...processedOptions,
906
906
  tools: optionTools,
907
907
  };
908
+ // Captured up-front (mirrors generate()'s inputPrompt) so the
909
+ // failure branch below can label a `generation:end` it emits before
910
+ // any content exists.
911
+ const inputPrompt = mergedOptions.input?.text ||
912
+ mergedOptions.prompt ||
913
+ "";
908
914
  try {
909
915
  // Route Claude models to native Anthropic SDK
910
916
  let result;
@@ -937,6 +943,17 @@ export class GoogleVertexProvider extends BaseProvider {
937
943
  catch (error) {
938
944
  this.fireGenerateOnError(options, error, streamStartTime);
939
945
  this.emitStreamEnd(modelName, streamStartTime, false, error);
946
+ // Unlike Google AI Studio's native stream, Vertex's turn loop is
947
+ // awaited to completion INSIDE executeNativeGemini3Stream /
948
+ // executeNativeAnthropicStream before this method ever returns a
949
+ // StreamResult — a first-call failure (e.g. a 500 on the initial
950
+ // request) rejects here, before the generic per-stream dedup
951
+ // machinery in NeuroLink.stream() (which only wraps an already
952
+ // -returned stream) ever runs. Without this call a failed Vertex
953
+ // stream reported zero `generation:end` events instead of one:
954
+ // `emitStreamEnd` above only ever emits `stream:end`, a different
955
+ // event Pipeline B and analytics don't listen for.
956
+ this.emitGenerationEnd(modelName, null, streamStartTime, false, error, inputPrompt);
940
957
  throw error;
941
958
  }
942
959
  }, (r) => r.stream,
@@ -5682,6 +5699,26 @@ export class GoogleVertexProvider extends BaseProvider {
5682
5699
  const usage = result?.usage && typeof result.usage === "object"
5683
5700
  ? result.usage
5684
5701
  : { input: 0, output: 0, total: 0 };
5702
+ // Mark on the result so the SDK-level runStandardGenerateRequest knows
5703
+ // this provider already emitted `generation:end` itself and skips its
5704
+ // own duplicate emission. Without this flag the public event listener
5705
+ // (and the observability test) would see two events per generate call.
5706
+ if (result && typeof result === "object") {
5707
+ result._generationEndEmitted =
5708
+ true;
5709
+ }
5710
+ // The failure path has no result to carry that flag — `result` is null —
5711
+ // so mark the error instead. Without this, `generateTextInternal`'s
5712
+ // wrapProviderError (which replaces this error with a new one further up
5713
+ // the stack) has nothing to copy forward, and emitGenerateErrorEvent goes
5714
+ // on to emit its own `generation:end` for a failure this provider already
5715
+ // reported — double-counting every failed native Vertex generate in
5716
+ // analytics and as two Pipeline B spans. Same shape as the AI Studio
5717
+ // (#1745) and Bedrock (#1762) fixes for the same gap.
5718
+ if (!success && error && typeof error === "object") {
5719
+ error._generationEndEmitted =
5720
+ true;
5721
+ }
5685
5722
  emitter.emit("generation:end", {
5686
5723
  provider: this.providerName,
5687
5724
  responseTime: Date.now() - startTime,
@@ -5702,14 +5739,6 @@ export class GoogleVertexProvider extends BaseProvider {
5702
5739
  ? { error: error instanceof Error ? error.message : String(error) }
5703
5740
  : {}),
5704
5741
  });
5705
- // Mark on the result so the SDK-level runStandardGenerateRequest knows
5706
- // this provider already emitted `generation:end` itself and skips its
5707
- // own duplicate emission. Without this flag the public event listener
5708
- // (and the observability test) would see two events per generate call.
5709
- if (result && typeof result === "object") {
5710
- result._generationEndEmitted =
5711
- true;
5712
- }
5713
5742
  }
5714
5743
  /**
5715
5744
  * Turn-clock-shaped adapter over the shared {@link buildLoopExitMessage}.