@juspay/neurolink 12.28.3 → 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.
- package/CHANGELOG.md +9 -2
- package/dist/adapters/tts/googleTTSHandler.d.ts +71 -1
- package/dist/adapters/tts/googleTTSHandler.js +254 -0
- package/dist/browser/neurolink.min.js +399 -399
- package/dist/cli/commands/voices.d.ts +23 -0
- package/dist/cli/commands/voices.js +102 -0
- package/dist/cli/loop/optionsSchema.d.ts +1 -1
- package/dist/cli/parser.js +3 -0
- package/dist/constants/enums.d.ts +13 -0
- package/dist/constants/enums.js +15 -0
- package/dist/core/baseProvider.d.ts +4 -0
- package/dist/core/baseProvider.js +245 -148
- package/dist/core/modules/structuredOutputPolicy.d.ts +27 -0
- package/dist/core/modules/structuredOutputPolicy.js +36 -0
- package/dist/core/nativeGenerateLoop.js +7 -1
- package/dist/neurolink.d.ts +42 -0
- package/dist/neurolink.js +102 -2
- package/dist/providers/catalog/index.generated.d.ts +1 -1
- package/dist/providers/catalog/index.generated.js +6 -0
- package/dist/providers/catalog/morph.json +97 -0
- package/dist/providers/catalog/novita.json +114 -0
- package/dist/providers/openaiChatCompletionsBase.d.ts +22 -0
- package/dist/providers/openaiChatCompletionsBase.js +291 -8
- package/dist/server/utils/validation.d.ts +34 -0
- package/dist/server/utils/validation.js +27 -0
- package/dist/server/websocket/WebSocketHandler.d.ts +4 -6
- package/dist/server/websocket/WebSocketHandler.js +185 -18
- package/dist/types/cli.d.ts +11 -0
- package/dist/types/generate.d.ts +15 -0
- package/dist/types/isolatedAgent.d.ts +16 -7
- package/dist/types/openaiCompatible.d.ts +6 -0
- package/dist/types/providerCatalog.generated.d.ts +2 -2
- package/dist/types/providers.d.ts +50 -3
- package/dist/types/server.d.ts +8 -0
- package/dist/types/stream.d.ts +30 -0
- package/dist/types/tts.d.ts +10 -0
- package/dist/types/utilities.d.ts +16 -3
- package/dist/utils/logger.d.ts +58 -13
- package/dist/utils/logger.js +156 -11
- package/dist/utils/providerHealth.d.ts +65 -1
- package/dist/utils/providerHealth.js +216 -54
- package/dist/utils/ttsProcessor.d.ts +30 -1
- package/dist/utils/ttsProcessor.js +76 -0
- package/docs-site/static/search-index.json +4 -4
- package/package.json +6 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { WebSocketError, WebSocketConnectionError } from "../errors.js";
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
|
+
import { WebSocketAgentRequestSchema } from "../utils/validation.js";
|
|
9
10
|
/**
|
|
10
11
|
* Default WebSocket configuration
|
|
11
12
|
*/
|
|
@@ -58,6 +59,46 @@ export class WebSocketConnectionManager {
|
|
|
58
59
|
if (this.connections.size >= this.config.maxConnections) {
|
|
59
60
|
throw new WebSocketConnectionError(`Maximum connections (${this.config.maxConnections}) reached`);
|
|
60
61
|
}
|
|
62
|
+
// Enforce the auth contract the config already declares. `auth.required`
|
|
63
|
+
// has existed on WebSocketConfig (with a `strategy`) since this manager
|
|
64
|
+
// was written, and `handleConnection` has always accepted and stored an
|
|
65
|
+
// AuthenticatedUser — but nothing ever checked either, so a deployer who
|
|
66
|
+
// set `required: true` got exactly the same open socket as one who did
|
|
67
|
+
// not. That was latent while the agent routes were stubs that returned
|
|
68
|
+
// canned values; it stops being latent the moment `tool_call` actually
|
|
69
|
+
// reaches `executeTool`, because then an unauthenticated client can
|
|
70
|
+
// invoke any registered tool with arbitrary arguments. Refuse the
|
|
71
|
+
// connection rather than letting a route decide, so every handler
|
|
72
|
+
// registered on this manager inherits the gate.
|
|
73
|
+
if (this.config.auth.required && !user) {
|
|
74
|
+
logger.warn(`[WebSocket] Rejected unauthenticated connection to ${path} (auth.required)`);
|
|
75
|
+
throw new WebSocketConnectionError("Authentication required for this WebSocket endpoint");
|
|
76
|
+
}
|
|
77
|
+
// Same gap, same fix, one field over. `ServerAuthConfig.roles`/
|
|
78
|
+
// `permissions` and `AuthenticatedUser.roles`/`permissions` have existed
|
|
79
|
+
// since this manager was written — the shipped "Role-Based Access
|
|
80
|
+
// Control" doc example configures them on a `/ws/admin` path — but
|
|
81
|
+
// nothing ever consulted either, so a deployer following that example
|
|
82
|
+
// got an endpoint anyone authenticated could reach, regardless of role.
|
|
83
|
+
// Semantics match the SDK's own `hasAnyRole`/`hasAllPermissions`
|
|
84
|
+
// (src/lib/auth/authContext.ts): roles are an any-of allowlist,
|
|
85
|
+
// permissions must all be present.
|
|
86
|
+
const requiredRoles = this.config.auth.roles;
|
|
87
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
88
|
+
const userRoles = user?.roles ?? [];
|
|
89
|
+
if (!requiredRoles.some((role) => userRoles.includes(role))) {
|
|
90
|
+
logger.warn(`[WebSocket] Rejected connection to ${path}: missing a required role`);
|
|
91
|
+
throw new WebSocketConnectionError("Insufficient role for this WebSocket endpoint");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const requiredPermissions = this.config.auth.permissions;
|
|
95
|
+
if (requiredPermissions && requiredPermissions.length > 0) {
|
|
96
|
+
const userPermissions = user?.permissions ?? [];
|
|
97
|
+
if (!requiredPermissions.every((permission) => userPermissions.includes(permission))) {
|
|
98
|
+
logger.warn(`[WebSocket] Rejected connection to ${path}: missing a required permission`);
|
|
99
|
+
throw new WebSocketConnectionError("Insufficient permissions for this WebSocket endpoint");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
61
102
|
const connection = {
|
|
62
103
|
id: generateConnectionId(),
|
|
63
104
|
socket,
|
|
@@ -284,6 +325,17 @@ export class WebSocketConnectionManager {
|
|
|
284
325
|
}
|
|
285
326
|
}
|
|
286
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Narrow an incoming message's raw `id` field to the one shape it is safe to
|
|
330
|
+
* echo back (see `WebSocketRequestId`); anything else — including a missing
|
|
331
|
+
* field, since most callers won't opt into correlation at all — is treated
|
|
332
|
+
* as "no id".
|
|
333
|
+
*/
|
|
334
|
+
function readRequestId(value) {
|
|
335
|
+
return typeof value === "string" || typeof value === "number"
|
|
336
|
+
? value
|
|
337
|
+
: undefined;
|
|
338
|
+
}
|
|
287
339
|
/**
|
|
288
340
|
* WebSocket message router for handling different message types
|
|
289
341
|
*/
|
|
@@ -309,7 +361,8 @@ export class WebSocketMessageRouter {
|
|
|
309
361
|
catch {
|
|
310
362
|
throw new WebSocketError("Invalid JSON message");
|
|
311
363
|
}
|
|
312
|
-
const { type, payload } = parsed;
|
|
364
|
+
const { type, payload, id } = parsed;
|
|
365
|
+
const requestId = readRequestId(id);
|
|
313
366
|
if (!type) {
|
|
314
367
|
throw new WebSocketError("Message type is required");
|
|
315
368
|
}
|
|
@@ -317,7 +370,22 @@ export class WebSocketMessageRouter {
|
|
|
317
370
|
if (!handler) {
|
|
318
371
|
throw new WebSocketError(`Unknown message type: ${type}`);
|
|
319
372
|
}
|
|
320
|
-
|
|
373
|
+
const result = await handler(connection, payload, requestId);
|
|
374
|
+
// Overlapping in-flight messages on one connection previously produced
|
|
375
|
+
// response frames with nothing tying them back to the request that
|
|
376
|
+
// caused them (silent when the client had more than one call open at
|
|
377
|
+
// once). A route that already tags its own frame — the streaming route
|
|
378
|
+
// does, for the intermediate frames it sends directly — is left alone;
|
|
379
|
+
// this only fills in the id for a plain result object that doesn't
|
|
380
|
+
// carry one, so a route that ignores `requestId` still gets correlated
|
|
381
|
+
// for free, and a client that never sends an `id` sees no change at all.
|
|
382
|
+
if (requestId !== undefined &&
|
|
383
|
+
result !== null &&
|
|
384
|
+
typeof result === "object" &&
|
|
385
|
+
!("id" in result)) {
|
|
386
|
+
return { ...result, id: requestId };
|
|
387
|
+
}
|
|
388
|
+
return result;
|
|
321
389
|
}
|
|
322
390
|
/**
|
|
323
391
|
* Get registered routes
|
|
@@ -329,23 +397,110 @@ export class WebSocketMessageRouter {
|
|
|
329
397
|
/**
|
|
330
398
|
* Create a WebSocket handler for AI agent interactions
|
|
331
399
|
*/
|
|
332
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Send one frame, reporting failure instead of throwing.
|
|
402
|
+
*
|
|
403
|
+
* A socket that has already closed throws on `send`. That matters in two
|
|
404
|
+
* places here: mid-stream, where an unguarded throw unwinds into `onMessage`'s
|
|
405
|
+
* catch, which then tries to send an error frame on the same dead socket and
|
|
406
|
+
* throws again — this time with nothing above it to catch, i.e. an unhandled
|
|
407
|
+
* rejection; and in that catch itself, for the same reason. Returning a
|
|
408
|
+
* boolean lets both callers stop cleanly instead.
|
|
409
|
+
*/
|
|
410
|
+
function trySend(connection, payload) {
|
|
411
|
+
const socket = connection.socket;
|
|
412
|
+
try {
|
|
413
|
+
socket.send(JSON.stringify(payload));
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Pull a correlation id off a raw incoming frame without depending on
|
|
422
|
+
* `WebSocketMessageRouter.handle` having successfully parsed it — the one
|
|
423
|
+
* case this is for is a message that fails *inside* `handle` (bad JSON,
|
|
424
|
+
* missing type, unknown route), where the router never gets far enough to
|
|
425
|
+
* return the id it would otherwise have tagged the response with. Failure
|
|
426
|
+
* here (not text, not JSON, no `id`) just means "no id", the same as if the
|
|
427
|
+
* client never sent one.
|
|
428
|
+
*/
|
|
429
|
+
function readIncomingRequestId(message) {
|
|
430
|
+
if (message.type !== "text") {
|
|
431
|
+
return undefined;
|
|
432
|
+
}
|
|
433
|
+
try {
|
|
434
|
+
const parsed = JSON.parse(message.data);
|
|
435
|
+
return readRequestId(parsed.id);
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return undefined;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
export function createAgentWebSocketHandler(neurolink) {
|
|
333
442
|
const router = new WebSocketMessageRouter();
|
|
334
443
|
// Register message routes
|
|
444
|
+
//
|
|
445
|
+
// `payload` is attacker-controlled JSON (WebSocketMessageRouter.handle does
|
|
446
|
+
// nothing but JSON.parse it), so `generate`/`stream` validate it against
|
|
447
|
+
// `WebSocketAgentRequestSchema` before it reaches the SDK rather than
|
|
448
|
+
// trusting a bare type assertion. The schema's `options` allowlist is
|
|
449
|
+
// deliberately narrower than `GenerateOptions`/`StreamOptions` — most
|
|
450
|
+
// importantly it has no `credentials` field, so a client can no longer
|
|
451
|
+
// point the server's outbound request at an arbitrary `baseURL` (SSRF) or
|
|
452
|
+
// swap in its own provider API key.
|
|
335
453
|
router.route("generate", async (connection, payload) => {
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
454
|
+
const parsed = WebSocketAgentRequestSchema.safeParse(payload);
|
|
455
|
+
if (!parsed.success) {
|
|
456
|
+
throw new WebSocketError(`Invalid generate payload: ${parsed.error.issues.map((issue) => issue.message).join(", ")}`);
|
|
457
|
+
}
|
|
458
|
+
const { prompt, options } = parsed.data;
|
|
459
|
+
const result = await neurolink.generate({
|
|
460
|
+
...options,
|
|
461
|
+
input: { text: prompt },
|
|
462
|
+
});
|
|
463
|
+
return { type: "response", content: result.content };
|
|
339
464
|
});
|
|
340
|
-
router.route("stream", async (connection, payload) => {
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
465
|
+
router.route("stream", async (connection, payload, requestId) => {
|
|
466
|
+
const parsed = WebSocketAgentRequestSchema.safeParse(payload);
|
|
467
|
+
if (!parsed.success) {
|
|
468
|
+
throw new WebSocketError(`Invalid stream payload: ${parsed.error.issues.map((issue) => issue.message).join(", ")}`);
|
|
469
|
+
}
|
|
470
|
+
const { prompt, options } = parsed.data;
|
|
471
|
+
// stream_start/chunk are sent directly through trySend rather than
|
|
472
|
+
// returned, so — unlike generate/tool_call's single returned frame —
|
|
473
|
+
// they sit outside WebSocketMessageRouter.handle's own id-tagging and
|
|
474
|
+
// must carry the correlation id themselves.
|
|
475
|
+
const withRequestId = (frame) => requestId !== undefined ? { ...frame, id: requestId } : frame;
|
|
476
|
+
if (!trySend(connection, withRequestId({ type: "stream_start", data: { prompt } }))) {
|
|
477
|
+
return undefined;
|
|
478
|
+
}
|
|
479
|
+
const result = await neurolink.stream({
|
|
480
|
+
...options,
|
|
481
|
+
input: { text: prompt },
|
|
482
|
+
});
|
|
483
|
+
let clientPresent = true;
|
|
484
|
+
for await (const chunk of result.stream) {
|
|
485
|
+
if ("content" in chunk && typeof chunk.content === "string") {
|
|
486
|
+
if (!trySend(connection, withRequestId({ type: "chunk", content: chunk.content }))) {
|
|
487
|
+
// The client is gone. Breaking runs the stream's IteratorClose,
|
|
488
|
+
// which now aborts the upstream request rather than leaving it in
|
|
489
|
+
// flight for a reader that no longer exists.
|
|
490
|
+
clientPresent = false;
|
|
491
|
+
logger.info(`[AgentWebSocket] Client went away mid-stream: ${connection.id}`);
|
|
492
|
+
break;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
// Returning nothing keeps onMessage from sending a completion frame on a
|
|
497
|
+
// socket that just refused one.
|
|
498
|
+
return clientPresent ? { type: "stream_complete" } : undefined;
|
|
344
499
|
});
|
|
345
500
|
router.route("tool_call", async (connection, payload) => {
|
|
346
|
-
const { toolName, args
|
|
347
|
-
|
|
348
|
-
return { type: "tool_result",
|
|
501
|
+
const { toolName, args } = payload;
|
|
502
|
+
const result = await neurolink.executeTool(toolName, args);
|
|
503
|
+
return { type: "tool_result", toolName, result };
|
|
349
504
|
});
|
|
350
505
|
return {
|
|
351
506
|
onOpen: async (connection) => {
|
|
@@ -358,19 +513,31 @@ export function createAgentWebSocketHandler(_neurolink) {
|
|
|
358
513
|
}));
|
|
359
514
|
},
|
|
360
515
|
onMessage: async (connection, message) => {
|
|
516
|
+
// Best-effort only: a malformed frame still fails inside
|
|
517
|
+
// router.handle below (and reports as an id-less error, same as
|
|
518
|
+
// before), this just lets a *valid* frame's error response carry the
|
|
519
|
+
// same correlation id a successful response would have gotten.
|
|
520
|
+
const requestId = readIncomingRequestId(message);
|
|
361
521
|
try {
|
|
362
522
|
const result = await router.handle(connection, message);
|
|
363
523
|
if (result) {
|
|
364
|
-
|
|
365
|
-
socket.send(JSON.stringify(result));
|
|
524
|
+
trySend(connection, result);
|
|
366
525
|
}
|
|
367
526
|
}
|
|
368
527
|
catch (error) {
|
|
369
|
-
|
|
370
|
-
socket
|
|
528
|
+
// Guarded: this catch is the last line of defence, so a throw from
|
|
529
|
+
// the send itself — which is exactly what a closed socket does — has
|
|
530
|
+
// nothing above it and would surface as an unhandled rejection.
|
|
531
|
+
const errorFrame = {
|
|
371
532
|
type: "error",
|
|
372
533
|
error: error.message,
|
|
373
|
-
}
|
|
534
|
+
};
|
|
535
|
+
if (requestId !== undefined) {
|
|
536
|
+
errorFrame.id = requestId;
|
|
537
|
+
}
|
|
538
|
+
if (!trySend(connection, errorFrame)) {
|
|
539
|
+
logger.warn(`[AgentWebSocket] Could not deliver error frame, socket closed: ${connection.id}`);
|
|
540
|
+
}
|
|
374
541
|
}
|
|
375
542
|
},
|
|
376
543
|
onClose: async (connection, code, reason) => {
|
package/dist/types/cli.d.ts
CHANGED
|
@@ -1753,6 +1753,17 @@ export type CliClassifierRouterFlags = {
|
|
|
1753
1753
|
/** LLM-classifier hard timeout in ms (--classifier-timeout). */
|
|
1754
1754
|
classifierTimeout?: number;
|
|
1755
1755
|
};
|
|
1756
|
+
/**
|
|
1757
|
+
* `neurolink voices` arguments — TTS voice discovery.
|
|
1758
|
+
*/
|
|
1759
|
+
export type CliVoicesCommandArgs = {
|
|
1760
|
+
/** TTS provider whose voices to list (e.g. google-ai, openai-tts). */
|
|
1761
|
+
provider: string;
|
|
1762
|
+
/** Optional language filter passed through to the provider (e.g. en-US). */
|
|
1763
|
+
language?: string;
|
|
1764
|
+
/** Emit the raw list as JSON instead of a table. */
|
|
1765
|
+
json?: boolean;
|
|
1766
|
+
};
|
|
1756
1767
|
/**
|
|
1757
1768
|
* Agent command arguments for multi-agent orchestration
|
|
1758
1769
|
*/
|
package/dist/types/generate.d.ts
CHANGED
|
@@ -1453,6 +1453,21 @@ export type TextGenerationOptions = {
|
|
|
1453
1453
|
*/
|
|
1454
1454
|
outputFormat?: VisionImageOutputFormat;
|
|
1455
1455
|
};
|
|
1456
|
+
/**
|
|
1457
|
+
* Video processing options (mirrors `GenerateOptions.videoOptions`). Never
|
|
1458
|
+
* declared here before, so `buildGenerateTextOptions` had nowhere to
|
|
1459
|
+
* forward the caller's setting even once the allowlist itself named it.
|
|
1460
|
+
*/
|
|
1461
|
+
videoOptions?: {
|
|
1462
|
+
/** Frames to extract. Unset lets VideoProcessor pick from the clip's duration; clamped to 100. */
|
|
1463
|
+
frames?: number;
|
|
1464
|
+
/** Frame encoder quality, clamped to 1-100. Default 80. */
|
|
1465
|
+
quality?: number;
|
|
1466
|
+
/** Frame encoding. Default jpeg. */
|
|
1467
|
+
format?: "jpeg" | "png";
|
|
1468
|
+
/** Not implemented yet (#433) — warns rather than silently doing nothing. */
|
|
1469
|
+
transcribeAudio?: boolean;
|
|
1470
|
+
};
|
|
1456
1471
|
enableSummarization?: boolean;
|
|
1457
1472
|
/**
|
|
1458
1473
|
* Skip injecting tool schemas into the system prompt.
|
|
@@ -43,13 +43,22 @@ export type WorkerInstanceOptions = {
|
|
|
43
43
|
* Log bridge sink. Fire-and-forget: listener errors never disrupt the
|
|
44
44
|
* worker.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
46
|
+
* Receives only log events this worker emitted. The logger routes per
|
|
47
|
+
* instance: the worker's `generate` / `stream` / `generateText` run their
|
|
48
|
+
* bodies inside an AsyncLocalStorage scope carrying the worker's id, and
|
|
49
|
+
* this bridge is subscribed to that id — so a sibling worker's, the host's
|
|
50
|
+
* or a background MCP reconnect's logs never arrive here, and `tag` is
|
|
51
|
+
* true attribution rather than "whichever bridge forwarded it".
|
|
52
|
+
*
|
|
53
|
+
* Two things stay outside the scope, by construction:
|
|
54
|
+
* - Logs emitted while a **consumer drains a returned stream**. Iteration
|
|
55
|
+
* happens in the consumer's async context, after `stream()` resolved.
|
|
56
|
+
* - Logs emitted **outside any call** — construction, background MCP
|
|
57
|
+
* reconnects, module init. These are unattributed rather than charged to
|
|
58
|
+
* an arbitrary instance.
|
|
59
|
+
*
|
|
60
|
+
* A process-wide sink (`logger.setEventEmitter`) still sees everything, so
|
|
61
|
+
* a host bridge is unaffected by this narrowing.
|
|
53
62
|
*/
|
|
54
63
|
onLog?: (event: WorkerLogEvent) => void;
|
|
55
64
|
/**
|
|
@@ -246,6 +246,12 @@ export type StreamLoopArgs = {
|
|
|
246
246
|
totalTokens: number;
|
|
247
247
|
}) => void;
|
|
248
248
|
resolveFinish: (reason: string) => void;
|
|
249
|
+
/**
|
|
250
|
+
* `response_format` for this turn's requests, computed once up front from
|
|
251
|
+
* `options.schema` (see `suppressResponseFormatWithTools`). Absent when no
|
|
252
|
+
* schema was requested, or when tools suppress it.
|
|
253
|
+
*/
|
|
254
|
+
responseFormat?: OpenAICompatResponseFormat;
|
|
249
255
|
};
|
|
250
256
|
export type OpenAICompatBuildBodyArgs = {
|
|
251
257
|
modelId: string;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingface" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
|
|
2
|
-
export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingFace" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
|
|
1
|
+
export type CatalogProviderName = "api-route" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingface" | "inception-labs" | "io-intelligence" | "mancer" | "mistral" | "morph" | "novita" | "perplexity" | "sambanova" | "together-ai" | "upstage" | "xai";
|
|
2
|
+
export type CatalogCredentialKey = "apiRoute" | "baseten" | "cerebras" | "cloudflare" | "deepseek" | "fireworks" | "friendli" | "gmicloud" | "groq" | "huggingFace" | "inceptionLabs" | "ioIntelligence" | "mancer" | "mistral" | "morph" | "novita" | "perplexity" | "sambanova" | "together" | "upstage" | "xai";
|
|
@@ -229,6 +229,14 @@ export type NeurolinkCredentials = {
|
|
|
229
229
|
apiKey?: string;
|
|
230
230
|
baseURL?: string;
|
|
231
231
|
};
|
|
232
|
+
morph?: {
|
|
233
|
+
apiKey?: string;
|
|
234
|
+
baseURL?: string;
|
|
235
|
+
};
|
|
236
|
+
novita?: {
|
|
237
|
+
apiKey?: string;
|
|
238
|
+
baseURL?: string;
|
|
239
|
+
};
|
|
232
240
|
perplexity?: {
|
|
233
241
|
apiKey?: string;
|
|
234
242
|
baseURL?: string;
|
|
@@ -1761,8 +1769,31 @@ export type ProviderHealthCheckOptions = {
|
|
|
1761
1769
|
includeConnectivityTest?: boolean;
|
|
1762
1770
|
includeModelValidation?: boolean;
|
|
1763
1771
|
cacheResults?: boolean;
|
|
1772
|
+
/**
|
|
1773
|
+
* Max age (ms) of a cached health-check result before it is treated as
|
|
1774
|
+
* stale. Only consulted when `cacheResults` is true — with
|
|
1775
|
+
* `cacheResults: false` this option has no effect. It does not affect the
|
|
1776
|
+
* circuit breaker's blacklist expiry, which uses its own fixed window
|
|
1777
|
+
* independent of any caller's `maxCacheAge`.
|
|
1778
|
+
*/
|
|
1764
1779
|
maxCacheAge?: number;
|
|
1765
1780
|
};
|
|
1781
|
+
/**
|
|
1782
|
+
* Outcome of a provider-specific config check's own outbound runtime probe
|
|
1783
|
+
* (LiteLLM's `/v1/models`, Ollama's `/api/tags` availability check) — the
|
|
1784
|
+
* request `checkLiteLLMConfig`/`checkOllamaConfig` make from inside step 1
|
|
1785
|
+
* (`checkEnvironmentConfiguration`), independent of the step-3 connectivity
|
|
1786
|
+
* test. `checkProviderHealth`'s circuit breaker needs to know whether this
|
|
1787
|
+
* probe ran at all (a blacklisted provider skips it — `ran: false`) and,
|
|
1788
|
+
* if it ran, whether it failed, so a dead local proxy counts toward the
|
|
1789
|
+
* breaker the same way a failing step-3 probe does. `ran: false` must never
|
|
1790
|
+
* move the breaker either way — it is not evidence the provider is up OR
|
|
1791
|
+
* down.
|
|
1792
|
+
*/
|
|
1793
|
+
export type ProviderRuntimeProbeOutcome = {
|
|
1794
|
+
ran: boolean;
|
|
1795
|
+
failed: boolean;
|
|
1796
|
+
};
|
|
1766
1797
|
/**
|
|
1767
1798
|
* Amazon Bedrock specific types
|
|
1768
1799
|
*/
|
|
@@ -2023,14 +2054,30 @@ export type ProviderDescriptor = {
|
|
|
2023
2054
|
/** How ProviderHealthChecker should verify this provider is reachable. */
|
|
2024
2055
|
healthCheck: "env-only" | "models-probe" | "live-generate";
|
|
2025
2056
|
/**
|
|
2026
|
-
*
|
|
2057
|
+
* ORDER (not membership) in the default health sweep
|
|
2027
2058
|
* (`ProviderHealthChecker.checkAllProvidersHealth` with no explicit
|
|
2028
2059
|
* list). Lower number = checked and reported first; the sweep's array
|
|
2029
2060
|
* order is behaviour for its first-healthy fallback consumers. Absent =
|
|
2030
|
-
*
|
|
2031
|
-
*
|
|
2061
|
+
* sorted after every explicitly-prioritized descriptor, in this file's
|
|
2062
|
+
* declaration order (a stable sort, so ties never reorder). Originally
|
|
2063
|
+
* this field ALSO controlled membership (absent = excluded), which
|
|
2064
|
+
* silently dropped every provider added without it from the sweep —
|
|
2065
|
+
* issue #1305, ~30 of ~38 registered providers, undocumented. Membership
|
|
2066
|
+
* is now `excludeFromHealthSweep` below, which every new descriptor
|
|
2067
|
+
* satisfies by default without needing this field at all.
|
|
2032
2068
|
*/
|
|
2033
2069
|
defaultHealthSweepPriority?: number;
|
|
2070
|
+
/**
|
|
2071
|
+
* Opt a provider OUT of `checkAllProvidersHealth`'s default sweep. Every
|
|
2072
|
+
* registered descriptor participates by default — a newly added provider
|
|
2073
|
+
* needs no action to be included, which is the fix for issue #1305
|
|
2074
|
+
* (silent, undocumented exclusion of most providers). Set this only for a
|
|
2075
|
+
* descriptor that genuinely should never appear in a health rollup; no
|
|
2076
|
+
* current descriptor sets it. `checkProviderHealth()` for a single named
|
|
2077
|
+
* provider is unaffected either way — this only gates the "all
|
|
2078
|
+
* providers" sweep.
|
|
2079
|
+
*/
|
|
2080
|
+
excludeFromHealthSweep?: true;
|
|
2034
2081
|
/**
|
|
2035
2082
|
* Preference rank for `getBestHealthyProvider`'s default auto-selection
|
|
2036
2083
|
* (lower = tried first). Deliberately a SEPARATE ordering from the sweep:
|
package/dist/types/server.d.ts
CHANGED
|
@@ -682,6 +682,14 @@ export type WebSocketMessage = {
|
|
|
682
682
|
data: string | ArrayBuffer;
|
|
683
683
|
timestamp: number;
|
|
684
684
|
};
|
|
685
|
+
/**
|
|
686
|
+
* Client-supplied correlation id on an agent-protocol WebSocket message
|
|
687
|
+
* (`{ type, payload, id }`). Echoed back on the matching response frame so a
|
|
688
|
+
* caller with more than one in-flight message on a connection can tell which
|
|
689
|
+
* frame answers which request; optional and ignored entirely by a client
|
|
690
|
+
* that never sends one.
|
|
691
|
+
*/
|
|
692
|
+
export type WebSocketRequestId = string | number;
|
|
685
693
|
/**
|
|
686
694
|
* Authenticated user information
|
|
687
695
|
*/
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -838,6 +838,36 @@ export type StreamResult = {
|
|
|
838
838
|
stopReason?: GenerateStopReason;
|
|
839
839
|
rawFinishReason?: string;
|
|
840
840
|
stepsUsed?: number;
|
|
841
|
+
/**
|
|
842
|
+
* Parsed structured output for a `stream({ schema })` turn, available
|
|
843
|
+
* AFTER the stream has been drained.
|
|
844
|
+
*
|
|
845
|
+
* Lives on metadata for the same reason `finishReason` above does: it is a
|
|
846
|
+
* mutable reference the loop fills in, so result-object spreads in stream
|
|
847
|
+
* wrappers cannot snapshot it before it resolves. A plain field rather than
|
|
848
|
+
* a promise, deliberately — when a middleware short-circuits the request
|
|
849
|
+
* and the loop never runs, an unresolved promise would hang every reader,
|
|
850
|
+
* whereas an absent field is simply absent. Readers must tolerate that:
|
|
851
|
+
* absence means the model never produced the object.
|
|
852
|
+
*/
|
|
853
|
+
structuredData?: unknown;
|
|
854
|
+
/**
|
|
855
|
+
* Tokens spent by the tool-free re-ask that produced `structuredData`,
|
|
856
|
+
* when one was needed.
|
|
857
|
+
*
|
|
858
|
+
* That re-ask is a second, separately-billed model call, and the stream's
|
|
859
|
+
* own `usage` has already resolved by the time it runs — so folding these
|
|
860
|
+
* tokens into it would mutate a value a caller may have read, and leaving
|
|
861
|
+
* them out entirely would under-report what the turn cost. Reported here
|
|
862
|
+
* instead: same delivery as `structuredData`, filled at the same moment,
|
|
863
|
+
* read at the same moment. Absent when no re-ask was needed, which is the
|
|
864
|
+
* common case. The generate path accounts for its equivalent re-ask
|
|
865
|
+
* inline, since there the usage has not been handed out yet.
|
|
866
|
+
*/
|
|
867
|
+
structuredDataUsage?: {
|
|
868
|
+
inputTokens: number;
|
|
869
|
+
outputTokens: number;
|
|
870
|
+
};
|
|
841
871
|
thoughtSignature?: string;
|
|
842
872
|
thoughts?: Array<{
|
|
843
873
|
id?: string;
|
package/dist/types/tts.d.ts
CHANGED
|
@@ -164,6 +164,16 @@ export declare const VALID_AUDIO_FORMATS: readonly TTSAudioFormat[];
|
|
|
164
164
|
export declare const VALID_TTS_QUALITIES: readonly TTSQuality[];
|
|
165
165
|
/** Valid Google TTS audio formats */
|
|
166
166
|
export type GoogleAudioEncoding = "MP3" | "LINEAR16" | "OGG_OPUS";
|
|
167
|
+
/**
|
|
168
|
+
* Audio encodings Google's *streaming* synthesis endpoint accepts.
|
|
169
|
+
*
|
|
170
|
+
* Deliberately a separate type from {@link GoogleAudioEncoding}: the
|
|
171
|
+
* batch and streaming endpoints do not accept the same set. `MP3` and
|
|
172
|
+
* `LINEAR16` are valid for batch and rejected by streaming with
|
|
173
|
+
* `INVALID_ARGUMENT: Unsupported audio encoding`, while `PCM` — raw
|
|
174
|
+
* 16-bit signed LE, headerless — exists only on the streaming side.
|
|
175
|
+
*/
|
|
176
|
+
export type GoogleStreamingAudioEncoding = "PCM" | "OGG_OPUS";
|
|
167
177
|
/**
|
|
168
178
|
* Type guard to check if an object is a TTSResult
|
|
169
179
|
*/
|
|
@@ -65,9 +65,24 @@ export type LogEntry = {
|
|
|
65
65
|
/** Optional additional data associated with the log entry (objects, arrays, etc.) */
|
|
66
66
|
data?: unknown;
|
|
67
67
|
};
|
|
68
|
+
/**
|
|
69
|
+
* Minimal emitter shape the logger forwards `"log-event"` payloads to.
|
|
70
|
+
*
|
|
71
|
+
* Structurally satisfied by `node:events`' `EventEmitter` and by NeuroLink's
|
|
72
|
+
* own typed emitter, so a sink can be either.
|
|
73
|
+
*/
|
|
74
|
+
export type LogEventEmitter = {
|
|
75
|
+
emit: (event: string, ...args: unknown[]) => boolean;
|
|
76
|
+
};
|
|
68
77
|
/**
|
|
69
78
|
* Logger interface matching the logger object shape
|
|
70
79
|
* Used for SDK tool contexts and other components that need a logger
|
|
80
|
+
*
|
|
81
|
+
* Deliberately a subset of the real logger: the per-instance routing methods
|
|
82
|
+
* (`runInInstanceScope`, `addScopedEventEmitter`, …) are internal plumbing
|
|
83
|
+
* between the SDK entry points and the logger, and this type is a structural
|
|
84
|
+
* contract a caller can satisfy — `SDKToolContext.logger`. Adding required
|
|
85
|
+
* members here would break anyone constructing that context themselves.
|
|
71
86
|
*/
|
|
72
87
|
export type Logger = {
|
|
73
88
|
debug: (...args: unknown[]) => void;
|
|
@@ -79,9 +94,7 @@ export type Logger = {
|
|
|
79
94
|
setLogLevel: (level: LogLevel) => void;
|
|
80
95
|
getLogs: (level?: LogLevel) => LogEntry[];
|
|
81
96
|
clearLogs: () => void;
|
|
82
|
-
setEventEmitter: (emitter:
|
|
83
|
-
emit: (event: string, ...args: unknown[]) => boolean;
|
|
84
|
-
}) => void;
|
|
97
|
+
setEventEmitter: (emitter: LogEventEmitter) => void;
|
|
85
98
|
clearEventEmitter: () => void;
|
|
86
99
|
};
|
|
87
100
|
export type StructuredError = {
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* - Structured data support for complex objects
|
|
14
14
|
* - Tabular data display
|
|
15
15
|
*/
|
|
16
|
-
import type { LogEntry, LogLevel } from "../types/index.js";
|
|
16
|
+
import type { LogEntry, LogEventEmitter, LogLevel } from "../types/index.js";
|
|
17
17
|
declare class NeuroLinkLogger {
|
|
18
18
|
private logLevel;
|
|
19
19
|
private logs;
|
|
@@ -21,6 +21,7 @@ declare class NeuroLinkLogger {
|
|
|
21
21
|
private isDebugMode;
|
|
22
22
|
private diagnosticsToStderr;
|
|
23
23
|
private eventEmitter?;
|
|
24
|
+
private readonly scopedEmitters;
|
|
24
25
|
constructor();
|
|
25
26
|
/**
|
|
26
27
|
* Sets the event emitter that will receive log events.
|
|
@@ -28,9 +29,7 @@ declare class NeuroLinkLogger {
|
|
|
28
29
|
*
|
|
29
30
|
* @param emitter - The event emitter instance
|
|
30
31
|
*/
|
|
31
|
-
setEventEmitter(emitter:
|
|
32
|
-
emit: (event: string, ...args: unknown[]) => boolean;
|
|
33
|
-
}): void;
|
|
32
|
+
setEventEmitter(emitter: LogEventEmitter): void;
|
|
34
33
|
/**
|
|
35
34
|
* Clears the event emitter reference.
|
|
36
35
|
* Should be called when a NeuroLink instance is disposed to prevent memory
|
|
@@ -40,9 +39,54 @@ declare class NeuroLinkLogger {
|
|
|
40
39
|
*
|
|
41
40
|
* @param ifEmitter - When provided, clear only if it is the current emitter
|
|
42
41
|
*/
|
|
43
|
-
clearEventEmitter(ifEmitter?:
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
clearEventEmitter(ifEmitter?: LogEventEmitter): void;
|
|
43
|
+
/**
|
|
44
|
+
* Runs `fn` with every log call beneath it attributed to `instanceId`.
|
|
45
|
+
*
|
|
46
|
+
* Nesting is safe and expected: a host turn that delegates to a worker ends
|
|
47
|
+
* up with the worker's id on top for the duration of the worker's call, and
|
|
48
|
+
* the host's id restored afterwards. Re-entering with the same id is a
|
|
49
|
+
* no-op in effect.
|
|
50
|
+
*
|
|
51
|
+
* @param instanceId - Identifier of the emitting NeuroLink instance
|
|
52
|
+
* @param fn - Work to run inside the scope
|
|
53
|
+
* @returns Whatever `fn` returns
|
|
54
|
+
*/
|
|
55
|
+
runInInstanceScope<T>(instanceId: string, fn: () => T): T;
|
|
56
|
+
/**
|
|
57
|
+
* The instance id currently attributed, or undefined outside any scope.
|
|
58
|
+
*/
|
|
59
|
+
getInstanceScope(): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Subscribes `emitter` to log events emitted by one instance only.
|
|
62
|
+
*
|
|
63
|
+
* Unlike {@link setEventEmitter} — a single process-wide sink that receives
|
|
64
|
+
* everything — a scoped emitter receives only events logged inside that
|
|
65
|
+
* instance's {@link runInInstanceScope}. Several emitters may share an id.
|
|
66
|
+
*
|
|
67
|
+
* @param instanceId - Instance whose events this emitter should receive
|
|
68
|
+
* @param emitter - The sink to subscribe
|
|
69
|
+
*/
|
|
70
|
+
addScopedEventEmitter(instanceId: string, emitter: LogEventEmitter): void;
|
|
71
|
+
/**
|
|
72
|
+
* Unsubscribes a scoped emitter. Must be called when the owning instance is
|
|
73
|
+
* disposed, or the emitter — and everything it closes over — is retained by
|
|
74
|
+
* the process-global logger for the lifetime of the process.
|
|
75
|
+
*
|
|
76
|
+
* @param instanceId - Instance the emitter was registered against
|
|
77
|
+
* @param emitter - The sink to unsubscribe
|
|
78
|
+
*/
|
|
79
|
+
removeScopedEventEmitter(instanceId: string, emitter: LogEventEmitter): void;
|
|
80
|
+
/**
|
|
81
|
+
* Drops every scoped emitter registered against an instance.
|
|
82
|
+
*
|
|
83
|
+
* Called from `NeuroLink.dispose()`: the registry lives on the
|
|
84
|
+
* process-global logger, so an instance that goes away without clearing its
|
|
85
|
+
* own entry would keep its sinks — and their closures — reachable forever.
|
|
86
|
+
*
|
|
87
|
+
* @param instanceId - Instance whose sinks should be dropped
|
|
88
|
+
*/
|
|
89
|
+
clearScopedEventEmitters(instanceId: string): void;
|
|
46
90
|
/**
|
|
47
91
|
* Sets the minimum log level that will be processed and output.
|
|
48
92
|
* Log messages with a level lower than this will be ignored.
|
|
@@ -234,12 +278,13 @@ export declare const logger: {
|
|
|
234
278
|
setDiagnosticsToStderr: (enabled: boolean) => void;
|
|
235
279
|
getLogs: (level?: LogLevel) => LogEntry[];
|
|
236
280
|
clearLogs: () => void;
|
|
237
|
-
setEventEmitter: (emitter:
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
281
|
+
setEventEmitter: (emitter: LogEventEmitter) => void;
|
|
282
|
+
clearEventEmitter: (ifEmitter?: LogEventEmitter) => void;
|
|
283
|
+
runInInstanceScope: <T>(instanceId: string, fn: () => T) => T;
|
|
284
|
+
getInstanceScope: () => string | undefined;
|
|
285
|
+
addScopedEventEmitter: (instanceId: string, emitter: LogEventEmitter) => void;
|
|
286
|
+
removeScopedEventEmitter: (instanceId: string, emitter: LogEventEmitter) => void;
|
|
287
|
+
clearScopedEventEmitters: (instanceId: string) => void;
|
|
243
288
|
};
|
|
244
289
|
/**
|
|
245
290
|
* MCP compatibility exports - all use the same unified logger instance.
|