@juspay/neurolink 11.25.4 → 11.26.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 +2 -4
- package/dist/browser/neurolink.min.js +380 -380
- package/dist/core/baseProvider.d.ts +5 -0
- package/dist/core/baseProvider.js +99 -54
- package/dist/factories/providerDescriptors.js +16 -0
- package/dist/models/modelResolver.js +28 -6
- package/dist/neurolink.d.ts +7 -20
- package/dist/neurolink.js +99 -85
- package/dist/routing/classifierRouter.js +18 -4
- package/dist/types/providers.d.ts +18 -0
- package/dist/types/stream.d.ts +44 -6
- package/dist/types/tts.d.ts +9 -0
- package/dist/types/tts.js +6 -0
- package/dist/utils/providerHealth.js +15 -20
- package/dist/utils/retryHandler.d.ts +0 -11
- package/dist/utils/retryHandler.js +6 -21
- package/dist/utils/ttsProcessor.d.ts +20 -1
- package/dist/utils/ttsProcessor.js +167 -0
- package/dist/utils/ttsStream.d.ts +15 -0
- package/dist/utils/ttsStream.js +225 -0
- package/package.json +3 -2
|
@@ -22,6 +22,75 @@ export const TTS_ERROR_CODES = {
|
|
|
22
22
|
SYNTHESIS_FAILED: "TTS_SYNTHESIS_FAILED",
|
|
23
23
|
INVALID_INPUT: "TTS_INVALID_INPUT",
|
|
24
24
|
};
|
|
25
|
+
const DEFAULT_STREAMING_BUFFER_SIZE = 120;
|
|
26
|
+
const SENTENCE_BOUNDARY = /[.!?]+(?:["')\]]+)?(?=\s|$)/g;
|
|
27
|
+
/** Internal signal raised after all buffered segments have been attempted. */
|
|
28
|
+
export class IncrementalTTSSynthesisError extends Error {
|
|
29
|
+
firstError;
|
|
30
|
+
failedSegments;
|
|
31
|
+
constructor(firstError, failedSegments) {
|
|
32
|
+
super(`Incremental TTS failed for ${failedSegments.length} segment${failedSegments.length === 1 ? "" : "s"}`);
|
|
33
|
+
this.name = "IncrementalTTSSynthesisError";
|
|
34
|
+
this.firstError = firstError;
|
|
35
|
+
this.failedSegments = [...failedSegments];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function findSentenceEnds(text) {
|
|
39
|
+
const ends = [];
|
|
40
|
+
for (const match of text.matchAll(SENTENCE_BOUNDARY)) {
|
|
41
|
+
ends.push((match.index ?? 0) + match[0].length);
|
|
42
|
+
}
|
|
43
|
+
return ends;
|
|
44
|
+
}
|
|
45
|
+
const HIGH_SURROGATE_START = 0xd800;
|
|
46
|
+
const HIGH_SURROGATE_END = 0xdbff;
|
|
47
|
+
const LOW_SURROGATE_START = 0xdc00;
|
|
48
|
+
const LOW_SURROGATE_END = 0xdfff;
|
|
49
|
+
/**
|
|
50
|
+
* Move a split index off the middle of a surrogate pair.
|
|
51
|
+
*
|
|
52
|
+
* The cap is measured in UTF-16 code units, so a hard split can land between
|
|
53
|
+
* the two halves of an astral character (emoji, rarer CJK). That would end one
|
|
54
|
+
* segment with a lone high surrogate and start the next with its low half, and
|
|
55
|
+
* providers receive U+FFFD instead of the character. Backing the split off by
|
|
56
|
+
* one code unit keeps the pair whole in the next segment.
|
|
57
|
+
*
|
|
58
|
+
* A split at index 1 is left alone: backing off would yield an empty segment
|
|
59
|
+
* with an unchanged remainder, and a cap that small cannot hold the pair anyway.
|
|
60
|
+
*/
|
|
61
|
+
function avoidSurrogateSplit(text, splitAt) {
|
|
62
|
+
if (splitAt <= 1 || splitAt >= text.length) {
|
|
63
|
+
return splitAt;
|
|
64
|
+
}
|
|
65
|
+
const high = text.charCodeAt(splitAt - 1);
|
|
66
|
+
const low = text.charCodeAt(splitAt);
|
|
67
|
+
const splitsPair = high >= HIGH_SURROGATE_START &&
|
|
68
|
+
high <= HIGH_SURROGATE_END &&
|
|
69
|
+
low >= LOW_SURROGATE_START &&
|
|
70
|
+
low <= LOW_SURROGATE_END;
|
|
71
|
+
return splitsPair ? splitAt - 1 : splitAt;
|
|
72
|
+
}
|
|
73
|
+
function takeBufferedSegment(buffer, flushBoundary, maxTextLength, inputComplete) {
|
|
74
|
+
const cappedText = buffer.slice(0, maxTextLength);
|
|
75
|
+
const sentenceEnds = findSentenceEnds(cappedText);
|
|
76
|
+
let splitAt = buffer.length >= flushBoundary ? sentenceEnds.at(-1) : undefined;
|
|
77
|
+
if (splitAt === undefined && buffer.length >= maxTextLength) {
|
|
78
|
+
splitAt = sentenceEnds.at(-1) ?? maxTextLength;
|
|
79
|
+
}
|
|
80
|
+
if (splitAt === undefined && inputComplete && buffer.trim()) {
|
|
81
|
+
splitAt = Math.min(buffer.length, maxTextLength);
|
|
82
|
+
}
|
|
83
|
+
if (splitAt === undefined) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
splitAt = avoidSurrogateSplit(buffer, splitAt);
|
|
87
|
+
const segment = buffer.slice(0, splitAt).trim();
|
|
88
|
+
const remainder = buffer.slice(splitAt).trimStart();
|
|
89
|
+
if (!segment) {
|
|
90
|
+
return { segment: "", remainder };
|
|
91
|
+
}
|
|
92
|
+
return { segment, remainder };
|
|
93
|
+
}
|
|
25
94
|
/**
|
|
26
95
|
* TTS Error class for text-to-speech specific errors
|
|
27
96
|
*/
|
|
@@ -289,4 +358,102 @@ export class TTSProcessor {
|
|
|
289
358
|
});
|
|
290
359
|
}
|
|
291
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Incrementally synthesize sentence-buffered text chunks.
|
|
363
|
+
*
|
|
364
|
+
* Text is flushed at a sentence boundary after `streamingBufferSize`
|
|
365
|
+
* characters, or hard-split before the provider's maximum text length.
|
|
366
|
+
* Each segment goes through `synthesize()`, preserving the existing handler
|
|
367
|
+
* registry, validation, error normalization, and telemetry seam.
|
|
368
|
+
*
|
|
369
|
+
* The most recent successful audio chunk is held until another succeeds or
|
|
370
|
+
* the input ends, so exactly one real audio chunk carries `isFinal: true`
|
|
371
|
+
* without emitting a separate empty terminator chunk.
|
|
372
|
+
*/
|
|
373
|
+
static async *synthesizeStream(textChunks, provider, options, shouldStop) {
|
|
374
|
+
const handler = this.getHandler(provider);
|
|
375
|
+
const maxTextLength = Math.max(1, handler?.maxTextLength ?? this.DEFAULT_MAX_TEXT_LENGTH);
|
|
376
|
+
const requestedBoundary = options.streamingBufferSize ?? DEFAULT_STREAMING_BUFFER_SIZE;
|
|
377
|
+
const flushBoundary = Math.min(Math.max(1, Math.trunc(requestedBoundary)), maxTextLength);
|
|
378
|
+
let buffer = "";
|
|
379
|
+
let chunkIndex = 0;
|
|
380
|
+
let cumulativeSize = 0;
|
|
381
|
+
let cumulativeDuration = 0;
|
|
382
|
+
let pendingChunk;
|
|
383
|
+
let segmentNumber = 0;
|
|
384
|
+
let firstFailure;
|
|
385
|
+
const failedSegments = [];
|
|
386
|
+
const synthesizeSegment = async (segment) => {
|
|
387
|
+
const currentSegment = ++segmentNumber;
|
|
388
|
+
try {
|
|
389
|
+
const result = await this.synthesize(segment, provider, options);
|
|
390
|
+
cumulativeSize += result.size;
|
|
391
|
+
cumulativeDuration += result.duration ?? 0;
|
|
392
|
+
return {
|
|
393
|
+
data: result.buffer,
|
|
394
|
+
format: result.format,
|
|
395
|
+
index: chunkIndex++,
|
|
396
|
+
isFinal: false,
|
|
397
|
+
cumulativeSize,
|
|
398
|
+
estimatedDuration: cumulativeDuration || undefined,
|
|
399
|
+
voice: result.voice,
|
|
400
|
+
sampleRate: result.sampleRate,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
if (failedSegments.length === 0) {
|
|
405
|
+
firstFailure = error;
|
|
406
|
+
}
|
|
407
|
+
failedSegments.push(currentSegment);
|
|
408
|
+
logger.warn(`[TTSProcessor] Incremental synthesis skipped a buffered segment: ${error instanceof Error ? error.message : String(error)}`);
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
for await (const textChunk of textChunks) {
|
|
413
|
+
if (shouldStop?.()) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
buffer += textChunk;
|
|
417
|
+
while (!shouldStop?.()) {
|
|
418
|
+
const buffered = takeBufferedSegment(buffer, flushBoundary, maxTextLength, false);
|
|
419
|
+
if (!buffered) {
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
buffer = buffered.remainder;
|
|
423
|
+
if (!buffered.segment) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const chunk = await synthesizeSegment(buffered.segment);
|
|
427
|
+
if (chunk) {
|
|
428
|
+
if (pendingChunk) {
|
|
429
|
+
yield pendingChunk;
|
|
430
|
+
}
|
|
431
|
+
pendingChunk = chunk;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
while (!shouldStop?.()) {
|
|
436
|
+
const buffered = takeBufferedSegment(buffer, flushBoundary, maxTextLength, true);
|
|
437
|
+
if (!buffered) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
buffer = buffered.remainder;
|
|
441
|
+
if (!buffered.segment) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const chunk = await synthesizeSegment(buffered.segment);
|
|
445
|
+
if (chunk) {
|
|
446
|
+
if (pendingChunk) {
|
|
447
|
+
yield pendingChunk;
|
|
448
|
+
}
|
|
449
|
+
pendingChunk = chunk;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (pendingChunk) {
|
|
453
|
+
yield { ...pendingChunk, isFinal: true };
|
|
454
|
+
}
|
|
455
|
+
if (failedSegments.length > 0) {
|
|
456
|
+
throw new IncrementalTTSSynthesisError(firstFailure, failedSegments);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
292
459
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TTSChunk, TTSMetadata, TTSOptions, TTSResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Preserve source-stream backpressure while interleaving incremental TTS audio.
|
|
4
|
+
* Source chunks are yielded before their derived audio, and TTS failures degrade
|
|
5
|
+
* to the unchanged source stream.
|
|
6
|
+
*/
|
|
7
|
+
export declare function interleaveTTSStream<T>(params: {
|
|
8
|
+
stream: AsyncIterable<T>;
|
|
9
|
+
provider: string;
|
|
10
|
+
options: TTSOptions;
|
|
11
|
+
onComplete?: (result: TTSResult | undefined, error?: NonNullable<TTSMetadata["error"]>) => void;
|
|
12
|
+
}): AsyncGenerator<T | {
|
|
13
|
+
type: "tts_audio";
|
|
14
|
+
audio: TTSChunk;
|
|
15
|
+
}>;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { NeuroLinkError } from "./errorHandling.js";
|
|
2
|
+
import { sanitizeErrorCause } from "./logSanitize.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { IncrementalTTSSynthesisError, TTS_ERROR_CODES, TTSProcessor, } from "./ttsProcessor.js";
|
|
5
|
+
import { TimeoutError as AsyncTimeoutError } from "./async/withTimeout.js";
|
|
6
|
+
function getStreamingTTSErrorDetails(error) {
|
|
7
|
+
const incrementalFailure = error instanceof IncrementalTTSSynthesisError ? error : undefined;
|
|
8
|
+
const cause = incrementalFailure?.firstError ?? error;
|
|
9
|
+
const safeMessage = sanitizeErrorCause(cause).message;
|
|
10
|
+
let detail;
|
|
11
|
+
if (cause instanceof AsyncTimeoutError) {
|
|
12
|
+
detail = {
|
|
13
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
14
|
+
message: safeMessage,
|
|
15
|
+
retriable: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
else if (cause instanceof NeuroLinkError) {
|
|
19
|
+
detail = {
|
|
20
|
+
code: cause.code,
|
|
21
|
+
message: safeMessage,
|
|
22
|
+
retriable: cause.retriable,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
detail = {
|
|
27
|
+
code: TTS_ERROR_CODES.SYNTHESIS_FAILED,
|
|
28
|
+
message: safeMessage,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const failedSegments = incrementalFailure?.failedSegments;
|
|
32
|
+
const count = failedSegments?.length ?? 1;
|
|
33
|
+
const positions = failedSegments?.join(", ") ?? "unknown";
|
|
34
|
+
return {
|
|
35
|
+
...detail,
|
|
36
|
+
message: `Incremental TTS failed for ${count} segment${count === 1 ? "" : "s"} (segment${count === 1 ? "" : "s"} ${positions}): ${detail.message}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
class AsyncTextQueue {
|
|
40
|
+
values = [];
|
|
41
|
+
waiters = [];
|
|
42
|
+
ended = false;
|
|
43
|
+
failure;
|
|
44
|
+
[Symbol.asyncIterator]() {
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
next() {
|
|
48
|
+
if (this.values.length > 0) {
|
|
49
|
+
const value = this.values.shift();
|
|
50
|
+
if (value !== undefined) {
|
|
51
|
+
return Promise.resolve({ value, done: false });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (this.failure !== undefined) {
|
|
55
|
+
return Promise.reject(this.failure);
|
|
56
|
+
}
|
|
57
|
+
if (this.ended) {
|
|
58
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
59
|
+
}
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
this.waiters.push({ resolve, reject });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
push(value) {
|
|
65
|
+
if (this.ended || this.failure !== undefined) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const waiter = this.waiters.shift();
|
|
69
|
+
if (waiter) {
|
|
70
|
+
waiter.resolve({ value, done: false });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.values.push(value);
|
|
74
|
+
}
|
|
75
|
+
end() {
|
|
76
|
+
if (this.ended || this.failure !== undefined) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.ended = true;
|
|
80
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
81
|
+
waiter.resolve({ value: undefined, done: true });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
fail(error) {
|
|
85
|
+
if (this.ended || this.failure !== undefined) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.failure = error;
|
|
89
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
90
|
+
waiter.reject(error);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function sourceEvent(iterator) {
|
|
95
|
+
return iterator.next().then((result) => ({ kind: "source", result }), (error) => ({ kind: "source-error", error }));
|
|
96
|
+
}
|
|
97
|
+
function audioEvent(iterator) {
|
|
98
|
+
return iterator.next().then((result) => ({ kind: "audio", result }), (error) => ({ kind: "audio-error", error }));
|
|
99
|
+
}
|
|
100
|
+
function textFromChunk(chunk) {
|
|
101
|
+
if (chunk &&
|
|
102
|
+
typeof chunk === "object" &&
|
|
103
|
+
"content" in chunk &&
|
|
104
|
+
typeof chunk.content === "string" &&
|
|
105
|
+
chunk.content.length > 0) {
|
|
106
|
+
return chunk.content;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function aggregateTTSChunks(chunks) {
|
|
111
|
+
const last = chunks.at(-1);
|
|
112
|
+
if (!last) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
const buffer = Buffer.concat(chunks.map((chunk) => chunk.data));
|
|
116
|
+
return {
|
|
117
|
+
buffer,
|
|
118
|
+
format: last.format,
|
|
119
|
+
size: buffer.length,
|
|
120
|
+
duration: last.estimatedDuration,
|
|
121
|
+
voice: last.voice,
|
|
122
|
+
sampleRate: last.sampleRate,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Preserve source-stream backpressure while interleaving incremental TTS audio.
|
|
127
|
+
* Source chunks are yielded before their derived audio, and TTS failures degrade
|
|
128
|
+
* to the unchanged source stream.
|
|
129
|
+
*/
|
|
130
|
+
export async function* interleaveTTSStream(params) {
|
|
131
|
+
const { stream, provider, options, onComplete } = params;
|
|
132
|
+
if (!TTSProcessor.supports(provider)) {
|
|
133
|
+
try {
|
|
134
|
+
for await (const chunk of stream) {
|
|
135
|
+
yield chunk;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
onComplete?.(undefined);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const textQueue = new AsyncTextQueue();
|
|
144
|
+
const sourceIterator = stream[Symbol.asyncIterator]();
|
|
145
|
+
let cancelled = false;
|
|
146
|
+
const audioIterator = TTSProcessor.synthesizeStream(textQueue, provider, options, () => cancelled)[Symbol.asyncIterator]();
|
|
147
|
+
const audioChunks = [];
|
|
148
|
+
let nextSource = sourceEvent(sourceIterator);
|
|
149
|
+
let nextAudio = audioEvent(audioIterator);
|
|
150
|
+
let completed = false;
|
|
151
|
+
let preferAudio = false;
|
|
152
|
+
try {
|
|
153
|
+
while (nextSource || nextAudio) {
|
|
154
|
+
const event = nextSource && nextAudio
|
|
155
|
+
? await Promise.race(preferAudio ? [nextAudio, nextSource] : [nextSource, nextAudio])
|
|
156
|
+
: nextSource
|
|
157
|
+
? await nextSource
|
|
158
|
+
: nextAudio
|
|
159
|
+
? await nextAudio
|
|
160
|
+
: undefined;
|
|
161
|
+
if (!event) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
if (event.kind === "source-error") {
|
|
165
|
+
textQueue.fail(event.error);
|
|
166
|
+
throw event.error;
|
|
167
|
+
}
|
|
168
|
+
if (event.kind === "audio-error") {
|
|
169
|
+
textQueue.end();
|
|
170
|
+
const error = getStreamingTTSErrorDetails(event.error);
|
|
171
|
+
logger.warn(`[TTSProcessor] Incremental stream disabled after an audio error: ${event.error instanceof Error
|
|
172
|
+
? event.error.message
|
|
173
|
+
: String(event.error)}`);
|
|
174
|
+
nextAudio = undefined;
|
|
175
|
+
preferAudio = false;
|
|
176
|
+
completed = true;
|
|
177
|
+
onComplete?.(aggregateTTSChunks(audioChunks), error);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (event.kind === "source") {
|
|
181
|
+
if (event.result.done) {
|
|
182
|
+
nextSource = undefined;
|
|
183
|
+
textQueue.end();
|
|
184
|
+
preferAudio = true;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const text = textFromChunk(event.result.value);
|
|
188
|
+
if (text) {
|
|
189
|
+
textQueue.push(text);
|
|
190
|
+
}
|
|
191
|
+
nextSource = sourceEvent(sourceIterator);
|
|
192
|
+
preferAudio = true;
|
|
193
|
+
yield event.result.value;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (event.result.done) {
|
|
197
|
+
nextAudio = undefined;
|
|
198
|
+
completed = true;
|
|
199
|
+
onComplete?.(aggregateTTSChunks(audioChunks));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
audioChunks.push(event.result.value);
|
|
203
|
+
nextAudio = audioEvent(audioIterator);
|
|
204
|
+
preferAudio = false;
|
|
205
|
+
yield { type: "tts_audio", audio: event.result.value };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
if (!completed) {
|
|
210
|
+
cancelled = true;
|
|
211
|
+
}
|
|
212
|
+
textQueue.end();
|
|
213
|
+
const releases = [];
|
|
214
|
+
if (nextSource && sourceIterator.return) {
|
|
215
|
+
releases.push(Promise.resolve().then(() => sourceIterator.return?.(undefined)));
|
|
216
|
+
}
|
|
217
|
+
if (nextAudio && audioIterator.return) {
|
|
218
|
+
releases.push(Promise.resolve().then(() => audioIterator.return?.(undefined)));
|
|
219
|
+
}
|
|
220
|
+
await Promise.allSettled(releases);
|
|
221
|
+
if (!completed) {
|
|
222
|
+
onComplete?.(undefined);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.26.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": {
|
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
"// CI tier — fast, no live AI calls, safe for every commit (test:unit; also see the separate provider-safety-net CI job, which runs build + test:providers-mocked + test:provider-structure + test:error-classifier-contract on every PR)": "",
|
|
143
143
|
"test:tool-routing": "pnpm exec tsx test/continuous-test-suite-tool-routing.ts",
|
|
144
144
|
"test:tool-routing-semantic": "pnpm exec tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
145
|
-
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
|
|
145
|
+
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:spans && pnpm run test:tool-routing && pnpm run test:tool-routing-cli && pnpm run test:tool-dedup && pnpm run test:model-pool && pnpm run test:classifier-router && pnpm run test:tool-routing-semantic && pnpm run test:mcp-result-cache && pnpm run test:model-not-found-retryable && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:vector-chroma && pnpm run test:vector-pgvector && pnpm run test:vector-pinecone && pnpm run test:provider-wiring && pnpm run test:docs-mcp",
|
|
146
146
|
"// CI tier — live providers, runs only when API keys are present (test:credentials and test:dynamic make real provider calls when keys are set, so they live here, not in test:unit; test:matrix, a different suite covering the full provider capability matrix, runs nightly via .github/workflows/live-matrix.yml — test:providers itself is still only wired into test:live, not any GitHub Actions workflow)": "",
|
|
147
147
|
"test:live": "pnpm run test:providers && pnpm run test:mcp:http && pnpm run test:mcp:sdk && pnpm run test:mcp:cli && pnpm run test:observability && pnpm run test:context && pnpm run test:memory && pnpm run test:tool-reliability && pnpm run test:evaluation && pnpm run test:autoresearch && pnpm run test:credentials && pnpm run test:dynamic",
|
|
148
148
|
"// CI tier — product output (image/video/TTS/PPT) — costs $$ per run (not wired into any GitHub Actions workflow as of this comment; run manually or add to live-matrix.yml if nightly coverage is needed)": "",
|
|
@@ -207,6 +207,7 @@
|
|
|
207
207
|
"test:archive:security": "pnpm exec tsx test/continuous-test-suite-archive-security.ts",
|
|
208
208
|
"test:office:security": "pnpm exec tsx test/continuous-test-suite-office-security.ts",
|
|
209
209
|
"test:model-pool": "pnpm exec tsx test/continuous-test-suite-model-pool.ts",
|
|
210
|
+
"test:classifier-router": "pnpm exec tsx test/continuous-test-suite-classifier-router.ts",
|
|
210
211
|
"test:vector-chroma": "pnpm exec tsx test/continuous-test-suite-vector-chroma.ts",
|
|
211
212
|
"test:vector-pgvector": "pnpm exec tsx test/continuous-test-suite-vector-pgvector.ts",
|
|
212
213
|
"test:vector-pinecone": "pnpm exec tsx test/continuous-test-suite-vector-pinecone.ts",
|