@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.
- package/CHANGELOG.md +3 -4
- package/dist/browser/neurolink.min.js +382 -382
- 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/proxy/clientAttribution.d.ts +40 -0
- package/dist/proxy/clientAttribution.js +28 -5
- package/dist/types/common.d.ts +87 -0
- package/dist/types/providers.d.ts +10 -0
- package/dist/types/voice.d.ts +13 -0
- package/dist/utils/ttsProcessor.d.ts +130 -5
- package/dist/utils/ttsProcessor.js +642 -97
- package/dist/utils/ttsStream.js +4 -0
- package/dist/voice/providers/OpenAITTS.d.ts +26 -8
- package/dist/voice/providers/OpenAITTS.js +327 -85
- 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
|
}
|
|
@@ -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
|
-
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* `unknown` rows in
|
|
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
|
/**
|
package/dist/types/common.d.ts
CHANGED
|
@@ -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
|
package/dist/types/voice.d.ts
CHANGED
|
@@ -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 */
|