@juspay/neurolink 12.6.0 → 12.6.1
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 +3 -7
- package/dist/browser/neurolink.min.js +99 -99
- package/dist/cli/proxy-clients/gemini.js +55 -13
- package/dist/providers/sagemaker/client.js +30 -2
- package/dist/providers/sagemaker/language-model.d.ts +9 -0
- package/dist/providers/sagemaker/language-model.js +37 -0
- package/dist/types/providers.d.ts +10 -0
- package/package.json +1 -1
|
@@ -57,20 +57,49 @@ function upsertEnvVars(original, vars) {
|
|
|
57
57
|
// Match an assignment at line start, tolerating `export ` and surrounding
|
|
58
58
|
// spaces. Anchored per-line so a key mentioned inside a comment or another
|
|
59
59
|
// value is not rewritten.
|
|
60
|
-
const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}[ \\t]*=.*$`, "m");
|
|
61
60
|
const line = `${key}=${value}`;
|
|
61
|
+
const all = new RegExp(`^[ \t]*(?:export[ \t]+)?${key}[ \t]*=.*(?:\r?\n|$)`, "gm");
|
|
62
|
+
const occurrences = text.match(all)?.length ?? 0;
|
|
63
|
+
if (occurrences === 0) {
|
|
64
|
+
text = `${text.length > 0 && !text.endsWith("\n") ? `${text}\n` : text}${line}\n`;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
// Rewrite the FIRST occurrence in place — position is the user's and a
|
|
68
|
+
// byte-exact restore depends on keeping it — then drop any later ones.
|
|
69
|
+
//
|
|
70
|
+
// Dropping them is not tidiness. Gemini CLI's dotenv takes the LAST
|
|
71
|
+
// assignment: with a duplicate left in place our value is silently
|
|
72
|
+
// overridden and Gemini talks to Google while the writer reports success.
|
|
73
|
+
// Measured: a .env with the proxy URL first and a dead port second sent the
|
|
74
|
+
// request to the dead port.
|
|
75
|
+
let seen = 0;
|
|
62
76
|
// A function replacement, never a string: `String.replace` expands `$&`,
|
|
63
77
|
// `$1` and friends inside a replacement *string*, so a proxy key
|
|
64
78
|
// containing `$&` would be stored as the matched assignment instead of
|
|
65
|
-
// itself.
|
|
66
|
-
text =
|
|
67
|
-
|
|
68
|
-
|
|
79
|
+
// itself.
|
|
80
|
+
text = text.replace(all, (match) => {
|
|
81
|
+
seen += 1;
|
|
82
|
+
if (seen > 1) {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
// Re-emit the terminator that was matched, not an assumed LF. The regex
|
|
86
|
+
// captures `\r?\n`, and `"…\r\n".endsWith("\n")` is true, so testing for
|
|
87
|
+
// LF alone silently rewrote a CRLF line ending to LF — breaking the
|
|
88
|
+
// byte-exact round-trip this function's own contract promises, on the
|
|
89
|
+
// ordinary single-occurrence path rather than only on duplicates.
|
|
90
|
+
// Gemini CLI is cross-platform; a Windows-authored .env is not exotic.
|
|
91
|
+
const terminator = match.endsWith("\r\n")
|
|
92
|
+
? "\r\n"
|
|
93
|
+
: match.endsWith("\n")
|
|
94
|
+
? "\n"
|
|
95
|
+
: "";
|
|
96
|
+
return `${line}${terminator}`;
|
|
97
|
+
});
|
|
69
98
|
}
|
|
70
99
|
return text;
|
|
71
100
|
}
|
|
72
101
|
/**
|
|
73
|
-
* Read the managed variables' values out of an `.env` body.
|
|
102
|
+
* Read the managed variables' effective values out of an `.env` body.
|
|
74
103
|
*
|
|
75
104
|
* Restore needs the original *values*, not the original file: replaying a
|
|
76
105
|
* whole snapshot would discard everything the user changed after apply().
|
|
@@ -78,9 +107,14 @@ function upsertEnvVars(original, vars) {
|
|
|
78
107
|
function readManagedVars(envText) {
|
|
79
108
|
const out = {};
|
|
80
109
|
for (const key of [BASE_URL_VAR, API_KEY_VAR]) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
110
|
+
// The LAST assignment, because that is the one dotenv resolves to. Reading
|
|
111
|
+
// the first would restore a value the CLI never actually used.
|
|
112
|
+
const all = [
|
|
113
|
+
...envText.matchAll(new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}[ \\t]*=(.*)$`, "gm")),
|
|
114
|
+
];
|
|
115
|
+
const last = all[all.length - 1];
|
|
116
|
+
if (last) {
|
|
117
|
+
out[key] = last[1] ?? "";
|
|
84
118
|
}
|
|
85
119
|
}
|
|
86
120
|
return out;
|
|
@@ -89,7 +123,10 @@ function readManagedVars(envText) {
|
|
|
89
123
|
function removeEnvVars(original, keys) {
|
|
90
124
|
let text = original;
|
|
91
125
|
for (const key of keys) {
|
|
92
|
-
|
|
126
|
+
// Global: a duplicated key must be cleared everywhere. Removing only the
|
|
127
|
+
// first leaves a later assignment behind, and dotenv takes the LAST one —
|
|
128
|
+
// so a "restored" .env would still be pointing at the proxy.
|
|
129
|
+
const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}[ \\t]*=.*(?:\\r?\\n|$)`, "gm");
|
|
93
130
|
text = text.replace(re, "");
|
|
94
131
|
}
|
|
95
132
|
return text;
|
|
@@ -188,11 +225,16 @@ export async function clearGeminiProxySettings(expectedBaseUrl) {
|
|
|
188
225
|
catch {
|
|
189
226
|
return false;
|
|
190
227
|
}
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
// The EFFECTIVE assignment, not the first one on the page. dotenv resolves
|
|
229
|
+
// the last, so a user who appended their own base URL after apply() has
|
|
230
|
+
// already repointed Gemini — even though our line is still sitting above
|
|
231
|
+
// theirs. Checking the first saw our value, passed the ownership test, and
|
|
232
|
+
// restore then deleted the endpoint the CLI was actually using.
|
|
233
|
+
const configuredUrl = readManagedVars(current)[BASE_URL_VAR];
|
|
234
|
+
if (configuredUrl === undefined) {
|
|
193
235
|
return false;
|
|
194
236
|
}
|
|
195
|
-
if (expectedBaseUrl &&
|
|
237
|
+
if (expectedBaseUrl && configuredUrl.trim() !== expectedBaseUrl) {
|
|
196
238
|
// Pointed somewhere else — the user's choice, not ours to revert.
|
|
197
239
|
logger.debug("[proxy] Gemini clear: base URL is not the one we wrote, leaving it intact");
|
|
198
240
|
return false;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { handleSageMakerError, SageMakerError, isRetryableError, getRetryDelay, } from "./errors.js";
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
+
import { isAbortError } from "../../utils/errorHandling.js";
|
|
9
10
|
import { tryImport } from "../../utils/tryImport.js";
|
|
10
11
|
/**
|
|
11
12
|
* Lazily load `@aws-sdk/client-sagemaker-runtime`.
|
|
@@ -114,7 +115,13 @@ export class SageMakerRuntimeClient {
|
|
|
114
115
|
};
|
|
115
116
|
const command = new InvokeEndpointCommand(input);
|
|
116
117
|
const client = await this.getClient();
|
|
117
|
-
const response = (await this.executeWithRetry(
|
|
118
|
+
const response = (await this.executeWithRetry(
|
|
119
|
+
// The signal goes to the transport, not just to whatever loop is
|
|
120
|
+
// above us: without it an aborted call keeps its HTTP request in
|
|
121
|
+
// flight until the endpoint answers. An AbortError matches none of
|
|
122
|
+
// RETRYABLE_ERROR_CONDITIONS, so executeWithRetry surfaces it rather
|
|
123
|
+
// than re-issuing a request the caller has already abandoned.
|
|
124
|
+
() => client.send(command, { abortSignal: params.abortSignal }), params.EndpointName));
|
|
118
125
|
const duration = Date.now() - startTime;
|
|
119
126
|
logger.debug("SageMaker endpoint invocation successful", {
|
|
120
127
|
endpointName: params.EndpointName,
|
|
@@ -130,6 +137,15 @@ export class SageMakerRuntimeClient {
|
|
|
130
137
|
};
|
|
131
138
|
}
|
|
132
139
|
catch (error) {
|
|
140
|
+
// The signal now reaches the transport, so this catch sees real
|
|
141
|
+
// AbortErrors for the first time — and must not wrap them.
|
|
142
|
+
// SageMakerError's constructor overwrites `.name`, and its generic
|
|
143
|
+
// fallback stamps `statusCode: 500`, which the retry classifier reads as
|
|
144
|
+
// "transient, try again". That fabricated status is what turned a
|
|
145
|
+
// cancellation into three attempts and 22 seconds.
|
|
146
|
+
if (isAbortError(error)) {
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
133
149
|
const duration = Date.now() - startTime;
|
|
134
150
|
logger.error("SageMaker endpoint invocation failed", {
|
|
135
151
|
endpointName: params.EndpointName,
|
|
@@ -169,7 +185,10 @@ export class SageMakerRuntimeClient {
|
|
|
169
185
|
};
|
|
170
186
|
const command = new InvokeEndpointWithResponseStreamCommand(input);
|
|
171
187
|
const client = await this.getClient();
|
|
172
|
-
const response = (await this.executeWithRetry(
|
|
188
|
+
const response = (await this.executeWithRetry(
|
|
189
|
+
// As above — an abort must tear down the response stream's connection,
|
|
190
|
+
// not merely stop the consumer reading from it.
|
|
191
|
+
() => client.send(command, { abortSignal: params.abortSignal }), params.EndpointName));
|
|
173
192
|
logger.debug("SageMaker streaming invocation started", {
|
|
174
193
|
endpointName: params.EndpointName,
|
|
175
194
|
setupDuration: Date.now() - startTime,
|
|
@@ -192,6 +211,15 @@ export class SageMakerRuntimeClient {
|
|
|
192
211
|
};
|
|
193
212
|
}
|
|
194
213
|
catch (error) {
|
|
214
|
+
// The signal now reaches the transport, so the streaming catch sees real
|
|
215
|
+
// AbortErrors for the first time — and must not wrap them.
|
|
216
|
+
// SageMakerError's constructor overwrites `.name`, and its generic
|
|
217
|
+
// fallback stamps `statusCode: 500`, which the retry classifier reads as
|
|
218
|
+
// "transient, try again". That fabricated status is what turned a
|
|
219
|
+
// cancellation into three attempts and 22 seconds.
|
|
220
|
+
if (isAbortError(error)) {
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
195
223
|
const duration = Date.now() - startTime;
|
|
196
224
|
logger.error("SageMaker streaming invocation failed", {
|
|
197
225
|
endpointName: params.EndpointName,
|
|
@@ -34,6 +34,15 @@ export declare class SageMakerLanguageModel implements SageMakerAsLanguageModel
|
|
|
34
34
|
private config;
|
|
35
35
|
private modelConfig;
|
|
36
36
|
constructor(modelId: string, config: SageMakerConfig, modelConfig: SageMakerModelConfig);
|
|
37
|
+
/**
|
|
38
|
+
* Read the caller's abort signal out of the AI SDK's call options.
|
|
39
|
+
*
|
|
40
|
+
* `doGenerate`/`doStream` type `options` as `Record<string, unknown>`, so the
|
|
41
|
+
* value arrives as `unknown` even though `LanguageModelV2CallOptions`
|
|
42
|
+
* declares `abortSignal?: AbortSignal`. `instanceof` is a real runtime check
|
|
43
|
+
* rather than an assertion, which is what rule 14 asks for here.
|
|
44
|
+
*/
|
|
45
|
+
private readAbortSignal;
|
|
37
46
|
/**
|
|
38
47
|
* Generate text synchronously using SageMaker endpoint
|
|
39
48
|
*/
|
|
@@ -10,6 +10,7 @@ import { handleSageMakerError } from "./errors.js";
|
|
|
10
10
|
import { estimateTokenUsage, createSageMakerStream, parseUsageFromResponseBody, } from "./streaming.js";
|
|
11
11
|
import { createAdaptiveSemaphore } from "./adaptive-semaphore.js";
|
|
12
12
|
import { logger } from "../../utils/logger.js";
|
|
13
|
+
import { isAbortError } from "../../utils/errorHandling.js";
|
|
13
14
|
/**
|
|
14
15
|
* Base synthetic streaming delay in milliseconds for simulating real-time response
|
|
15
16
|
* Can be configured via SAGEMAKER_BASE_STREAMING_DELAY_MS environment variable
|
|
@@ -113,6 +114,18 @@ export class SageMakerLanguageModel {
|
|
|
113
114
|
specificationVersion: this.specificationVersion,
|
|
114
115
|
});
|
|
115
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Read the caller's abort signal out of the AI SDK's call options.
|
|
119
|
+
*
|
|
120
|
+
* `doGenerate`/`doStream` type `options` as `Record<string, unknown>`, so the
|
|
121
|
+
* value arrives as `unknown` even though `LanguageModelV2CallOptions`
|
|
122
|
+
* declares `abortSignal?: AbortSignal`. `instanceof` is a real runtime check
|
|
123
|
+
* rather than an assertion, which is what rule 14 asks for here.
|
|
124
|
+
*/
|
|
125
|
+
readAbortSignal(options) {
|
|
126
|
+
const signal = options.abortSignal;
|
|
127
|
+
return signal instanceof AbortSignal ? signal : undefined;
|
|
128
|
+
}
|
|
116
129
|
/**
|
|
117
130
|
* Generate text synchronously using SageMaker endpoint
|
|
118
131
|
*/
|
|
@@ -134,6 +147,7 @@ export class SageMakerLanguageModel {
|
|
|
134
147
|
Body: JSON.stringify(sagemakerRequest),
|
|
135
148
|
ContentType: "application/json",
|
|
136
149
|
Accept: "application/json",
|
|
150
|
+
abortSignal: this.readAbortSignal(options),
|
|
137
151
|
});
|
|
138
152
|
// Parse SageMaker response
|
|
139
153
|
const responseBody = JSON.parse(new TextDecoder().decode(response.Body));
|
|
@@ -223,6 +237,11 @@ export class SageMakerLanguageModel {
|
|
|
223
237
|
duration,
|
|
224
238
|
error: error instanceof Error ? error.message : String(error),
|
|
225
239
|
});
|
|
240
|
+
// An abort is not a provider failure: wrapping it here would restore
|
|
241
|
+
// the fabricated 500 the client guard just avoided.
|
|
242
|
+
if (isAbortError(error)) {
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
226
245
|
throw handleSageMakerError(error, this.modelConfig.endpointName);
|
|
227
246
|
}
|
|
228
247
|
}
|
|
@@ -260,6 +279,7 @@ export class SageMakerLanguageModel {
|
|
|
260
279
|
Body: JSON.stringify(requestWithStreaming),
|
|
261
280
|
ContentType: this.modelConfig.contentType || "application/json",
|
|
262
281
|
Accept: this.modelConfig.accept || "application/json",
|
|
282
|
+
abortSignal: this.readAbortSignal(options),
|
|
263
283
|
});
|
|
264
284
|
// Create intelligent streaming response
|
|
265
285
|
const stream = await createSageMakerStream(response.Body, this.modelConfig.endpointName, this.config, {
|
|
@@ -298,6 +318,13 @@ export class SageMakerLanguageModel {
|
|
|
298
318
|
};
|
|
299
319
|
}
|
|
300
320
|
catch (streamingError) {
|
|
321
|
+
// A cancelled turn is not a missing capability. This fallback exists
|
|
322
|
+
// for endpoints that cannot stream; re-issuing doGenerate() here with
|
|
323
|
+
// the same already-aborted signal only adds a wasted call and a
|
|
324
|
+
// misleading warning before the abort surfaces anyway.
|
|
325
|
+
if (isAbortError(streamingError)) {
|
|
326
|
+
throw streamingError;
|
|
327
|
+
}
|
|
301
328
|
logger.warn("Streaming failed, falling back to non-streaming", {
|
|
302
329
|
endpointName: this.modelConfig.endpointName,
|
|
303
330
|
error: streamingError instanceof Error
|
|
@@ -351,6 +378,11 @@ export class SageMakerLanguageModel {
|
|
|
351
378
|
logger.error("SageMaker doStream failed", {
|
|
352
379
|
error: error instanceof Error ? error.message : String(error),
|
|
353
380
|
});
|
|
381
|
+
// An abort is not a provider failure: wrapping it here would restore
|
|
382
|
+
// the fabricated 500 the client guard just avoided.
|
|
383
|
+
if (isAbortError(error)) {
|
|
384
|
+
throw error;
|
|
385
|
+
}
|
|
354
386
|
throw handleSageMakerError(error, this.modelConfig.endpointName);
|
|
355
387
|
}
|
|
356
388
|
}
|
|
@@ -681,6 +713,11 @@ export class SageMakerLanguageModel {
|
|
|
681
713
|
error: error instanceof Error ? error.message : String(error),
|
|
682
714
|
batchSize: prompts.length,
|
|
683
715
|
});
|
|
716
|
+
// An abort is not a provider failure: wrapping it here would restore
|
|
717
|
+
// the fabricated 500 the client guard just avoided.
|
|
718
|
+
if (isAbortError(error)) {
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
684
721
|
throw handleSageMakerError(error, this.modelConfig.endpointName);
|
|
685
722
|
}
|
|
686
723
|
}
|
|
@@ -1282,6 +1282,16 @@ export type InvokeEndpointParams = {
|
|
|
1282
1282
|
TargetVariant?: string;
|
|
1283
1283
|
/** Inference ID for request tracking */
|
|
1284
1284
|
InferenceId?: string;
|
|
1285
|
+
/**
|
|
1286
|
+
* Cancels the in-flight HTTP request, not just the loop around it.
|
|
1287
|
+
*
|
|
1288
|
+
* Named in camelCase deliberately: every other field here mirrors an AWS
|
|
1289
|
+
* `InvokeEndpointCommandInput` member and keeps its PascalCase, whereas this
|
|
1290
|
+
* one is a transport option handed to `client.send()` as
|
|
1291
|
+
* `@smithy/types` `HttpHandlerOptions` — it is never part of the command
|
|
1292
|
+
* payload, and spelling it differently keeps that boundary visible.
|
|
1293
|
+
*/
|
|
1294
|
+
abortSignal?: AbortSignal;
|
|
1285
1295
|
};
|
|
1286
1296
|
/**
|
|
1287
1297
|
* Response from SageMaker endpoint invocation
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.6.
|
|
3
|
+
"version": "12.6.1",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|