@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.
@@ -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 "../core/constants.js";
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
- // Forward TTS options too without this, the fake-streaming fallback
529
- // path silently drops `tts` and the resulting StreamResult never
530
- // produces a `tts_audio` chunk even when synthesis was requested.
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
- // Create a synthetic stream from the generate result that simulates progressive delivery
549
- return {
550
- stream: (async function* () {
551
- if (result?.content) {
552
- // Split content into words for more natural streaming
553
- const words = result.content.split(/(\s+)/); // Keep whitespace
554
- let buffer = "";
555
- for (let i = 0; i < words.length; i++) {
556
- buffer += words[i];
557
- // Yield chunks of roughly 5-10 words or at punctuation
558
- const shouldYield = i === words.length - 1 || // Last word
559
- buffer.length > 50 || // Buffer getting long
560
- /[.!?;,]\s*$/.test(buffer); // End of sentence/clause
561
- if (shouldYield && buffer.trim()) {
562
- yield { content: buffer };
563
- buffer = "";
564
- // Small delay to simulate streaming (1-10ms)
565
- await new Promise((resolve) => {
566
- setTimeout(resolve, Math.random() * 9 + 1);
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
- // Yield all remaining content
571
- if (buffer.trim()) {
572
- yield { content: buffer };
632
+ else {
633
+ delete ttsMetadata.error;
573
634
  }
635
+ ttsMetadata.latency = Date.now() - ttsStartedAt;
574
636
  }
575
- // 🔧 CRITICAL FIX: Yield image output if present
576
- if (result?.imageOutput) {
577
- yield {
578
- type: "image",
579
- imageOutput: result.imageOutput,
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().includes(normalizedQuery) ||
30
- normalizedQuery.includes(model.id.toLowerCase()));
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().includes(normalizedQuery) ||
36
- normalizedQuery.includes(model.name.toLowerCase()));
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().includes(providerQuery) ||
44
- model.name.toLowerCase().includes(normalizedQuery));
65
+ return (includesAtWordBoundary(model.id.toLowerCase(), providerQuery) ||
66
+ includesAtWordBoundary(model.name.toLowerCase(), normalizedQuery));
45
67
  });
46
68
  if (providerMatch) {
47
69
  return providerMatch;
@@ -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
- * process.stdout.write(chunk.content);
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
- * TTS Mode 2 synthesis helper for the stream() pipeline.
1036
- *
1037
- * m5 — extracted from runStandardStreamRequest so the surrounding generator
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.