@juspay/neurolink 12.41.3 → 12.41.4
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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +371 -371
- package/dist/core/baseProvider.js +24 -13
- package/dist/middleware/builtin/lifecycle.js +29 -6
- package/dist/neurolink.js +16 -13
- package/dist/providers/googleVertex/client.d.ts +20 -0
- package/dist/providers/googleVertex/client.js +42 -20
- package/dist/utils/lifecycleCallbacks.d.ts +30 -1
- package/dist/utils/lifecycleCallbacks.js +93 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
97
|
-
|
|
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 &&
|
|
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
|
|
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
|
};
|
|
@@ -271,6 +271,26 @@ export declare class GoogleVertexProvider extends BaseProvider {
|
|
|
271
271
|
* this for free via the AI SDK `wrapStream` middleware; native @google/genai
|
|
272
272
|
* bypasses that wrapper, so native consumers need their lifecycle
|
|
273
273
|
* callbacks invoked from here.
|
|
274
|
+
*
|
|
275
|
+
* `BaseProvider.stream()` always passes the result this method returns
|
|
276
|
+
* through `wrapStreamWithLifecycleCallbacks` afterwards too (that wrapper
|
|
277
|
+
* has "no early return when there are no callbacks" — see its own doc
|
|
278
|
+
* comment) — so unconditionally firing here double-fires onChunk/onFinish
|
|
279
|
+
* for every native Vertex stream. `isLifecycleStreamCallbacksOwnedByBaseProvider`
|
|
280
|
+
* reads the SAME nested lifecycle config off the SAME `options` object
|
|
281
|
+
* `wrapStreamWithLifecycleCallbacks` reads from (NeuroLink.applyStreamLifecycleMiddleware
|
|
282
|
+
* stamps it before either method runs), so both sites agree on which one
|
|
283
|
+
* actually fires without a second, Vertex-specific marker — and without
|
|
284
|
+
* relying on a Symbol on the `StreamResult` itself, which would not
|
|
285
|
+
* survive `withStreamModelFallback`'s rebuild of a plain forwarding object
|
|
286
|
+
* between this method and `wrapStreamWithLifecycleCallbacks`.
|
|
287
|
+
*
|
|
288
|
+
* onError is deliberately NOT gated by that marker — same precedent as
|
|
289
|
+
* `middleware/builtin/lifecycle.ts`'s `wrapStream` ("onError is NOT
|
|
290
|
+
* covered by this flag: it keeps its own, independent dedup via
|
|
291
|
+
* fireOnErrorOnce's shared fired-marker"). `fireOnErrorOnce` stamps the
|
|
292
|
+
* error so `BaseProvider`'s own catch (which checks
|
|
293
|
+
* `hasLifecycleErrorFired`) skips its own fire for the same failure.
|
|
274
294
|
*/
|
|
275
295
|
private wrapStreamResultWithLifecycle;
|
|
276
296
|
/**
|
|
@@ -22,6 +22,7 @@ import { FileDetector } from "../../utils/fileDetector.js";
|
|
|
22
22
|
import { mergeMediaFileAliases, normalizeVisionImageFormats, processUnifiedFilesArray, } from "../../utils/messageBuilder.js";
|
|
23
23
|
import { logger } from "../../utils/logger.js";
|
|
24
24
|
import { drainDetachedPump } from "../../utils/drainDetachedPump.js";
|
|
25
|
+
import { fireOnErrorOnce, getLifecycleMiddlewareConfig, isLifecycleStreamCallbacksOwnedByBaseProvider, } from "../../utils/lifecycleCallbacks.js";
|
|
25
26
|
import { GEMINI_ELISION_NOTE, planGeminiLoopReclaim, previewGeminiToolResponseText, } from "../../context/geminiLoopGuard.js";
|
|
26
27
|
import { ANTHROPIC_ELISION_NOTE, planAnthropicLoopReclaim, previewAnthropicToolResultText, } from "../../context/anthropicLoopGuard.js";
|
|
27
28
|
import { hasRestrictedOutputLimit, RESTRICTED_OUTPUT_TOKEN_LIMIT, toVertexAnthropicModelId, } from "../../utils/modelDetection.js";
|
|
@@ -5618,24 +5619,47 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5618
5619
|
* this for free via the AI SDK `wrapStream` middleware; native @google/genai
|
|
5619
5620
|
* bypasses that wrapper, so native consumers need their lifecycle
|
|
5620
5621
|
* callbacks invoked from here.
|
|
5622
|
+
*
|
|
5623
|
+
* `BaseProvider.stream()` always passes the result this method returns
|
|
5624
|
+
* through `wrapStreamWithLifecycleCallbacks` afterwards too (that wrapper
|
|
5625
|
+
* has "no early return when there are no callbacks" — see its own doc
|
|
5626
|
+
* comment) — so unconditionally firing here double-fires onChunk/onFinish
|
|
5627
|
+
* for every native Vertex stream. `isLifecycleStreamCallbacksOwnedByBaseProvider`
|
|
5628
|
+
* reads the SAME nested lifecycle config off the SAME `options` object
|
|
5629
|
+
* `wrapStreamWithLifecycleCallbacks` reads from (NeuroLink.applyStreamLifecycleMiddleware
|
|
5630
|
+
* stamps it before either method runs), so both sites agree on which one
|
|
5631
|
+
* actually fires without a second, Vertex-specific marker — and without
|
|
5632
|
+
* relying on a Symbol on the `StreamResult` itself, which would not
|
|
5633
|
+
* survive `withStreamModelFallback`'s rebuild of a plain forwarding object
|
|
5634
|
+
* between this method and `wrapStreamWithLifecycleCallbacks`.
|
|
5635
|
+
*
|
|
5636
|
+
* onError is deliberately NOT gated by that marker — same precedent as
|
|
5637
|
+
* `middleware/builtin/lifecycle.ts`'s `wrapStream` ("onError is NOT
|
|
5638
|
+
* covered by this flag: it keeps its own, independent dedup via
|
|
5639
|
+
* fireOnErrorOnce's shared fired-marker"). `fireOnErrorOnce` stamps the
|
|
5640
|
+
* error so `BaseProvider`'s own catch (which checks
|
|
5641
|
+
* `hasLifecycleErrorFired`) skips its own fire for the same failure.
|
|
5621
5642
|
*/
|
|
5622
5643
|
wrapStreamResultWithLifecycle(options, result, startTime) {
|
|
5623
5644
|
const onChunk = options.onChunk;
|
|
5624
5645
|
const onFinish = options
|
|
5625
5646
|
.onFinish;
|
|
5626
|
-
|
|
5627
|
-
|
|
5647
|
+
// `StreamOptions.onError` is already properly typed as `OnErrorCallback`
|
|
5648
|
+
// (unlike the two casts above, kept from before that field existed on
|
|
5649
|
+
// the type) — read directly so `fireOnErrorOnce` below gets it without
|
|
5650
|
+
// a cast.
|
|
5651
|
+
const onError = options.onError;
|
|
5628
5652
|
if (typeof onChunk !== "function" &&
|
|
5629
5653
|
typeof onFinish !== "function" &&
|
|
5630
5654
|
typeof onError !== "function") {
|
|
5631
5655
|
return result;
|
|
5632
5656
|
}
|
|
5657
|
+
const streamCallbacksOwnedByBaseProvider = isLifecycleStreamCallbacksOwnedByBaseProvider(getLifecycleMiddlewareConfig(options));
|
|
5633
5658
|
const originalIterable = result.stream;
|
|
5634
5659
|
let accumulated = "";
|
|
5635
5660
|
let sequence = 0;
|
|
5636
|
-
const provider = this.providerName;
|
|
5637
5661
|
const fireOnChunk = (payload) => {
|
|
5638
|
-
if (typeof onChunk !== "function") {
|
|
5662
|
+
if (typeof onChunk !== "function" || streamCallbacksOwnedByBaseProvider) {
|
|
5639
5663
|
return;
|
|
5640
5664
|
}
|
|
5641
5665
|
try {
|
|
@@ -5647,7 +5671,8 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5647
5671
|
}
|
|
5648
5672
|
};
|
|
5649
5673
|
const fireOnFinish = (payload) => {
|
|
5650
|
-
if (typeof onFinish !== "function"
|
|
5674
|
+
if (typeof onFinish !== "function" ||
|
|
5675
|
+
streamCallbacksOwnedByBaseProvider) {
|
|
5651
5676
|
return;
|
|
5652
5677
|
}
|
|
5653
5678
|
try {
|
|
@@ -5686,21 +5711,18 @@ export class GoogleVertexProvider extends BaseProvider {
|
|
|
5686
5711
|
});
|
|
5687
5712
|
}
|
|
5688
5713
|
catch (err) {
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
}
|
|
5702
|
-
}
|
|
5703
|
-
throw err;
|
|
5714
|
+
const errInst = err instanceof Error ? err : new Error(String(err));
|
|
5715
|
+
fireOnErrorOnce(onError, errInst, {
|
|
5716
|
+
error: errInst,
|
|
5717
|
+
duration: Date.now() - startTime,
|
|
5718
|
+
recoverable: false,
|
|
5719
|
+
});
|
|
5720
|
+
// Rethrow the normalized `errInst` (not the raw `err`) so the
|
|
5721
|
+
// fired-mark `fireOnErrorOnce` just stamped on it propagates to
|
|
5722
|
+
// BaseProvider's own catch (`hasLifecycleErrorFired`), which
|
|
5723
|
+
// would otherwise see an unstamped error and fire onError again
|
|
5724
|
+
// for the same logical failure.
|
|
5725
|
+
throw errInst;
|
|
5704
5726
|
}
|
|
5705
5727
|
},
|
|
5706
5728
|
};
|
|
@@ -27,7 +27,36 @@
|
|
|
27
27
|
* structuredClone / cross-realm postMessage where a closed-over
|
|
28
28
|
* WeakSet does not.
|
|
29
29
|
*/
|
|
30
|
-
import type { LifecycleErrorPayload, OnErrorCallback } from "../types/index.js";
|
|
30
|
+
import type { LifecycleErrorPayload, LifecycleMiddlewareConfig, OnErrorCallback } from "../types/index.js";
|
|
31
|
+
/**
|
|
32
|
+
* Read the consumer-facing lifecycle callbacks buried inside a request's
|
|
33
|
+
* middleware blob. The parameter is `unknown` on purpose: request options
|
|
34
|
+
* arrive as several structurally-unrelated shapes (StreamOptions,
|
|
35
|
+
* TextGenerationOptions), and the lifecycle branch is an optional add-on
|
|
36
|
+
* none of them declare — a single structural view keeps the read cast-free
|
|
37
|
+
* at every call site.
|
|
38
|
+
*
|
|
39
|
+
* Shared (moved out of `BaseProvider`) because two independent firing sites
|
|
40
|
+
* now need the same read: `BaseProvider.wrapStreamWithLifecycleCallbacks`
|
|
41
|
+
* (every provider's stream) and `GoogleVertexProvider.wrapStreamResultWithLifecycle`
|
|
42
|
+
* (Vertex's own native-stream firer, which must check the SAME
|
|
43
|
+
* `isLifecycleStreamCallbacksOwnedByBaseProvider` marker on the SAME nested
|
|
44
|
+
* config object that BaseProvider's wrapper reads from `options`, so that
|
|
45
|
+
* both sites agree on who owns firing without a second marker).
|
|
46
|
+
*/
|
|
47
|
+
export declare function getLifecycleMiddlewareConfig(options: unknown): LifecycleMiddlewareConfig | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* Stamp a stream lifecycle config object with
|
|
50
|
+
* `STREAM_CALLBACKS_OWNED_BY_BASE_PROVIDER`. See that symbol's doc comment
|
|
51
|
+
* for who may call this (only `applyStreamLifecycleMiddleware`) and why.
|
|
52
|
+
*/
|
|
53
|
+
export declare function markLifecycleStreamCallbacksOwnedByBaseProvider(config: object): void;
|
|
54
|
+
/**
|
|
55
|
+
* True when `markLifecycleStreamCallbacksOwnedByBaseProvider` already
|
|
56
|
+
* stamped this config. The model-level lifecycle middleware reads this to
|
|
57
|
+
* decide whether to fire its own onChunk/onFinish for a stream config.
|
|
58
|
+
*/
|
|
59
|
+
export declare function isLifecycleStreamCallbacksOwnedByBaseProvider(config: unknown): boolean;
|
|
31
60
|
/**
|
|
32
61
|
* Returns true when `markLifecycleErrorFired` or a previous
|
|
33
62
|
* `fireOnErrorOnce` call has already stamped this error.
|
|
@@ -28,6 +28,99 @@
|
|
|
28
28
|
* WeakSet does not.
|
|
29
29
|
*/
|
|
30
30
|
const ON_ERROR_FIRED = Symbol.for("neurolink.onErrorFired");
|
|
31
|
+
/**
|
|
32
|
+
* Read the consumer-facing lifecycle callbacks buried inside a request's
|
|
33
|
+
* middleware blob. The parameter is `unknown` on purpose: request options
|
|
34
|
+
* arrive as several structurally-unrelated shapes (StreamOptions,
|
|
35
|
+
* TextGenerationOptions), and the lifecycle branch is an optional add-on
|
|
36
|
+
* none of them declare — a single structural view keeps the read cast-free
|
|
37
|
+
* at every call site.
|
|
38
|
+
*
|
|
39
|
+
* Shared (moved out of `BaseProvider`) because two independent firing sites
|
|
40
|
+
* now need the same read: `BaseProvider.wrapStreamWithLifecycleCallbacks`
|
|
41
|
+
* (every provider's stream) and `GoogleVertexProvider.wrapStreamResultWithLifecycle`
|
|
42
|
+
* (Vertex's own native-stream firer, which must check the SAME
|
|
43
|
+
* `isLifecycleStreamCallbacksOwnedByBaseProvider` marker on the SAME nested
|
|
44
|
+
* config object that BaseProvider's wrapper reads from `options`, so that
|
|
45
|
+
* both sites agree on who owns firing without a second marker).
|
|
46
|
+
*/
|
|
47
|
+
export function getLifecycleMiddlewareConfig(options) {
|
|
48
|
+
return options?.middleware
|
|
49
|
+
?.middlewareConfig?.lifecycle?.config;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Marks a stream lifecycle config as "BaseProvider.wrapStreamWithLifecycleCallbacks
|
|
53
|
+
* already fires onChunk/onFinish for this config, authoritatively, from
|
|
54
|
+
* NeuroLink's own normalized stream — the model-level lifecycle middleware
|
|
55
|
+
* (middleware/builtin/lifecycle.ts) must not ALSO fire them from the raw
|
|
56
|
+
* per-provider V3 chunks it sees."
|
|
57
|
+
*
|
|
58
|
+
* Only `NeuroLink.applyStreamLifecycleMiddleware()` stamps a config with this
|
|
59
|
+
* marker, and only for stream()'s config: it is the one function that builds
|
|
60
|
+
* a lifecycle config fresh from stream()'s top-level onChunk/onFinish/onError
|
|
61
|
+
* options, for a call that unconditionally passes through
|
|
62
|
+
* `BaseProvider.wrapStreamWithLifecycleCallbacks` afterwards — see that
|
|
63
|
+
* method's doc comment ("the only point every provider's stream passes
|
|
64
|
+
* through unconditionally"). Without this marker both layers fire
|
|
65
|
+
* onChunk/onFinish for the same logical stream: once (unreliable, raw,
|
|
66
|
+
* provider-shape-dependent) from the model-level middleware, once (reliable,
|
|
67
|
+
* normalized) from BaseProvider.
|
|
68
|
+
*
|
|
69
|
+
* `NeuroLink.applyGenerateLifecycleMiddleware()` must NEVER stamp its config
|
|
70
|
+
* this way. BaseProvider has no equivalent guaranteed-delivery layer for
|
|
71
|
+
* generate()'s onFinish: `BaseProvider.finalizeNativeGenerate`'s own doc
|
|
72
|
+
* comment states onFinish is deliberately NOT fired there because "the
|
|
73
|
+
* native paths now wrap their model, so the lifecycle middleware fires it" —
|
|
74
|
+
* the model-level middleware is the SOLE firing site for every native
|
|
75
|
+
* generate loop that goes through the model wrap (sagemaker,
|
|
76
|
+
* openaiChatCompletionsBase, anthropic). The one generate path that bypasses
|
|
77
|
+
* the model wrap (GoogleVertex's native @google/genai loop) fires onFinish
|
|
78
|
+
* itself via `BaseProvider.fireGenerateOnFinish` precisely because the
|
|
79
|
+
* middleware never runs for it. Stamping the generate config would make the
|
|
80
|
+
* model-level middleware suppress its own firing with nothing left to fire
|
|
81
|
+
* it, silently breaking onFinish for every one of those providers.
|
|
82
|
+
*
|
|
83
|
+
* A config a consumer builds directly — e.g. calling the exported
|
|
84
|
+
* `createLifecycleMiddleware` and applying it to their own model via the
|
|
85
|
+
* `ai` package's `wrapLanguageModel`, entirely outside NeuroLink's
|
|
86
|
+
* BaseProvider/stream()/generate() call path — is never stamped, so that
|
|
87
|
+
* standalone use keeps firing onChunk/onFinish exactly as before (CLAUDE.md
|
|
88
|
+
* rule 5: public SDK API must not break existing callers).
|
|
89
|
+
*
|
|
90
|
+
* Symbol-stamped on the object itself (not a typed field) for the same
|
|
91
|
+
* reason as `ON_ERROR_FIRED` above: it survives across separate module
|
|
92
|
+
* copies of the same config object where a closed-over WeakSet would not.
|
|
93
|
+
*/
|
|
94
|
+
const STREAM_CALLBACKS_OWNED_BY_BASE_PROVIDER = Symbol.for("neurolink.lifecycleStreamCallbacksOwnedByBaseProvider");
|
|
95
|
+
/**
|
|
96
|
+
* Stamp a stream lifecycle config object with
|
|
97
|
+
* `STREAM_CALLBACKS_OWNED_BY_BASE_PROVIDER`. See that symbol's doc comment
|
|
98
|
+
* for who may call this (only `applyStreamLifecycleMiddleware`) and why.
|
|
99
|
+
*/
|
|
100
|
+
export function markLifecycleStreamCallbacksOwnedByBaseProvider(config) {
|
|
101
|
+
try {
|
|
102
|
+
Object.defineProperty(config, STREAM_CALLBACKS_OWNED_BY_BASE_PROVIDER, {
|
|
103
|
+
value: true,
|
|
104
|
+
enumerable: false,
|
|
105
|
+
writable: false,
|
|
106
|
+
configurable: false,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Non-extensible config object — worst case is the pre-fix double fire.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* True when `markLifecycleStreamCallbacksOwnedByBaseProvider` already
|
|
115
|
+
* stamped this config. The model-level lifecycle middleware reads this to
|
|
116
|
+
* decide whether to fire its own onChunk/onFinish for a stream config.
|
|
117
|
+
*/
|
|
118
|
+
export function isLifecycleStreamCallbacksOwnedByBaseProvider(config) {
|
|
119
|
+
if (config === null || typeof config !== "object") {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
return (config[STREAM_CALLBACKS_OWNED_BY_BASE_PROVIDER] === true);
|
|
123
|
+
}
|
|
31
124
|
function stampFired(error) {
|
|
32
125
|
try {
|
|
33
126
|
Object.defineProperty(error, ON_ERROR_FIRED, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.41.
|
|
3
|
+
"version": "12.41.4",
|
|
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": {
|