@juspay/neurolink 12.6.0 → 12.7.0

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
  }
@@ -16,6 +16,46 @@
16
16
  * client is then still attributable by its own User-Agent rather than
17
17
  * collapsing into one bucket with every other unknown.
18
18
  */
19
+ /**
20
+ * Deliberately NOT mapped, with the measurement that ruled each one out.
21
+ *
22
+ * Copilot CLI is the one client here that cannot be identified from its
23
+ * User-Agent, and both of the strings it sends are actively unsafe to key on:
24
+ *
25
+ * - `OpenAI/JS 5.20.1` — the stock OpenAI JS SDK UA, sent by every caller of
26
+ * that SDK. Mapping it to Copilot would file unrelated OpenAI-SDK traffic
27
+ * under Copilot's name, which is worse than leaving it unattributed.
28
+ - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — not a
29
+ * CLI's User-Agent at all. This is what `curl` sends on a machine whose
30
+ * `~/.curlrc` sets `user-agent`, so every curl-driven caller on such a host
31
+ * shares it: scripts, agents, health probes. It accounted for the largest
32
+ * single block of `unknown` rows in the log this table was measured against,
33
+ * which is exactly what made it tempting.
34
+ *
35
+ * Recorded because the first pass got this wrong in a way worth naming. The
36
+ * string was seen arriving at two capture servers during Copilot CLI and
37
+ * Gemini CLI runs and was written up as "sent by both CLIs". It was neither:
38
+ * it was the aliveness `curl` fired at each capture server moments before
39
+ * the CLI, picking up that host's curlrc. The paths gave it away on review —
40
+ * the Gemini-side hit was a bare `GET /v1beta/models`, which is the probe's
41
+ * URL and not one the CLI requests. A shared string across two unrelated
42
+ * clients should have read as "shared dependency or shared tooling", not as
43
+ * a property of either client.
44
+ *
45
+ * Copilot does send `x-initiator` and `x-interaction-type`, but neither is
46
+ * exclusive to it either. Attributing it needs a signal nobody has found yet,
47
+ * so it stays `unknown` and remains traceable through the stored raw header.
48
+ */
49
+ /**
50
+ * The client names this table can produce.
51
+ *
52
+ * Exported so a test can check the roster of CLIs the proxy configures against
53
+ * the roster it can actually name. Those two lists drifted apart silently once
54
+ * already: five configurators shipped while the table still knew only Claude
55
+ * Code, and nothing failed, because an unattributed client looks exactly like
56
+ * a quiet one.
57
+ */
58
+ export declare function getMappedClientNames(): ReadonlySet<string>;
19
59
  /**
20
60
  * Derive a stable client name, or "unknown" when the header is absent or
21
61
  * unrecognised. Never throws: attribution must not be able to fail a request.
@@ -49,16 +49,39 @@ const CLIENT_PREFIXES = [
49
49
  * - `OpenAI/JS 5.20.1` — the stock OpenAI JS SDK UA, sent by every caller of
50
50
  * that SDK. Mapping it to Copilot would file unrelated OpenAI-SDK traffic
51
51
  * under Copilot's name, which is worse than leaving it unattributed.
52
- * - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — a
53
- * spoofed browser string. It looks like a fingerprint and is not one: it was
54
- * captured from **both** Copilot CLI and Gemini CLI, so it identifies no
55
- * client at all. (It also accounts for the largest single block of
56
- * `unknown` rows in this machine's log, which is what made it tempting.)
52
+ - `Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)` — not a
53
+ * CLI's User-Agent at all. This is what `curl` sends on a machine whose
54
+ * `~/.curlrc` sets `user-agent`, so every curl-driven caller on such a host
55
+ * shares it: scripts, agents, health probes. It accounted for the largest
56
+ * single block of `unknown` rows in the log this table was measured against,
57
+ * which is exactly what made it tempting.
58
+ *
59
+ * Recorded because the first pass got this wrong in a way worth naming. The
60
+ * string was seen arriving at two capture servers during Copilot CLI and
61
+ * Gemini CLI runs and was written up as "sent by both CLIs". It was neither:
62
+ * it was the aliveness `curl` fired at each capture server moments before
63
+ * the CLI, picking up that host's curlrc. The paths gave it away on review —
64
+ * the Gemini-side hit was a bare `GET /v1beta/models`, which is the probe's
65
+ * URL and not one the CLI requests. A shared string across two unrelated
66
+ * clients should have read as "shared dependency or shared tooling", not as
67
+ * a property of either client.
57
68
  *
58
69
  * Copilot does send `x-initiator` and `x-interaction-type`, but neither is
59
70
  * exclusive to it either. Attributing it needs a signal nobody has found yet,
60
71
  * so it stays `unknown` and remains traceable through the stored raw header.
61
72
  */
73
+ /**
74
+ * The client names this table can produce.
75
+ *
76
+ * Exported so a test can check the roster of CLIs the proxy configures against
77
+ * the roster it can actually name. Those two lists drifted apart silently once
78
+ * already: five configurators shipped while the table still knew only Claude
79
+ * Code, and nothing failed, because an unattributed client looks exactly like
80
+ * a quiet one.
81
+ */
82
+ export function getMappedClientNames() {
83
+ return new Set(CLIENT_PREFIXES.map(([, name]) => name));
84
+ }
62
85
  /** Cap stored User-Agents. They are attacker-influenced and unbounded. */
63
86
  const MAX_USER_AGENT_CHARS = 200;
64
87
  /**
@@ -352,6 +352,93 @@ export type TTSHandler = {
352
352
  * @throws {TTSError} On synthesis failure, timeout, or configuration issues
353
353
  */
354
354
  synthesize(text: string, options: TTSOptions): Promise<TTSResult>;
355
+ /**
356
+ * Stream provider-native audio for one pre-validated text segment.
357
+ *
358
+ * Return `undefined` when the requested options cannot be delivered
359
+ * incrementally. The processor then uses `synthesize()` and preserves the
360
+ * buffered fallback for handlers and formats without native support.
361
+ * Provider-local indexes, cumulative sizes, and finality are normalized by
362
+ * the processor before chunks reach the public stream — an implementation
363
+ * may leave `isFinal` false on every chunk rather than hold a fragment back
364
+ * to label the last one, and the processor discards reported finality
365
+ * either way.
366
+ *
367
+ * Yield `TTSChunk` fragments. The member is declared `unknown` — not a
368
+ * method signature — on purpose, and that is a deliberate trade.
369
+ *
370
+ * This is an OPTIONAL member added to a public structural type that
371
+ * consumers already implement. Any type narrower than `unknown` rejects some
372
+ * existing handler that already carries a member of this name, which is a
373
+ * source break under Critical Rule 5 whatever that other shape happens to
374
+ * be. That is not hypothetical: a member returning a sync `Generator`, an
375
+ * `async` method returning a `Promise` of an async iterable, a
376
+ * callback-style member returning `void` or `Promise<void>`, and a plain
377
+ * boolean capability flag all compile against `origin/release` today, and
378
+ * every one of them is rejected by a declared method signature — including
379
+ * an intentionally wide one such as `(...args: never[]) => unknown`, which
380
+ * still cannot accept the boolean. Only `unknown` accepts them all.
381
+ *
382
+ * The cost is that this member cannot contextually type an implementation's
383
+ * parameters. Authors annotate their own signature instead — OpenAI TTS
384
+ * declares `synthesizeStream(text: string, options: TTSOptions):
385
+ * AsyncIterable<TTSChunk> | undefined` on the class — which keeps full
386
+ * compiler checking of what that implementation yields. Nothing is checked
387
+ * at this member; usability is decided at runtime by the processor.
388
+ *
389
+ * The processor validates each fragment at runtime instead. A fragment is
390
+ * audio only when it carries a non-empty `data` `Buffer` (or `Uint8Array`);
391
+ * its `format` is honoured only when it names a real `TTSAudioFormat`, and
392
+ * the requested format is used otherwise. A fragment that fails that test —
393
+ * including an empty (`data.length === 0`) read — is skipped and never
394
+ * reaches the consumer. A native stream that completes without yielding a
395
+ * single deliverable fragment is treated as "no incremental delivery after
396
+ * all" and falls back to `synthesize()` for that segment.
397
+ *
398
+ * That filtering is specific to this native path. The buffered path
399
+ * forwards whatever `synthesize()` returns, a zero-byte buffer included, so
400
+ * a consumer of the public stream can still observe an empty chunk when a
401
+ * handler produces one.
402
+ *
403
+ * `undefined` is the only capability signal. Everything the processor does
404
+ * to decide whether this capability exists is asked BEFORE the segment's
405
+ * work starts, and every way that question can fail is a handler bug that
406
+ * the processor answers the same way: it serves that segment from
407
+ * `synthesize()` rather than losing it (the throwing modes also log a
408
+ * warning; a member that is merely not callable falls back silently). None
409
+ * of them should be used as a deliberate fallback mechanism. The modes it
410
+ * anticipates cover each read as well as each call, because reading a
411
+ * property can run a getter or a `Proxy` trap that throws just as a call
412
+ * can:
413
+ *
414
+ * - reading `synthesizeStream` off the handler throws;
415
+ * - the member is present but not callable;
416
+ * - reading `isConfigured` off the handler throws, or calling it throws —
417
+ * the segment falls back to the buffered path, where `synthesize()` asks
418
+ * again and a throw there fails the segment shaped, exactly as it would
419
+ * for a handler with no native member (a handler that merely reports
420
+ * itself unconfigured is not a bug: that segment fails with
421
+ * `TTS_PROVIDER_NOT_CONFIGURED`, as it always has);
422
+ * - calling the member throws;
423
+ * - the returned value's async-iterability cannot be established, including
424
+ * a value whose `Symbol.asyncIterator` property cannot even be read.
425
+ *
426
+ * An error raised once the segment's own work has started, by contrast,
427
+ * fails that segment like any other synthesis failure — it is not re-served
428
+ * from the buffered path. Iteration begins at the `[Symbol.asyncIterator]()`
429
+ * call, so that call throwing, that call handing back something that is not
430
+ * an iterator, and a first read that rejects are all segment failures rather
431
+ * than fallbacks.
432
+ *
433
+ * Implementations MUST enforce their own timeout and cancel any active
434
+ * transport when the returned iterable is closed.
435
+ *
436
+ * The value the processor looks for is a callable of the shape
437
+ * `(text: string, options: TTSOptions) => AsyncIterable<TTSChunk> | undefined`,
438
+ * invoked with the handler as `this`. `text` is one buffered text segment
439
+ * within the provider's length limit.
440
+ */
441
+ synthesizeStream?: unknown;
355
442
  /**
356
443
  * Get available voices for the provider
357
444
  *
@@ -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
@@ -134,6 +134,14 @@ export type VoiceTurn = {
134
134
  };
135
135
  /**
136
136
  * TTS-capable voice provider type
137
+ *
138
+ * @deprecated Use the canonical `TTSHandler` contract instead. Nothing in
139
+ * this package consumes `TTSProvider`; it is kept at its original shape so
140
+ * existing external callers keep compiling. `TTSHandler` is not a drop-in
141
+ * replacement — it requires `isConfigured()`, makes `getVoices` and
142
+ * `maxTextLength` optional, and its `synthesizeStream` may return `undefined`
143
+ * to select the buffered path — so this is a distinct legacy shape, not an
144
+ * alias.
137
145
  */
138
146
  export type TTSProvider = {
139
147
  /**
@@ -155,6 +163,11 @@ export type TTSProvider = {
155
163
  };
156
164
  /**
157
165
  * TTS stream chunk for streaming synthesis
166
+ *
167
+ * @deprecated Use the canonical `TTSChunk` type instead. Kept at its
168
+ * original shape so existing external callers keep compiling: `TTSChunk`
169
+ * narrows `format` to `TTSAudioFormat` and has no `timestampMs`, so it
170
+ * is not a drop-in replacement.
158
171
  */
159
172
  export type TTSStreamChunk = {
160
173
  /** Audio data chunk */