@juspay/neurolink 12.41.3 → 12.41.5

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.
@@ -24,7 +24,7 @@ import { sanitizeErrorCause } from "../utils/logSanitize.js";
24
24
  import { createAnalytics as buildAnalytics } from "./analytics.js";
25
25
  import { ErrorCategory, ErrorSeverity } from "../constants/enums.js";
26
26
  import { duckTypedStatusCode, extractRetryAfterMsFromError, } from "../utils/providerRetry.js";
27
- import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "../utils/lifecycleCallbacks.js";
27
+ import { getLifecycleMiddlewareConfig, hasLifecycleErrorFired, markLifecycleErrorFired, } from "../utils/lifecycleCallbacks.js";
28
28
  import { resolveLifecycleTimeoutMs } from "../utils/lifecycleTimeout.js";
29
29
  import { logger } from "../utils/logger.js";
30
30
  import { interleaveTTSStream } from "../utils/ttsStream.js";
@@ -51,18 +51,6 @@ import { generateOnceNative } from "../utils/nativeSingleShot.js";
51
51
  import { validateExecutionControl } from "../utils/parameterValidation.js";
52
52
  import { extractTokenUsage } from "../utils/tokenUtils.js";
53
53
  import { preserveLiveStreamAccessors } from "../utils/streamResultAccessors.js";
54
- /**
55
- * Read the consumer-facing lifecycle callbacks buried inside a request's
56
- * middleware blob. The parameter is `unknown` on purpose: request options
57
- * arrive as several structurally-unrelated shapes (StreamOptions,
58
- * TextGenerationOptions), and the lifecycle branch is an optional add-on
59
- * none of them declare — a single structural view keeps the read cast-free
60
- * at every call site.
61
- */
62
- function getLifecycleMiddlewareConfig(options) {
63
- return options?.middleware
64
- ?.middlewareConfig?.lifecycle?.config;
65
- }
66
54
  /**
67
55
  * Abstract base class for all AI providers
68
56
  * Tools are integrated as first-class citizens - always available by default
@@ -819,9 +807,32 @@ export class BaseProvider {
819
807
  yield chunk;
820
808
  }
821
809
  if (onFinish) {
810
+ // Read `result.usage` / `result.finishReason` here, after the
811
+ // drain loop above has fully exhausted `upstreamIterable` — the
812
+ // same point native background-loop providers (Vertex's Gemini3
813
+ // and Claude paths) resolve these fields by, whether they are
814
+ // plain values set before `result` was returned or live getters
815
+ // preserved through `withStreamModelFallback`
816
+ // (`preserveLiveStreamAccessors`). Both fields are generic
817
+ // `StreamResult` members for every provider, not Vertex-specific,
818
+ // so this enrichment is additive for all of them: only present
819
+ // when `result` actually carries a value, never a synthesized
820
+ // default (a provider that never populated these fields keeps
821
+ // getting the exact same bare `{text, duration}` payload as
822
+ // before).
823
+ const { usage, finishReason } = result;
822
824
  await safeFire(() => onFinish({
823
825
  text: accumulated,
824
826
  duration: Date.now() - startTime,
827
+ ...(usage
828
+ ? {
829
+ usage: {
830
+ promptTokens: usage.input ?? 0,
831
+ completionTokens: usage.output ?? 0,
832
+ },
833
+ }
834
+ : {}),
835
+ ...(finishReason ? { finishReason } : {}),
825
836
  }), "onFinish");
826
837
  }
827
838
  }
@@ -1,6 +1,6 @@
1
1
  import { logger } from "../../utils/logger.js";
2
2
  import { isRecoverableError } from "../../utils/errorHandling.js";
3
- import { fireOnErrorOnce } from "../../utils/lifecycleCallbacks.js";
3
+ import { fireOnErrorOnce, isLifecycleStreamCallbacksOwnedByBaseProvider, } from "../../utils/lifecycleCallbacks.js";
4
4
  /**
5
5
  * Normalize a thrown value to an `Error` while preserving structured
6
6
  * fields (`code`, `status`, `statusCode`, `retryAfter`, `details`,
@@ -88,19 +88,42 @@ export function createLifecycleMiddleware(config = {}) {
88
88
  if (!config.onChunk && !config.onFinish && !config.onError) {
89
89
  return result;
90
90
  }
91
+ // BaseProvider.wrapStreamWithLifecycleCallbacks already fires
92
+ // onChunk/onFinish for this exact config object, from NeuroLink's
93
+ // own normalized stream, for every NeuroLink-internal stream() call
94
+ // regardless of provider/transport. When that marker is present,
95
+ // firing onChunk/onFinish here too would deliver each one twice —
96
+ // once from these raw, provider-shaped V3 chunks, once from
97
+ // BaseProvider's normalized ones. onError is NOT covered by this
98
+ // flag: it keeps its own, independent dedup via `fireOnErrorOnce`'s
99
+ // shared fired-marker below, unchanged.
100
+ const streamCallbacksOwnedByBaseProvider = isLifecycleStreamCallbacksOwnedByBaseProvider(config);
91
101
  let sequenceNumber = 0;
92
102
  let accumulatedText = "";
93
103
  const transformStream = new TransformStream({
94
104
  transform(chunk, controller) {
95
105
  try {
96
- if (chunk.type === "text-delta") {
97
- accumulatedText += chunk.textDelta;
106
+ // V3 stream parts carry the delta on `delta` (see
107
+ // LanguageModelV3StreamPart in types/aiCompat.ts); `textDelta`
108
+ // is a V2-era AI-SDK field that V3 providers (e.g. Bedrock's
109
+ // bridge, this package's own openaiChatCompletionsBase) never
110
+ // set. Reading `chunk.textDelta` alone silently produced
111
+ // `undefined`, which string-concatenated into a literal
112
+ // "undefined" per chunk. Falling back to `textDelta` keeps any
113
+ // caller still emitting the older shape working unchanged.
114
+ const textDelta = chunk.type === "text-delta"
115
+ ? (chunk.delta ?? chunk.textDelta)
116
+ : undefined;
117
+ if (chunk.type === "text-delta" && textDelta !== undefined) {
118
+ accumulatedText += textDelta;
98
119
  }
99
- if (config.onChunk && chunk.type) {
120
+ if (config.onChunk &&
121
+ chunk.type &&
122
+ !streamCallbacksOwnedByBaseProvider) {
100
123
  try {
101
124
  const callbackResult = config.onChunk({
102
125
  type: chunk.type,
103
- textDelta: chunk.type === "text-delta" ? chunk.textDelta : undefined,
126
+ textDelta,
104
127
  sequenceNumber: sequenceNumber++,
105
128
  });
106
129
  Promise.resolve(callbackResult).catch((e) => {
@@ -124,7 +147,7 @@ export function createLifecycleMiddleware(config = {}) {
124
147
  }
125
148
  },
126
149
  flush() {
127
- if (config.onFinish) {
150
+ if (config.onFinish && !streamCallbacksOwnedByBaseProvider) {
128
151
  try {
129
152
  const callbackResult = config.onFinish({
130
153
  text: accumulatedText,
package/dist/neurolink.js CHANGED
@@ -79,7 +79,7 @@ import { tracers } from "./telemetry/tracers.js";
79
79
  import { getConversationMessages, storeConversationTurn, } from "./utils/conversationMemory.js";
80
80
  // Enhanced error handling imports
81
81
  import { CircuitBreaker, ERROR_CODES, ErrorFactory, isAbortError, isRetriableError, logStructuredError, NeuroLinkError, withRetry, withTimeout, } from "./utils/errorHandling.js";
82
- import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "./utils/lifecycleCallbacks.js";
82
+ import { hasLifecycleErrorFired, markLifecycleErrorFired, markLifecycleStreamCallbacksOwnedByBaseProvider, } from "./utils/lifecycleCallbacks.js";
83
83
  import { interleaveTTSStream } from "./utils/ttsStream.js";
84
84
  import { resolveLifecycleTimeoutMs } from "./utils/lifecycleTimeout.js";
85
85
  import { cloneOptionsForCallIsolation } from "./utils/cloneOptions.js";
@@ -782,6 +782,20 @@ export class NeuroLink {
782
782
  if (!options.onFinish && !options.onError && !options.onChunk) {
783
783
  return;
784
784
  }
785
+ const config = {
786
+ ...options.middleware?.middlewareConfig?.lifecycle?.config,
787
+ ...(options.onFinish !== undefined ? { onFinish: options.onFinish } : {}),
788
+ ...(options.onError !== undefined ? { onError: options.onError } : {}),
789
+ ...(options.onChunk !== undefined ? { onChunk: options.onChunk } : {}),
790
+ };
791
+ // BaseProvider.wrapStreamWithLifecycleCallbacks unconditionally wraps
792
+ // every provider's stream() result and fires onChunk/onFinish from
793
+ // there, reliably, for this exact config object. Stamp it so the
794
+ // model-level lifecycle middleware (builtin/lifecycle.ts) skips its own
795
+ // onChunk/onFinish firing for it instead of delivering each one twice.
796
+ // See markLifecycleStreamCallbacksOwnedByBaseProvider's doc comment for
797
+ // why generate()'s config below is never stamped this way.
798
+ markLifecycleStreamCallbacksOwnedByBaseProvider(config);
785
799
  options.middleware = {
786
800
  ...options.middleware,
787
801
  middlewareConfig: {
@@ -789,18 +803,7 @@ export class NeuroLink {
789
803
  lifecycle: {
790
804
  ...options.middleware?.middlewareConfig?.lifecycle,
791
805
  enabled: true,
792
- config: {
793
- ...options.middleware?.middlewareConfig?.lifecycle?.config,
794
- ...(options.onFinish !== undefined
795
- ? { onFinish: options.onFinish }
796
- : {}),
797
- ...(options.onError !== undefined
798
- ? { onError: options.onError }
799
- : {}),
800
- ...(options.onChunk !== undefined
801
- ? { onChunk: options.onChunk }
802
- : {}),
803
- },
806
+ config,
804
807
  },
805
808
  },
806
809
  };
@@ -68,6 +68,33 @@ export declare class AmazonBedrockProvider extends BaseProvider {
68
68
  private convertToBedrockMessages;
69
69
  getBedrockClient(): BedrockRuntimeClient;
70
70
  protected executeStream(options: StreamOptions): Promise<StreamResult>;
71
+ /**
72
+ * A multimodal turn skips model middleware (see modelBridge.ts's header).
73
+ * WARN when middleware was configured, per request or on the provider,
74
+ * since it silently did not run; multimodal turns alone are unremarkable,
75
+ * so stay at debug otherwise.
76
+ */
77
+ private logMultimodalMiddlewareSkip;
78
+ /**
79
+ * `inferenceConfig` carries `maxTokens` and `temperature` only, so any other
80
+ * sampling option a middleware sets would be dropped without a trace.
81
+ * `tools` is reported where it is compared, at each call site.
82
+ */
83
+ private warnOnUnsentMiddlewareOptions;
84
+ /**
85
+ * Runs the turn behind `streamingConversationLoop`: the model-middleware
86
+ * path for a text-only starting turn, or the direct fallback for a
87
+ * multimodal one. Extracted only to keep `streamingConversationLoop`
88
+ * readable — the branching logic itself is unchanged from what it
89
+ * replaced.
90
+ */
91
+ private buildBedrockStreamTurn;
92
+ /**
93
+ * Turns a settled loop outcome's usage into the shape both the analytics
94
+ * resolution and the stream's own `finally` block report. Shared so the
95
+ * two call sites in `streamingConversationLoop` can't drift.
96
+ */
97
+ private summarizeBedrockStreamUsage;
71
98
  private streamingConversationLoop;
72
99
  /**
73
100
  * Health check for Amazon Bedrock service