@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.
@@ -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. The callback form treats the value as literal.
66
- text = re.test(text)
67
- ? text.replace(re, () => line)
68
- : `${text.length > 0 && !text.endsWith("\n") ? `${text}\n` : text}${line}\n`;
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
- const m = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}[ \\t]*=(.*)$`, "m").exec(envText);
82
- if (m) {
83
- out[key] = m[1] ?? "";
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
- const re = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${key}[ \\t]*=.*(?:\\r?\\n|$)`, "m");
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
- const configured = new RegExp(`^[ \\t]*(?:export[ \\t]+)?${BASE_URL_VAR}[ \\t]*=[ \\t]*(.*)$`, "m").exec(current);
192
- if (!configured) {
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 && configured[1]?.trim() !== 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(() => client.send(command), params.EndpointName));
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(() => client.send(command), params.EndpointName));
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.0",
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": {