@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.
- package/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +410 -408
- package/dist/core/baseProvider.js +24 -13
- package/dist/middleware/builtin/lifecycle.js +29 -6
- package/dist/neurolink.js +16 -13
- package/dist/providers/amazonBedrock/client.d.ts +27 -0
- package/dist/providers/amazonBedrock/client.js +418 -113
- package/dist/providers/amazonBedrock/modelBridge.d.ts +100 -0
- package/dist/providers/amazonBedrock/modelBridge.js +226 -0
- 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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges Bedrock's native `BedrockMessage[]` conversation shape and
|
|
3
|
+
* `AgenticLoopChunk` stream to the AI SDK V3 model-middleware protocol
|
|
4
|
+
* (`LanguageModelV3`), so `BaseProvider.applyMiddlewareToModel()` can wrap a
|
|
5
|
+
* turn built entirely out of `runAgenticLoop` — the same engine Bedrock's
|
|
6
|
+
* `generate()` and `executeStream()` already run on.
|
|
7
|
+
*
|
|
8
|
+
* Scope: only the conversation `generate()`/`executeStream()` hand to the
|
|
9
|
+
* loop as its STARTING point ever crosses this boundary. Both reset
|
|
10
|
+
* `conversationHistory` to a single fresh message before calling the loop
|
|
11
|
+
* (see `client.ts`), so that starting point is always `system + one user
|
|
12
|
+
* message` — never prior assistant/tool-result turns, which are constructed
|
|
13
|
+
* entirely inside the loop's own steps and stay in native
|
|
14
|
+
* `BedrockMessage[]`/`ConverseCommand` shape. A general, lossless
|
|
15
|
+
* `BedrockMessage` <-> `ModelMessage` converter is not attempted here, and
|
|
16
|
+
* would not be possible anyway: `BedrockToolResult` carries no `toolName`,
|
|
17
|
+
* which a faithful `ToolModelMessage` requires.
|
|
18
|
+
*
|
|
19
|
+
* Further scoped to TEXT-ONLY turns. A multimodal starting message (image,
|
|
20
|
+
* document, or — pathologically — a tool-use/tool-result block) skips
|
|
21
|
+
* wrapping entirely; the caller checks `isTextOnlyBedrockConversation` and
|
|
22
|
+
* falls back to calling the loop directly, logging that the turn was
|
|
23
|
+
* excluded. This mirrors the plan's AI Studio precedent: an excluded shape
|
|
24
|
+
* must be explicit in code, not implied by no test ever sending one.
|
|
25
|
+
*
|
|
26
|
+
* Who owns the outcome. When the wrapped `doGenerate`/`doStream` ran the loop,
|
|
27
|
+
* the loop's own result is authoritative for `finishReason`, `usage`,
|
|
28
|
+
* `toolExecutions` and the final conversation: a middleware may replace the
|
|
29
|
+
* content or the stream parts a caller sees, but it cannot rewrite the
|
|
30
|
+
* metadata of a turn that already reached the wire. When a middleware never
|
|
31
|
+
* calls through (a cache hit, a refusal, a synthetic answer) no loop ran, and
|
|
32
|
+
* its own V3 result is what gets reported.
|
|
33
|
+
*/
|
|
34
|
+
import type { ToolInputSchema } from "@aws-sdk/client-bedrock-runtime";
|
|
35
|
+
import type { AgenticLoopChunk, BedrockMessage, LanguageModelV3CallOptions, LanguageModelV3StreamPart, ModelMessage } from "../../types/index.js";
|
|
36
|
+
/** Names of the unsent options `params` carries; an empty list counts as unset. */
|
|
37
|
+
export declare function unsentBedrockMiddlewareOptions(params: LanguageModelV3CallOptions): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Unwrap Bedrock's `ToolSpecification.inputSchema` — always `{ json:
|
|
40
|
+
* DocumentType }` on the wire (see `ToolInputSchema.JsonMember` in the AWS
|
|
41
|
+
* SDK), never the bare schema — to the plain JSON schema object the V3 tool
|
|
42
|
+
* shape's `inputSchema` field expects. `client.ts`'s own tool-formatting path
|
|
43
|
+
* (`formatToolsForBedrock`) always sends `{ json: schema }` to AWS for this
|
|
44
|
+
* exact reason; handing a middleware the wrapper itself instead of the
|
|
45
|
+
* schema breaks any schema-inspecting middleware (e.g. a guardrail
|
|
46
|
+
* validating tool arguments against the declared shape).
|
|
47
|
+
*/
|
|
48
|
+
export declare function unwrapBedrockToolInputSchema(inputSchema: ToolInputSchema | undefined): unknown;
|
|
49
|
+
/** True when every content block of every message is plain text. */
|
|
50
|
+
export declare function isTextOnlyBedrockConversation(conversation: BedrockMessage[]): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* `system` + the (already text-only) starting conversation, as a V3 prompt.
|
|
53
|
+
* The system prompt is always sent as its own leading message — Bedrock
|
|
54
|
+
* carries it out-of-band (`ConverseCommandInput.system`), but the V3 prompt
|
|
55
|
+
* shape has no separate slot, so `bedrockConversationFromV3Prompt` is the
|
|
56
|
+
* inverse that pulls it back out.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildV3PromptFromBedrock(systemPrompt: string, conversation: BedrockMessage[]): ModelMessage[];
|
|
59
|
+
/**
|
|
60
|
+
* Inverse of `buildV3PromptFromBedrock`, read AFTER `transformParams` has
|
|
61
|
+
* run — this is what makes a middleware's prompt rewrite (correction 1)
|
|
62
|
+
* reach the wire: the caller passes the ORIGINAL prompt into `doGenerate`/
|
|
63
|
+
* `doStream`, and by AI SDK contract `transformParams` has already produced
|
|
64
|
+
* the version handed to the closure by the time this runs.
|
|
65
|
+
*
|
|
66
|
+
* A `tool` role message has no faithful Bedrock shape at this boundary (no
|
|
67
|
+
* toolUseId/toolName pairing survives `ToolModelMessage`), so one is
|
|
68
|
+
* dropped with a WARN rather than guessed at. The starting turn Bedrock
|
|
69
|
+
* ever hands across this boundary never contains one; seeing one here means
|
|
70
|
+
* middleware injected it.
|
|
71
|
+
*/
|
|
72
|
+
export declare function bedrockConversationFromV3Prompt(prompt: ModelMessage[]): {
|
|
73
|
+
system: string;
|
|
74
|
+
conversation: BedrockMessage[];
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Wrap a native `AgenticLoopChunk` source into the V3 stream contract, one
|
|
78
|
+
* chunk at a time, forwarding cancellation to both the source iterator and
|
|
79
|
+
* `cancel` (which the caller wires to the turn's own `AbortController`, so
|
|
80
|
+
* breaking out of a wrapped stream tears down the underlying AWS SDK
|
|
81
|
+
* request — correction 4). `finish` resolves the terminal `finish` part
|
|
82
|
+
* once the loop settles; middleware inspecting the returned stream sees a
|
|
83
|
+
* genuine usage-bearing finish part even when nothing downstream reads it
|
|
84
|
+
* back out (a guardrail that blocks before calling `doStream` never
|
|
85
|
+
* produces this stream at all, which is the `!loopPromise` case its own
|
|
86
|
+
* caller handles).
|
|
87
|
+
*/
|
|
88
|
+
export declare function bedrockChunksToV3Stream(source: AsyncIterable<AgenticLoopChunk>, finish: Promise<LanguageModelV3StreamPart>, cancel: () => void): ReadableStream<LanguageModelV3StreamPart>;
|
|
89
|
+
/**
|
|
90
|
+
* Inverse of `bedrockChunksToV3Stream`: drains a (possibly
|
|
91
|
+
* middleware-wrapped) V3 stream back into Bedrock's native chunk shape.
|
|
92
|
+
* `finish`/`error` parts are consumed here but not surfaced as chunks —
|
|
93
|
+
* `streamingConversationLoop` reads the turn's outcome from the loop's own
|
|
94
|
+
* `resultPromise`, which already carries richer usage/finishReason/
|
|
95
|
+
* toolExecutions than a V3 finish part does; this function exists only to
|
|
96
|
+
* satisfy the wrapped stream's own contract (a middleware may expect its
|
|
97
|
+
* returned stream to be drained to completion) and to `throw` a middleware
|
|
98
|
+
* `error` part into the consumer.
|
|
99
|
+
*/
|
|
100
|
+
export declare function bedrockV3StreamToChunks(stream: ReadableStream<LanguageModelV3StreamPart>): AsyncIterable<AgenticLoopChunk>;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridges Bedrock's native `BedrockMessage[]` conversation shape and
|
|
3
|
+
* `AgenticLoopChunk` stream to the AI SDK V3 model-middleware protocol
|
|
4
|
+
* (`LanguageModelV3`), so `BaseProvider.applyMiddlewareToModel()` can wrap a
|
|
5
|
+
* turn built entirely out of `runAgenticLoop` — the same engine Bedrock's
|
|
6
|
+
* `generate()` and `executeStream()` already run on.
|
|
7
|
+
*
|
|
8
|
+
* Scope: only the conversation `generate()`/`executeStream()` hand to the
|
|
9
|
+
* loop as its STARTING point ever crosses this boundary. Both reset
|
|
10
|
+
* `conversationHistory` to a single fresh message before calling the loop
|
|
11
|
+
* (see `client.ts`), so that starting point is always `system + one user
|
|
12
|
+
* message` — never prior assistant/tool-result turns, which are constructed
|
|
13
|
+
* entirely inside the loop's own steps and stay in native
|
|
14
|
+
* `BedrockMessage[]`/`ConverseCommand` shape. A general, lossless
|
|
15
|
+
* `BedrockMessage` <-> `ModelMessage` converter is not attempted here, and
|
|
16
|
+
* would not be possible anyway: `BedrockToolResult` carries no `toolName`,
|
|
17
|
+
* which a faithful `ToolModelMessage` requires.
|
|
18
|
+
*
|
|
19
|
+
* Further scoped to TEXT-ONLY turns. A multimodal starting message (image,
|
|
20
|
+
* document, or — pathologically — a tool-use/tool-result block) skips
|
|
21
|
+
* wrapping entirely; the caller checks `isTextOnlyBedrockConversation` and
|
|
22
|
+
* falls back to calling the loop directly, logging that the turn was
|
|
23
|
+
* excluded. This mirrors the plan's AI Studio precedent: an excluded shape
|
|
24
|
+
* must be explicit in code, not implied by no test ever sending one.
|
|
25
|
+
*
|
|
26
|
+
* Who owns the outcome. When the wrapped `doGenerate`/`doStream` ran the loop,
|
|
27
|
+
* the loop's own result is authoritative for `finishReason`, `usage`,
|
|
28
|
+
* `toolExecutions` and the final conversation: a middleware may replace the
|
|
29
|
+
* content or the stream parts a caller sees, but it cannot rewrite the
|
|
30
|
+
* metadata of a turn that already reached the wire. When a middleware never
|
|
31
|
+
* calls through (a cache hit, a refusal, a synthetic answer) no loop ran, and
|
|
32
|
+
* its own V3 result is what gets reported.
|
|
33
|
+
*/
|
|
34
|
+
import { logger } from "../../utils/logger.js";
|
|
35
|
+
/**
|
|
36
|
+
* Sampling and output-shaping options of a V3 call that Converse is never
|
|
37
|
+
* sent: both turn builders fill `inferenceConfig` with `maxTokens` and
|
|
38
|
+
* `temperature` alone. The bridge seeds none of these, so a value present
|
|
39
|
+
* when `doGenerate`/`doStream` runs was put there by a middleware, and would
|
|
40
|
+
* otherwise be dropped without a trace.
|
|
41
|
+
*/
|
|
42
|
+
const UNSENT_V3_OPTIONS = [
|
|
43
|
+
"topP",
|
|
44
|
+
"topK",
|
|
45
|
+
"presencePenalty",
|
|
46
|
+
"frequencyPenalty",
|
|
47
|
+
"stopSequences",
|
|
48
|
+
"seed",
|
|
49
|
+
"toolChoice",
|
|
50
|
+
"responseFormat",
|
|
51
|
+
];
|
|
52
|
+
/** Names of the unsent options `params` carries; an empty list counts as unset. */
|
|
53
|
+
export function unsentBedrockMiddlewareOptions(params) {
|
|
54
|
+
return UNSENT_V3_OPTIONS.filter((key) => {
|
|
55
|
+
const value = params[key];
|
|
56
|
+
return Array.isArray(value) ? value.length > 0 : value !== undefined;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Unwrap Bedrock's `ToolSpecification.inputSchema` — always `{ json:
|
|
61
|
+
* DocumentType }` on the wire (see `ToolInputSchema.JsonMember` in the AWS
|
|
62
|
+
* SDK), never the bare schema — to the plain JSON schema object the V3 tool
|
|
63
|
+
* shape's `inputSchema` field expects. `client.ts`'s own tool-formatting path
|
|
64
|
+
* (`formatToolsForBedrock`) always sends `{ json: schema }` to AWS for this
|
|
65
|
+
* exact reason; handing a middleware the wrapper itself instead of the
|
|
66
|
+
* schema breaks any schema-inspecting middleware (e.g. a guardrail
|
|
67
|
+
* validating tool arguments against the declared shape).
|
|
68
|
+
*/
|
|
69
|
+
export function unwrapBedrockToolInputSchema(inputSchema) {
|
|
70
|
+
return inputSchema && "json" in inputSchema ? inputSchema.json : undefined;
|
|
71
|
+
}
|
|
72
|
+
/** True when every content block of every message is plain text. */
|
|
73
|
+
export function isTextOnlyBedrockConversation(conversation) {
|
|
74
|
+
return conversation.every((message) => message.content.every((block) => block.text !== undefined &&
|
|
75
|
+
block.image === undefined &&
|
|
76
|
+
block.document === undefined &&
|
|
77
|
+
block.toolUse === undefined &&
|
|
78
|
+
block.toolResult === undefined));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `system` + the (already text-only) starting conversation, as a V3 prompt.
|
|
82
|
+
* The system prompt is always sent as its own leading message — Bedrock
|
|
83
|
+
* carries it out-of-band (`ConverseCommandInput.system`), but the V3 prompt
|
|
84
|
+
* shape has no separate slot, so `bedrockConversationFromV3Prompt` is the
|
|
85
|
+
* inverse that pulls it back out.
|
|
86
|
+
*/
|
|
87
|
+
export function buildV3PromptFromBedrock(systemPrompt, conversation) {
|
|
88
|
+
const prompt = [{ role: "system", content: systemPrompt }];
|
|
89
|
+
for (const message of conversation) {
|
|
90
|
+
const text = message.content.map((block) => block.text ?? "").join("");
|
|
91
|
+
if (message.role === "user") {
|
|
92
|
+
prompt.push({ role: "user", content: text });
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
prompt.push({ role: "assistant", content: text });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return prompt;
|
|
99
|
+
}
|
|
100
|
+
function contentToText(content) {
|
|
101
|
+
if (typeof content === "string") {
|
|
102
|
+
return content;
|
|
103
|
+
}
|
|
104
|
+
return content
|
|
105
|
+
.filter((part) => typeof part === "object" && part !== null && part.type === "text")
|
|
106
|
+
.map((part) => part.text)
|
|
107
|
+
.join("");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Inverse of `buildV3PromptFromBedrock`, read AFTER `transformParams` has
|
|
111
|
+
* run — this is what makes a middleware's prompt rewrite (correction 1)
|
|
112
|
+
* reach the wire: the caller passes the ORIGINAL prompt into `doGenerate`/
|
|
113
|
+
* `doStream`, and by AI SDK contract `transformParams` has already produced
|
|
114
|
+
* the version handed to the closure by the time this runs.
|
|
115
|
+
*
|
|
116
|
+
* A `tool` role message has no faithful Bedrock shape at this boundary (no
|
|
117
|
+
* toolUseId/toolName pairing survives `ToolModelMessage`), so one is
|
|
118
|
+
* dropped with a WARN rather than guessed at. The starting turn Bedrock
|
|
119
|
+
* ever hands across this boundary never contains one; seeing one here means
|
|
120
|
+
* middleware injected it.
|
|
121
|
+
*/
|
|
122
|
+
export function bedrockConversationFromV3Prompt(prompt) {
|
|
123
|
+
const systemParts = [];
|
|
124
|
+
const conversation = [];
|
|
125
|
+
for (const message of prompt) {
|
|
126
|
+
if (message.role === "system") {
|
|
127
|
+
systemParts.push(message.content);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (message.role === "tool") {
|
|
131
|
+
logger.warn("[AmazonBedrockProvider] middleware transformParams introduced a 'tool' role message; it has no Bedrock wire shape at this boundary and was dropped");
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
conversation.push({
|
|
135
|
+
role: message.role,
|
|
136
|
+
content: [{ text: contentToText(message.content) }],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return { system: systemParts.join("\n\n"), conversation };
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Wrap a native `AgenticLoopChunk` source into the V3 stream contract, one
|
|
143
|
+
* chunk at a time, forwarding cancellation to both the source iterator and
|
|
144
|
+
* `cancel` (which the caller wires to the turn's own `AbortController`, so
|
|
145
|
+
* breaking out of a wrapped stream tears down the underlying AWS SDK
|
|
146
|
+
* request — correction 4). `finish` resolves the terminal `finish` part
|
|
147
|
+
* once the loop settles; middleware inspecting the returned stream sees a
|
|
148
|
+
* genuine usage-bearing finish part even when nothing downstream reads it
|
|
149
|
+
* back out (a guardrail that blocks before calling `doStream` never
|
|
150
|
+
* produces this stream at all, which is the `!loopPromise` case its own
|
|
151
|
+
* caller handles).
|
|
152
|
+
*/
|
|
153
|
+
export function bedrockChunksToV3Stream(source, finish, cancel) {
|
|
154
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
155
|
+
return new ReadableStream({
|
|
156
|
+
async pull(controller) {
|
|
157
|
+
try {
|
|
158
|
+
const next = await iterator.next();
|
|
159
|
+
if (next.done) {
|
|
160
|
+
controller.enqueue(await finish);
|
|
161
|
+
controller.close();
|
|
162
|
+
}
|
|
163
|
+
else if (next.value.reasoning) {
|
|
164
|
+
controller.enqueue({
|
|
165
|
+
type: "reasoning-delta",
|
|
166
|
+
delta: next.value.reasoning,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
controller.enqueue({ type: "text-delta", delta: next.value.content });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
controller.error(error);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
async cancel() {
|
|
178
|
+
cancel();
|
|
179
|
+
await iterator.return?.();
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Inverse of `bedrockChunksToV3Stream`: drains a (possibly
|
|
185
|
+
* middleware-wrapped) V3 stream back into Bedrock's native chunk shape.
|
|
186
|
+
* `finish`/`error` parts are consumed here but not surfaced as chunks —
|
|
187
|
+
* `streamingConversationLoop` reads the turn's outcome from the loop's own
|
|
188
|
+
* `resultPromise`, which already carries richer usage/finishReason/
|
|
189
|
+
* toolExecutions than a V3 finish part does; this function exists only to
|
|
190
|
+
* satisfy the wrapped stream's own contract (a middleware may expect its
|
|
191
|
+
* returned stream to be drained to completion) and to `throw` a middleware
|
|
192
|
+
* `error` part into the consumer.
|
|
193
|
+
*/
|
|
194
|
+
export async function* bedrockV3StreamToChunks(stream) {
|
|
195
|
+
const reader = stream.getReader();
|
|
196
|
+
let done = false;
|
|
197
|
+
try {
|
|
198
|
+
while (true) {
|
|
199
|
+
const next = await reader.read();
|
|
200
|
+
if (next.done) {
|
|
201
|
+
done = true;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const part = next.value;
|
|
205
|
+
if (part.type === "text-delta") {
|
|
206
|
+
yield { content: part.delta };
|
|
207
|
+
}
|
|
208
|
+
else if (part.type === "reasoning-delta") {
|
|
209
|
+
yield { content: "", reasoning: part.delta };
|
|
210
|
+
}
|
|
211
|
+
else if (part.type === "error") {
|
|
212
|
+
throw part.error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
try {
|
|
218
|
+
if (!done) {
|
|
219
|
+
await reader.cancel();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
reader.releaseLock();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -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.5",
|
|
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": {
|