@juspay/neurolink 12.28.4 → 12.29.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.
Files changed (45) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
  3. package/dist/adapters/tts/googleTTSHandler.js +254 -0
  4. package/dist/browser/neurolink.min.js +399 -399
  5. package/dist/cli/commands/voices.d.ts +23 -0
  6. package/dist/cli/commands/voices.js +102 -0
  7. package/dist/cli/loop/optionsSchema.d.ts +1 -1
  8. package/dist/cli/parser.js +3 -0
  9. package/dist/constants/enums.d.ts +13 -0
  10. package/dist/constants/enums.js +15 -0
  11. package/dist/core/baseProvider.d.ts +4 -0
  12. package/dist/core/baseProvider.js +245 -148
  13. package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
  14. package/dist/core/modules/structuredOutputPolicy.js +36 -0
  15. package/dist/core/nativeGenerateLoop.js +7 -1
  16. package/dist/neurolink.d.ts +42 -0
  17. package/dist/neurolink.js +102 -2
  18. package/dist/providers/catalog/index.generated.d.ts +1 -1
  19. package/dist/providers/catalog/index.generated.js +6 -0
  20. package/dist/providers/catalog/morph.json +97 -0
  21. package/dist/providers/catalog/novita.json +114 -0
  22. package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
  23. package/dist/providers/openaiChatCompletionsBase.js +291 -8
  24. package/dist/server/utils/validation.d.ts +34 -0
  25. package/dist/server/utils/validation.js +27 -0
  26. package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
  27. package/dist/server/websocket/WebSocketHandler.js +185 -18
  28. package/dist/types/cli.d.ts +11 -0
  29. package/dist/types/generate.d.ts +15 -0
  30. package/dist/types/isolatedAgent.d.ts +16 -7
  31. package/dist/types/openaiCompatible.d.ts +6 -0
  32. package/dist/types/providerCatalog.generated.d.ts +2 -2
  33. package/dist/types/providers.d.ts +50 -3
  34. package/dist/types/server.d.ts +8 -0
  35. package/dist/types/stream.d.ts +30 -0
  36. package/dist/types/tts.d.ts +10 -0
  37. package/dist/types/utilities.d.ts +16 -3
  38. package/dist/utils/logger.d.ts +58 -13
  39. package/dist/utils/logger.js +156 -11
  40. package/dist/utils/providerHealth.d.ts +65 -1
  41. package/dist/utils/providerHealth.js +216 -54
  42. package/dist/utils/ttsProcessor.d.ts +30 -1
  43. package/dist/utils/ttsProcessor.js +76 -0
  44. package/docs-site/static/search-index.json +4 -4
  45. package/package.json +6 -1
@@ -13,6 +13,19 @@
13
13
  * - Structured data support for complex objects
14
14
  * - Tabular data display
15
15
  */
16
+ import { AsyncLocalStorage } from "node:async_hooks";
17
+ /**
18
+ * Identifies which NeuroLink instance is emitting, for the async operation
19
+ * currently on the stack. Populated by `runInInstanceScope()` — which the SDK
20
+ * entry points (`generate`, `stream`, `generateText`) wrap their bodies in —
21
+ * so a log call anywhere beneath them can be attributed without every call
22
+ * site threading an instance through.
23
+ *
24
+ * Empty for logs emitted outside any call (construction, background MCP
25
+ * reconnects, module init). Those stay unattributed rather than being charged
26
+ * to whichever instance happens to hold the global sink.
27
+ */
28
+ const instanceLogScope = new AsyncLocalStorage();
16
29
  // OTel trace context for log correlation (optional — gracefully no-ops if OTel not initialized)
17
30
  let traceApi = null;
18
31
  let traceApiPromise = null;
@@ -48,6 +61,30 @@ function argvRequestsJsonOutput() {
48
61
  return argv.some((arg, index) => (FORMAT_FLAGS.includes(arg) && argv[index + 1] === "json") ||
49
62
  FORMAT_FLAGS.some((flag) => arg === `${flag}=json`));
50
63
  }
64
+ /**
65
+ * Best-effort isolated copy of a log call's `data` payload for one consumer.
66
+ * `NeuroLinkLogger.log()` hands the same call's data to every scoped emitter
67
+ * for the active instance plus the process-wide sink — a designed, supported
68
+ * multi-consumer configuration — so a copy per consumer is what keeps one
69
+ * consumer's in-place mutation (e.g. redacting a field before forwarding it)
70
+ * from silently corrupting what a sibling consumer, or stored log history,
71
+ * observes. Non-object values need no copy — they are already by-value.
72
+ * Values `structuredClone` cannot handle (functions, etc.) are handed through
73
+ * unchanged rather than dropped: isolation is not achievable for those
74
+ * without lossy serialization, so that narrow case keeps the old
75
+ * shared-reference behavior instead of losing the payload.
76
+ */
77
+ function cloneLogData(data) {
78
+ if (data === null || typeof data !== "object") {
79
+ return data;
80
+ }
81
+ try {
82
+ return structuredClone(data);
83
+ }
84
+ catch {
85
+ return data;
86
+ }
87
+ }
51
88
  class NeuroLinkLogger {
52
89
  logLevel = "info";
53
90
  logs = [];
@@ -55,6 +92,7 @@ class NeuroLinkLogger {
55
92
  isDebugMode;
56
93
  diagnosticsToStderr = false;
57
94
  eventEmitter;
95
+ scopedEmitters = new Map();
58
96
  constructor() {
59
97
  // Cache debug mode check to avoid repeated array searches
60
98
  this.isDebugMode =
@@ -90,6 +128,75 @@ class NeuroLinkLogger {
90
128
  }
91
129
  this.eventEmitter = undefined;
92
130
  }
131
+ /**
132
+ * Runs `fn` with every log call beneath it attributed to `instanceId`.
133
+ *
134
+ * Nesting is safe and expected: a host turn that delegates to a worker ends
135
+ * up with the worker's id on top for the duration of the worker's call, and
136
+ * the host's id restored afterwards. Re-entering with the same id is a
137
+ * no-op in effect.
138
+ *
139
+ * @param instanceId - Identifier of the emitting NeuroLink instance
140
+ * @param fn - Work to run inside the scope
141
+ * @returns Whatever `fn` returns
142
+ */
143
+ runInInstanceScope(instanceId, fn) {
144
+ return instanceLogScope.run(instanceId, fn);
145
+ }
146
+ /**
147
+ * The instance id currently attributed, or undefined outside any scope.
148
+ */
149
+ getInstanceScope() {
150
+ return instanceLogScope.getStore();
151
+ }
152
+ /**
153
+ * Subscribes `emitter` to log events emitted by one instance only.
154
+ *
155
+ * Unlike {@link setEventEmitter} — a single process-wide sink that receives
156
+ * everything — a scoped emitter receives only events logged inside that
157
+ * instance's {@link runInInstanceScope}. Several emitters may share an id.
158
+ *
159
+ * @param instanceId - Instance whose events this emitter should receive
160
+ * @param emitter - The sink to subscribe
161
+ */
162
+ addScopedEventEmitter(instanceId, emitter) {
163
+ const existing = this.scopedEmitters.get(instanceId);
164
+ if (existing) {
165
+ existing.add(emitter);
166
+ return;
167
+ }
168
+ this.scopedEmitters.set(instanceId, new Set([emitter]));
169
+ }
170
+ /**
171
+ * Unsubscribes a scoped emitter. Must be called when the owning instance is
172
+ * disposed, or the emitter — and everything it closes over — is retained by
173
+ * the process-global logger for the lifetime of the process.
174
+ *
175
+ * @param instanceId - Instance the emitter was registered against
176
+ * @param emitter - The sink to unsubscribe
177
+ */
178
+ removeScopedEventEmitter(instanceId, emitter) {
179
+ const existing = this.scopedEmitters.get(instanceId);
180
+ if (!existing) {
181
+ return;
182
+ }
183
+ existing.delete(emitter);
184
+ if (existing.size === 0) {
185
+ this.scopedEmitters.delete(instanceId);
186
+ }
187
+ }
188
+ /**
189
+ * Drops every scoped emitter registered against an instance.
190
+ *
191
+ * Called from `NeuroLink.dispose()`: the registry lives on the
192
+ * process-global logger, so an instance that goes away without clearing its
193
+ * own entry would keep its sinks — and their closures — reachable forever.
194
+ *
195
+ * @param instanceId - Instance whose sinks should be dropped
196
+ */
197
+ clearScopedEventEmitters(instanceId) {
198
+ this.scopedEmitters.delete(instanceId);
199
+ }
93
200
  /**
94
201
  * Sets the minimum log level that will be processed and output.
95
202
  * Log messages with a level lower than this will be ignored.
@@ -274,18 +381,50 @@ class NeuroLinkLogger {
274
381
  timestamp: new Date(),
275
382
  data,
276
383
  };
277
- // Emit log event if emitter is configured
278
- if (this.eventEmitter) {
279
- try {
280
- this.eventEmitter.emit("log-event", {
281
- level,
282
- message,
283
- timestamp: new Date().getTime(),
284
- data,
285
- });
384
+ // Emit to the per-instance sinks for whichever instance is on the stack,
385
+ // then to the process-wide sink. The two are independent: a scoped sink
386
+ // never sees another instance's events, and the global sink still sees
387
+ // everything, so existing setEventEmitter() consumers are unaffected.
388
+ //
389
+ // Several emitters may be registered for the same instance id (see
390
+ // addScopedEventEmitter's own JSDoc), and the global sink always runs
391
+ // alongside them — so one call's `data` object is, by design, handed to
392
+ // more than one consumer. Each consumer below gets its OWN clone rather
393
+ // than the shared `data` reference, so an in-place mutation by one (e.g.
394
+ // a bridge redacting a field before forwarding it) cannot corrupt what a
395
+ // sibling consumer sees or what `entry` — which keeps the original,
396
+ // unmodified `data` — stores for `getLogs()`.
397
+ const scopeId = instanceLogScope.getStore();
398
+ const scoped = scopeId === undefined ? undefined : this.scopedEmitters.get(scopeId);
399
+ if (scoped || this.eventEmitter) {
400
+ const timestampMs = entry.timestamp.getTime();
401
+ if (scoped) {
402
+ for (const emitter of scoped) {
403
+ try {
404
+ emitter.emit("log-event", {
405
+ level,
406
+ message,
407
+ timestamp: timestampMs,
408
+ data: cloneLogData(data),
409
+ });
410
+ }
411
+ catch {
412
+ // Silently ignore emitter errors to avoid disrupting logging
413
+ }
414
+ }
286
415
  }
287
- catch {
288
- // Silently ignore emitter errors to avoid disrupting logging
416
+ if (this.eventEmitter) {
417
+ try {
418
+ this.eventEmitter.emit("log-event", {
419
+ level,
420
+ message,
421
+ timestamp: timestampMs,
422
+ data: cloneLogData(data),
423
+ });
424
+ }
425
+ catch {
426
+ // Silently ignore emitter errors to avoid disrupting logging
427
+ }
289
428
  }
290
429
  }
291
430
  // Store log entry
@@ -496,6 +635,12 @@ export const logger = {
496
635
  clearLogs: () => neuroLinkLogger.clearLogs(),
497
636
  setEventEmitter: (emitter) => neuroLinkLogger.setEventEmitter(emitter),
498
637
  clearEventEmitter: (ifEmitter) => neuroLinkLogger.clearEventEmitter(ifEmitter),
638
+ // Per-instance routing (see NeuroLinkLogger.runInInstanceScope)
639
+ runInInstanceScope: (instanceId, fn) => neuroLinkLogger.runInInstanceScope(instanceId, fn),
640
+ getInstanceScope: () => neuroLinkLogger.getInstanceScope(),
641
+ addScopedEventEmitter: (instanceId, emitter) => neuroLinkLogger.addScopedEventEmitter(instanceId, emitter),
642
+ removeScopedEventEmitter: (instanceId, emitter) => neuroLinkLogger.removeScopedEventEmitter(instanceId, emitter),
643
+ clearScopedEventEmitters: (instanceId) => neuroLinkLogger.clearScopedEventEmitters(instanceId),
499
644
  };
500
645
  /**
501
646
  * MCP compatibility exports - all use the same unified logger instance.
@@ -8,8 +8,53 @@ export declare class ProviderHealthChecker {
8
8
  private static healthCache;
9
9
  private static readonly DEFAULT_TIMEOUT;
10
10
  private static readonly DEFAULT_CACHE_AGE;
11
+ /**
12
+ * Fixed backoff window for the circuit breaker below, in milliseconds.
13
+ * Deliberately NOT the per-call `maxCacheAge` option: `consecutiveFailures`
14
+ * is a single process-wide map keyed only by provider name, shared by every
15
+ * caller (e.g. `checkFallbackProviderAvailability` hard-codes a 15s
16
+ * `maxCacheAge` for its own health-status cache). Evicting the breaker
17
+ * entry using whichever caller happens to read it next would let a
18
+ * short-TTL caller silently erase a backoff another caller is relying on
19
+ * — see `checkProviderHealth`. This constant governs breaker expiry alone
20
+ * and is intentionally independent of `maxCacheAge`/`DEFAULT_CACHE_AGE`.
21
+ */
22
+ private static readonly CIRCUIT_BREAKER_RESET_MS;
11
23
  private static readonly CONSECUTIVE_FAILURE_THRESHOLD;
24
+ /**
25
+ * Circuit breaker state, keyed by provider.
26
+ *
27
+ * `count` is a count of consecutive failed CONNECTIVITY PROBES (plus the
28
+ * rare case of the check itself throwing) — never of "the provider is
29
+ * unconfigured". Those are different facts and only one of them is worth
30
+ * backing off from. A missing credential is a free, local, determinate
31
+ * answer that costs nothing to re-derive and flips the instant an env var
32
+ * is set; treating it as a failure is what let a sweep of ~38 providers
33
+ * (issue #1305 grew it from 8) blacklist every provider the machine
34
+ * happens not to have credentials for, after which the sweep reported
35
+ * `isConfigured: false` and no `responseTime` for a provider that was
36
+ * merely unconfigured — and kept reporting it after the key was supplied.
37
+ *
38
+ * `lastFailureAt` bounds how long a run of failures is held against a
39
+ * provider; see `checkProviderHealth`.
40
+ */
12
41
  private static consecutiveFailures;
42
+ /**
43
+ * Hard cap on simultaneous per-provider checks inside
44
+ * `checkAllProvidersHealth`. Issue #1305: the sweep used to cover only 8
45
+ * of ~38 registered providers; now every non-opted-out descriptor is
46
+ * included (see `excludeFromHealthSweep`). Firing all ~38 at once via a
47
+ * single `Promise.allSettled` would be harmless by default
48
+ * (`includeConnectivityTest` defaults to false, so most calls never
49
+ * leave the process), but a caller who opts into
50
+ * `includeConnectivityTest: true` would otherwise open up to ~38
51
+ * concurrent outbound HTTP connections to that many different vendors in
52
+ * one burst. A worker pool keeps that bounded without reducing coverage
53
+ * — every provider is still checked, just not all in the same instant,
54
+ * and without the latency a fixed batch would add (see
55
+ * `checkAllProvidersHealth`).
56
+ */
57
+ private static readonly MAX_CONCURRENT_HEALTH_CHECKS;
13
58
  /**
14
59
  * Validate and return a safe failure threshold value
15
60
  */
@@ -236,7 +281,26 @@ export declare class ProviderHealthChecker {
236
281
  */
237
282
  static getBestHealthyProvider(preferredProviders?: string[]): Promise<string | null>;
238
283
  /**
239
- * Get health status for all registered providers
284
+ * Get health status for all registered providers.
285
+ *
286
+ * Membership: every descriptor in PROVIDER_DESCRIPTORS EXCEPT those that
287
+ * explicitly opt out via `excludeFromHealthSweep` (none currently do).
288
+ * Before issue #1305's fix, membership was implicit opt-IN via
289
+ * `defaultHealthSweepPriority`, which only 8 of ~38 descriptors carried —
290
+ * every other registered provider was silently invisible to this sweep,
291
+ * including anything a caller had actually configured and could use.
292
+ *
293
+ * Order: `defaultHealthSweepPriority` still decides ORDER for the
294
+ * providers that set it (lower = first — order is behaviour for
295
+ * first-healthy-wins callers like `getBestHealthyProvider`); everything
296
+ * else sorts after them, in PROVIDER_DESCRIPTORS's own declaration order
297
+ * (Array.prototype.sort is stable, so ties never reshuffle).
298
+ *
299
+ * Checks run through a worker pool of at most
300
+ * MAX_CONCURRENT_HEALTH_CHECKS, so a full sweep of every provider can't
301
+ * fire dozens of concurrent requests at once when a caller opts into
302
+ * `includeConnectivityTest`, and no check waits on a slower one beside
303
+ * it.
240
304
  */
241
305
  static checkAllProvidersHealth(options?: ProviderHealthCheckOptions): Promise<ProviderHealthStatusOptions[]>;
242
306
  /**