@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
|
@@ -72,6 +72,11 @@ export declare abstract class BaseProvider implements AIProvider {
|
|
|
72
72
|
* that supplied `onError`).
|
|
73
73
|
*/
|
|
74
74
|
private fireLifecycleErrorCallback;
|
|
75
|
+
/**
|
|
76
|
+
* Build the fake-stream output and apply the same incremental TTS wrapper
|
|
77
|
+
* used by the standard NeuroLink stream path.
|
|
78
|
+
*/
|
|
79
|
+
private createFakeStreamingOutput;
|
|
75
80
|
/**
|
|
76
81
|
* Execute fake streaming - extracted method for reusability
|
|
77
82
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { context, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
2
|
import { directAgentTools } from "../agent/directTools.js";
|
|
3
|
-
import { isImageGenerationModel } from "
|
|
3
|
+
import { isImageGenerationModel } from "./constants.js";
|
|
4
4
|
import { MiddlewareFactory } from "../middleware/factory.js";
|
|
5
5
|
import { modelSupports } from "../models/modelRegistry.js";
|
|
6
6
|
import { ATTR, tracers } from "../telemetry/index.js";
|
|
@@ -13,6 +13,7 @@ import { duckTypedStatusCode, extractRetryAfterMsFromError, } from "../utils/pro
|
|
|
13
13
|
import { hasLifecycleErrorFired, markLifecycleErrorFired, } from "../utils/lifecycleCallbacks.js";
|
|
14
14
|
import { resolveLifecycleTimeoutMs } from "../utils/lifecycleTimeout.js";
|
|
15
15
|
import { logger } from "../utils/logger.js";
|
|
16
|
+
import { interleaveTTSStream } from "../utils/ttsStream.js";
|
|
16
17
|
import { TimeoutError as AsyncTimeoutError, withTimeoutFn, } from "../utils/async/withTimeout.js";
|
|
17
18
|
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
|
|
18
19
|
import { shouldDisableBuiltinTools } from "../utils/toolUtils.js";
|
|
@@ -486,6 +487,61 @@ export class BaseProvider {
|
|
|
486
487
|
logger.warn("[lifecycle] onError callback error:", e);
|
|
487
488
|
}
|
|
488
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* Build the fake-stream output and apply the same incremental TTS wrapper
|
|
492
|
+
* used by the standard NeuroLink stream path.
|
|
493
|
+
*/
|
|
494
|
+
createFakeStreamingOutput(result, options, onTTSComplete) {
|
|
495
|
+
const incrementalTTS = options.tts?.enabled === true;
|
|
496
|
+
const source = (async function* () {
|
|
497
|
+
if (result?.content) {
|
|
498
|
+
const words = result.content.split(/(\s+)/);
|
|
499
|
+
let buffer = "";
|
|
500
|
+
for (let i = 0; i < words.length; i++) {
|
|
501
|
+
buffer += words[i];
|
|
502
|
+
const shouldYield = i === words.length - 1 ||
|
|
503
|
+
buffer.length > 50 ||
|
|
504
|
+
/[.!?;,]\s*$/.test(buffer);
|
|
505
|
+
if (shouldYield && buffer.trim()) {
|
|
506
|
+
yield { content: buffer };
|
|
507
|
+
buffer = "";
|
|
508
|
+
await new Promise((resolve) => {
|
|
509
|
+
setTimeout(resolve, Math.random() * 9 + 1);
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (buffer.trim()) {
|
|
514
|
+
yield { content: buffer };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (result?.imageOutput) {
|
|
518
|
+
yield { type: "image", imageOutput: result.imageOutput };
|
|
519
|
+
}
|
|
520
|
+
if (result?.audio && !incrementalTTS) {
|
|
521
|
+
yield {
|
|
522
|
+
type: "tts_audio",
|
|
523
|
+
audio: {
|
|
524
|
+
data: result.audio.buffer,
|
|
525
|
+
format: result.audio.format,
|
|
526
|
+
index: 0,
|
|
527
|
+
isFinal: true,
|
|
528
|
+
cumulativeSize: result.audio.size,
|
|
529
|
+
voice: result.audio.voice,
|
|
530
|
+
sampleRate: result.audio.sampleRate,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
})();
|
|
535
|
+
if (!incrementalTTS || !options.tts) {
|
|
536
|
+
return source;
|
|
537
|
+
}
|
|
538
|
+
return interleaveTTSStream({
|
|
539
|
+
stream: source,
|
|
540
|
+
provider: options.tts.provider ?? options.provider ?? this.providerName,
|
|
541
|
+
options: options.tts,
|
|
542
|
+
onComplete: onTTSComplete,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
489
545
|
/**
|
|
490
546
|
* Execute fake streaming - extracted method for reusability
|
|
491
547
|
*/
|
|
@@ -525,10 +581,10 @@ export class BaseProvider {
|
|
|
525
581
|
skipToolPromptInjection: options.skipToolPromptInjection,
|
|
526
582
|
timeout: options.timeout,
|
|
527
583
|
stt: options.stt,
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
tts: options.tts,
|
|
584
|
+
// Streaming TTS is synthesized incrementally by
|
|
585
|
+
// createFakeStreamingOutput; do not let generate() perform a duplicate
|
|
586
|
+
// input- or whole-response synthesis first.
|
|
587
|
+
tts: options.tts?.enabled ? undefined : options.tts,
|
|
532
588
|
};
|
|
533
589
|
logger.debug(`Calling generate for fake streaming`, {
|
|
534
590
|
provider: this.providerName,
|
|
@@ -545,58 +601,45 @@ export class BaseProvider {
|
|
|
545
601
|
hasImageOutput: !!result?.imageOutput,
|
|
546
602
|
timestamp: Date.now(),
|
|
547
603
|
});
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
604
|
+
const incrementalTTS = options.tts?.enabled === true;
|
|
605
|
+
const ttsProvider = options.tts?.provider ?? options.provider ?? this.providerName;
|
|
606
|
+
const ttsStartedAt = Date.now();
|
|
607
|
+
let resolveAudio;
|
|
608
|
+
let audioSettled = false;
|
|
609
|
+
const audio = incrementalTTS
|
|
610
|
+
? new Promise((resolve) => {
|
|
611
|
+
resolveAudio = resolve;
|
|
612
|
+
}).catch(() => undefined)
|
|
613
|
+
: undefined;
|
|
614
|
+
const ttsMetadata = incrementalTTS
|
|
615
|
+
? {
|
|
616
|
+
attempted: TTSProcessor.supports(ttsProvider),
|
|
617
|
+
success: false,
|
|
618
|
+
}
|
|
619
|
+
: result?.ttsMetadata;
|
|
620
|
+
const onTTSComplete = incrementalTTS
|
|
621
|
+
? (ttsResult, error) => {
|
|
622
|
+
if (audioSettled) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
audioSettled = true;
|
|
626
|
+
if (ttsMetadata) {
|
|
627
|
+
ttsMetadata.success =
|
|
628
|
+
error === undefined && ttsResult !== undefined;
|
|
629
|
+
if (error) {
|
|
630
|
+
ttsMetadata.error = error;
|
|
569
631
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
yield { content: buffer };
|
|
632
|
+
else {
|
|
633
|
+
delete ttsMetadata.error;
|
|
573
634
|
}
|
|
635
|
+
ttsMetadata.latency = Date.now() - ttsStartedAt;
|
|
574
636
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
// Yield synthesized audio so callers using stream() with tts.enabled
|
|
583
|
-
// still receive a tts_audio chunk on the fake-streaming fallback
|
|
584
|
-
// path (matches the discriminator used by the real streaming path).
|
|
585
|
-
if (result?.audio) {
|
|
586
|
-
yield {
|
|
587
|
-
type: "tts_audio",
|
|
588
|
-
audio: {
|
|
589
|
-
data: result.audio.buffer,
|
|
590
|
-
format: result.audio.format,
|
|
591
|
-
index: 0,
|
|
592
|
-
isFinal: true,
|
|
593
|
-
cumulativeSize: result.audio.size,
|
|
594
|
-
voice: result.audio.voice,
|
|
595
|
-
sampleRate: result.audio.sampleRate,
|
|
596
|
-
},
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
})(),
|
|
637
|
+
resolveAudio?.(ttsResult);
|
|
638
|
+
}
|
|
639
|
+
: undefined;
|
|
640
|
+
// Create a synthetic stream from the generate result that simulates progressive delivery
|
|
641
|
+
return {
|
|
642
|
+
stream: this.createFakeStreamingOutput(result, options, onTTSComplete),
|
|
600
643
|
usage: result?.usage,
|
|
601
644
|
provider: result?.provider,
|
|
602
645
|
model: result?.model,
|
|
@@ -618,6 +661,8 @@ export class BaseProvider {
|
|
|
618
661
|
// 🔧 FIX: Include analytics and evaluation from generate result
|
|
619
662
|
analytics: result?.analytics,
|
|
620
663
|
evaluation: result?.evaluation,
|
|
664
|
+
audio,
|
|
665
|
+
ttsMetadata,
|
|
621
666
|
};
|
|
622
667
|
}
|
|
623
668
|
catch (error) {
|
|
@@ -11,6 +11,8 @@ import { API_KEY_FORMATS } from "../utils/providerConfig.js";
|
|
|
11
11
|
export const PROVIDER_DESCRIPTORS = [
|
|
12
12
|
{
|
|
13
13
|
name: AIProviderName.BEDROCK,
|
|
14
|
+
defaultHealthSweepPriority: 5,
|
|
15
|
+
autoSelectPreference: 7,
|
|
14
16
|
aliases: ["aws"],
|
|
15
17
|
credentialsKey: "bedrock",
|
|
16
18
|
envVars: {
|
|
@@ -32,6 +34,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
32
34
|
},
|
|
33
35
|
{
|
|
34
36
|
name: AIProviderName.OPENAI,
|
|
37
|
+
defaultHealthSweepPriority: 4,
|
|
38
|
+
autoSelectPreference: 3,
|
|
35
39
|
aliases: ["gpt", "chatgpt"],
|
|
36
40
|
credentialsKey: "openai",
|
|
37
41
|
envVars: { apiKey: "OPENAI_API_KEY", baseURL: "OPENAI_BASE_URL" },
|
|
@@ -75,6 +79,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
75
79
|
},
|
|
76
80
|
{
|
|
77
81
|
name: AIProviderName.VERTEX,
|
|
82
|
+
defaultHealthSweepPriority: 1,
|
|
83
|
+
autoSelectPreference: 5,
|
|
78
84
|
aliases: ["googleVertex"],
|
|
79
85
|
credentialsKey: "vertex",
|
|
80
86
|
envVars: {
|
|
@@ -115,6 +121,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
115
121
|
},
|
|
116
122
|
{
|
|
117
123
|
name: AIProviderName.ANTHROPIC,
|
|
124
|
+
defaultHealthSweepPriority: 3,
|
|
125
|
+
autoSelectPreference: 4,
|
|
118
126
|
aliases: ["claude"],
|
|
119
127
|
credentialsKey: "anthropic",
|
|
120
128
|
envVars: {
|
|
@@ -137,6 +145,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
137
145
|
},
|
|
138
146
|
{
|
|
139
147
|
name: AIProviderName.AZURE,
|
|
148
|
+
defaultHealthSweepPriority: 6,
|
|
149
|
+
autoSelectPreference: 8,
|
|
140
150
|
aliases: ["azureOpenai"],
|
|
141
151
|
credentialsKey: "azure",
|
|
142
152
|
envVars: {
|
|
@@ -160,6 +170,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
160
170
|
},
|
|
161
171
|
{
|
|
162
172
|
name: AIProviderName.GOOGLE_AI,
|
|
173
|
+
defaultHealthSweepPriority: 2,
|
|
174
|
+
autoSelectPreference: 6,
|
|
163
175
|
aliases: ["googleAiStudio", "google", "gemini", "google-ai-studio"],
|
|
164
176
|
credentialsKey: "googleAiStudio",
|
|
165
177
|
envVars: {
|
|
@@ -202,6 +214,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
202
214
|
},
|
|
203
215
|
{
|
|
204
216
|
name: AIProviderName.OLLAMA,
|
|
217
|
+
defaultHealthSweepPriority: 8,
|
|
218
|
+
autoSelectPreference: 2,
|
|
205
219
|
aliases: ["local"],
|
|
206
220
|
credentialsKey: "ollama",
|
|
207
221
|
envVars: {
|
|
@@ -234,6 +248,8 @@ export const PROVIDER_DESCRIPTORS = [
|
|
|
234
248
|
},
|
|
235
249
|
{
|
|
236
250
|
name: AIProviderName.LITELLM,
|
|
251
|
+
defaultHealthSweepPriority: 7,
|
|
252
|
+
autoSelectPreference: 1,
|
|
237
253
|
aliases: [],
|
|
238
254
|
credentialsKey: "litellm",
|
|
239
255
|
envVars: {
|
|
@@ -5,6 +5,22 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { MODEL_REGISTRY, MODEL_ALIASES, USE_CASE_RECOMMENDATIONS, getAllModels, getModelById, getModelsByProvider, getAvailableProviders, calculateCost, formatModelForDisplay, } from "./modelRegistry.js";
|
|
7
7
|
import { isNonNullObject } from "../utils/typeUtils.js";
|
|
8
|
+
const MIN_FUZZY_QUERY_LENGTH = 4;
|
|
9
|
+
function escapeRegExp(value) {
|
|
10
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* True when `needle` appears in `haystack` at a real token boundary (hyphen,
|
|
14
|
+
* underscore, dot, slash, whitespace, or string start/end) — not merely as
|
|
15
|
+
* an arbitrary substring. Model ids use hyphens/dots ("gpt-4.1-mini");
|
|
16
|
+
* MODEL_REGISTRY .name fields use spaces ("GPT-4 Omni"), hence including
|
|
17
|
+
* \s in the boundary class.
|
|
18
|
+
*/
|
|
19
|
+
function includesAtWordBoundary(haystack, needle) {
|
|
20
|
+
const boundary = "(?:^|[-_./\\s])";
|
|
21
|
+
const pattern = new RegExp(`${boundary}${escapeRegExp(needle)}${boundary.replace("^|", "$|")}`);
|
|
22
|
+
return pattern.test(haystack);
|
|
23
|
+
}
|
|
8
24
|
/**
|
|
9
25
|
* Model resolver class with advanced search and recommendation functionality
|
|
10
26
|
*/
|
|
@@ -23,25 +39,31 @@ export class ModelResolver {
|
|
|
23
39
|
const resolvedId = MODEL_ALIASES[normalizedQuery];
|
|
24
40
|
return MODEL_REGISTRY[resolvedId] || null;
|
|
25
41
|
}
|
|
42
|
+
// Underspecified queries produce ambiguous, iteration-order-dependent
|
|
43
|
+
// matches (see Task 13 of the model metadata consolidation plan) — skip
|
|
44
|
+
// fuzzy matching entirely below this length.
|
|
45
|
+
if (normalizedQuery.length < MIN_FUZZY_QUERY_LENGTH) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
26
48
|
// Fuzzy matching
|
|
27
49
|
const allModels = getAllModels();
|
|
28
50
|
// Try partial matching on ID
|
|
29
|
-
const idMatch = allModels.find((model) => model.id.toLowerCase()
|
|
30
|
-
normalizedQuery
|
|
51
|
+
const idMatch = allModels.find((model) => includesAtWordBoundary(model.id.toLowerCase(), normalizedQuery) ||
|
|
52
|
+
includesAtWordBoundary(normalizedQuery, model.id.toLowerCase()));
|
|
31
53
|
if (idMatch) {
|
|
32
54
|
return idMatch;
|
|
33
55
|
}
|
|
34
56
|
// Try partial matching on name
|
|
35
|
-
const nameMatch = allModels.find((model) => model.name.toLowerCase()
|
|
36
|
-
normalizedQuery
|
|
57
|
+
const nameMatch = allModels.find((model) => includesAtWordBoundary(model.name.toLowerCase(), normalizedQuery) ||
|
|
58
|
+
includesAtWordBoundary(normalizedQuery, model.name.toLowerCase()));
|
|
37
59
|
if (nameMatch) {
|
|
38
60
|
return nameMatch;
|
|
39
61
|
}
|
|
40
62
|
// Try provider-specific matching
|
|
41
63
|
const providerMatch = allModels.find((model) => {
|
|
42
64
|
const providerQuery = `${model.provider}-${normalizedQuery}`;
|
|
43
|
-
return (model.id.toLowerCase()
|
|
44
|
-
model.name.toLowerCase()
|
|
65
|
+
return (includesAtWordBoundary(model.id.toLowerCase(), providerQuery) ||
|
|
66
|
+
includesAtWordBoundary(model.name.toLowerCase(), normalizedQuery));
|
|
45
67
|
});
|
|
46
68
|
if (providerMatch) {
|
|
47
69
|
return providerMatch;
|
package/dist/neurolink.d.ts
CHANGED
|
@@ -945,7 +945,9 @@ export declare class NeuroLink {
|
|
|
945
945
|
*
|
|
946
946
|
* // Consume the stream
|
|
947
947
|
* for await (const chunk of result.stream) {
|
|
948
|
-
*
|
|
948
|
+
* if ("content" in chunk) {
|
|
949
|
+
* process.stdout.write(chunk.content);
|
|
950
|
+
* }
|
|
949
951
|
* }
|
|
950
952
|
*
|
|
951
953
|
* // Advanced streaming with options
|
|
@@ -1031,25 +1033,10 @@ export declare class NeuroLink {
|
|
|
1031
1033
|
private validateStreamRequestOptions;
|
|
1032
1034
|
private maybeHandleWorkflowStreamRequest;
|
|
1033
1035
|
private runStandardStreamRequest;
|
|
1034
|
-
/**
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
* stays under the max-lines-per-function lint budget. Behaviour preserved
|
|
1039
|
-
* exactly:
|
|
1040
|
-
* - When Mode 2 is enabled (`tts.enabled && tts.useAiResponse`) AND the
|
|
1041
|
-
* model produced non-empty content: synthesises one final audio buffer
|
|
1042
|
-
* and returns it as an `audioChunk` for the caller to `yield`. Resolves
|
|
1043
|
-
* `ttsResolver` with the `TTSResult`.
|
|
1044
|
-
* - When Mode 2 is enabled but synthesis fails: logs a warning and resolves
|
|
1045
|
-
* `ttsResolver` with `undefined`.
|
|
1046
|
-
* - When Mode 2 is requested but skipped (empty content / wrong mode):
|
|
1047
|
-
* resolves `ttsResolver` with `undefined` early so callers awaiting
|
|
1048
|
-
* `result.audio` unblock before the surrounding `finally` cleanup
|
|
1049
|
-
* completes (Issue 7 latency micro-opt — the finally block also resolves
|
|
1050
|
-
* defensively, so this is a redundant early signal, not a coverage fix).
|
|
1051
|
-
*/
|
|
1052
|
-
private synthesizeStreamModeTwo;
|
|
1036
|
+
/** Wrap one provider stream with incremental TTS synthesis. */
|
|
1037
|
+
private createIncrementalTTSStream;
|
|
1038
|
+
/** Prevent provider fallback streams from duplicating outer streaming TTS. */
|
|
1039
|
+
private deferProviderStreamTTS;
|
|
1053
1040
|
/**
|
|
1054
1041
|
* Prepare stream options: initialize memory, MCP, retrieval, orchestration,
|
|
1055
1042
|
* Ollama tool auto-disable, factory processing, and tool detection.
|