@juspay/neurolink 13.3.0 → 13.4.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 +2 -2
- package/dist/types/livekit.d.ts +37 -11
- package/dist/voice/livekit/config.d.ts +13 -8
- package/dist/voice/livekit/config.js +72 -8
- package/dist/voice/livekit/index.d.ts +1 -0
- package/dist/voice/livekit/index.js +1 -0
- package/dist/voice/livekit/realtimeMcpTools.d.ts +8 -7
- package/dist/voice/livekit/realtimeMcpTools.js +10 -9
- package/dist/voice/livekit/realtimeMemory.d.ts +11 -3
- package/dist/voice/livekit/realtimeMemory.js +23 -5
- package/dist/voice/livekit/realtimeModel.d.ts +33 -0
- package/dist/voice/livekit/realtimeModel.js +89 -0
- package/dist/voice/livekit/realtimeVoiceAgent.d.ts +10 -6
- package/dist/voice/livekit/realtimeVoiceAgent.js +42 -39
- package/dist/voice/livekit/schemaSanitizer.d.ts +2 -0
- package/dist/voice/livekit/schemaSanitizer.js +2 -0
- package/docs-site/static/search-index.json +11 -3
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
## [13.
|
|
1
|
+
## [13.4.0](https://github.com/juspay/neurolink/compare/v13.3.0...v13.4.0) (2026-10-18)
|
|
2
2
|
|
|
3
3
|
### Features
|
|
4
4
|
|
|
5
|
-
- **(
|
|
5
|
+
- **(voice):** add an xAI realtime backend beside Gemini Live ([7ae5ca4](https://github.com/juspay/neurolink/commit/7ae5ca4c5b313e8ca50c5c20d8fc33d1fa529ac5))
|
|
6
6
|
|
|
7
7
|
## [11.2.3](https://github.com/juspay/neurolink/compare/v11.2.2...v11.2.3) (2026-08-19)
|
|
8
8
|
|
package/dist/types/livekit.d.ts
CHANGED
|
@@ -421,30 +421,52 @@ export type LiveKitServerCredentials = {
|
|
|
421
421
|
apiKey: string;
|
|
422
422
|
apiSecret: string;
|
|
423
423
|
};
|
|
424
|
+
/**
|
|
425
|
+
* The realtime speech-to-speech model a call runs on: Gemini Live on Vertex
|
|
426
|
+
* (the default) or xAI Grok Voice through LiveKit's xAI plugin.
|
|
427
|
+
*/
|
|
428
|
+
export type RealtimeVoiceBackend = "gemini" | "xai";
|
|
429
|
+
/**
|
|
430
|
+
* A LiveKit realtime model as far as callers of the model factory can rely on.
|
|
431
|
+
* It is declared structurally so the published declarations name no optional
|
|
432
|
+
* LiveKit plugin; at runtime it is a `RealtimeModel` subclass of the backend's
|
|
433
|
+
* plugin.
|
|
434
|
+
*/
|
|
435
|
+
export type RealtimeLlm = {
|
|
436
|
+
readonly model: string;
|
|
437
|
+
label: () => string;
|
|
438
|
+
close: () => Promise<void>;
|
|
439
|
+
};
|
|
424
440
|
/**
|
|
425
441
|
* Realtime voice configuration resolved from the environment.
|
|
426
442
|
*
|
|
427
|
-
* In speech-to-speech mode one realtime model (Gemini Live on Vertex
|
|
428
|
-
* reasoning, TTS, and turn detection — so there is no
|
|
429
|
-
* config. `resolveRealtimeVoiceConfig` fills every
|
|
430
|
-
* (with defaults); `RealtimeVoiceAgentConfig` lets a
|
|
443
|
+
* In speech-to-speech mode one realtime model (Gemini Live on Vertex, or xAI
|
|
444
|
+
* Grok Voice) does STT, reasoning, TTS, and turn detection — so there is no
|
|
445
|
+
* separate STT/TTS/VAD/EOU config. `resolveRealtimeVoiceConfig` fills every
|
|
446
|
+
* field from `process.env` (with defaults); `RealtimeVoiceAgentConfig` lets a
|
|
447
|
+
* caller override any of them. The API key of the xai backend is deliberately
|
|
448
|
+
* not part of the config: the xAI plugin reads `XAI_API_KEY` itself.
|
|
431
449
|
*/
|
|
432
450
|
export type RealtimeVoiceConfig = {
|
|
433
|
-
/**
|
|
451
|
+
/** Which realtime model family handles the call (VOICE_REALTIME_BACKEND; default "gemini"). */
|
|
452
|
+
backend: RealtimeVoiceBackend;
|
|
453
|
+
/** Vertex project id (from VERTEX_PROJECT / GOOGLE_AUTH_* / GOOGLE_CLOUD_PROJECT_ID). Gemini only. */
|
|
434
454
|
project: string | undefined;
|
|
435
|
-
/** Vertex location; native-audio Live is served on `global`, not regionally. */
|
|
455
|
+
/** Vertex location; native-audio Live is served on `global`, not regionally. Gemini only. */
|
|
436
456
|
location: string;
|
|
437
|
-
/** Realtime model id (e.g. "gemini-live-2.5-flash"). */
|
|
457
|
+
/** Realtime model id (e.g. "gemini-live-2.5-flash" for gemini, "grok-voice-3.0" for xai). */
|
|
438
458
|
model: string;
|
|
439
|
-
/** Optional
|
|
459
|
+
/** Optional voice name for either backend; omit for the plugin default. */
|
|
440
460
|
voice: string | undefined;
|
|
441
|
-
/**
|
|
461
|
+
/** xai only: WebSocket base URL of the realtime endpoint (XAI_REALTIME_BASE_URL); unset keeps the plugin's own endpoint. */
|
|
462
|
+
xaiBaseURL: string | undefined;
|
|
463
|
+
/** Response modality: "AUDIO" (native S2S) or "TEXT" (half-cascade). Gemini only. */
|
|
442
464
|
responseModality: string;
|
|
443
465
|
/** System prompt / instructions for the agent. */
|
|
444
466
|
systemPrompt: string;
|
|
445
467
|
/** Opening line the agent speaks on connect ("" disables). */
|
|
446
468
|
greeting: string;
|
|
447
|
-
/** Whether to bridge Lighthouse MCP tools as
|
|
469
|
+
/** Whether to bridge Lighthouse MCP tools as function tools of the realtime model (either backend). */
|
|
448
470
|
toolsEnabled: boolean;
|
|
449
471
|
/** Full URL of the MCP server the tools are bridged from. */
|
|
450
472
|
mcpUrl: string;
|
|
@@ -474,10 +496,14 @@ export type RealtimeVoiceLogContext = {
|
|
|
474
496
|
* everything via env.
|
|
475
497
|
*/
|
|
476
498
|
export type RealtimeVoiceAgentConfig = {
|
|
499
|
+
/** Realtime backend; omitted means the environment's VOICE_REALTIME_BACKEND, and without it "gemini". */
|
|
500
|
+
backend?: RealtimeVoiceBackend;
|
|
477
501
|
project?: string;
|
|
478
502
|
location?: string;
|
|
479
503
|
model?: string;
|
|
480
504
|
voice?: string;
|
|
505
|
+
/** xai only: WebSocket base URL of the realtime endpoint. */
|
|
506
|
+
xaiBaseURL?: string;
|
|
481
507
|
responseModality?: string;
|
|
482
508
|
systemPrompt?: string;
|
|
483
509
|
greeting?: string;
|
|
@@ -736,7 +762,7 @@ export type BuildRealtimeMcpToolsParams = {
|
|
|
736
762
|
* Hard cap per MCP tool call, in milliseconds (default 30000).
|
|
737
763
|
*
|
|
738
764
|
* Without one, a stalled MCP server holds the realtime turn open forever:
|
|
739
|
-
*
|
|
765
|
+
* The realtime model waits on the function result, so the user gets silence rather than
|
|
740
766
|
* an error. Bounding the call turns that into a normal tool failure the
|
|
741
767
|
* model can talk about.
|
|
742
768
|
*/
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* See docs/features/livekit-voice-agent.md.
|
|
9
9
|
*/
|
|
10
|
-
import type { LiveKitServerConfig, LiveKitBrainDefaults, RealtimeVoiceConfig } from "../../types/index.js";
|
|
10
|
+
import type { LiveKitServerConfig, LiveKitBrainDefaults, RealtimeVoiceAgentConfig, RealtimeVoiceConfig } from "../../types/index.js";
|
|
11
11
|
/**
|
|
12
12
|
* Resolve LiveKit server connection settings from the environment.
|
|
13
13
|
*
|
|
@@ -24,16 +24,21 @@ export declare function resolveLiveKitServerConfig(): LiveKitServerConfig;
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function resolveBrainDefaults(): LiveKitBrainDefaults;
|
|
26
26
|
/**
|
|
27
|
-
* Resolve the realtime (
|
|
28
|
-
*
|
|
27
|
+
* Resolve the realtime (speech-to-speech) voice configuration from the
|
|
28
|
+
* environment.
|
|
29
29
|
*
|
|
30
30
|
* Every value has a safe default so a worker can run with no realtime-specific
|
|
31
|
-
* env at all.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
31
|
+
* env at all. `VOICE_REALTIME_BACKEND` picks the model family (`gemini`, the
|
|
32
|
+
* default, or `xai`); `backend` here overrides it and then the environment's
|
|
33
|
+
* value is not read. A non-blank `xaiBaseURL` here likewise replaces
|
|
34
|
+
* `XAI_REALTIME_BASE_URL` after the same ws or wss check.
|
|
35
|
+
* `VOICE_LIVE_MODEL` overrides the backend's default model.
|
|
36
|
+
* Vertex `project` is resolved from `VERTEX_PROJECT`, then the service-account
|
|
37
|
+
* project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then `GOOGLE_CLOUD_PROJECT_ID`;
|
|
38
|
+
* project, location and modality only matter to the gemini backend. The MCP URL
|
|
39
|
+
* is `VOICE_MCP_URL` if set, otherwise `${LIGHTHOUSE_URL}/ai/mcp/v2`.
|
|
35
40
|
*/
|
|
36
|
-
export declare function resolveRealtimeVoiceConfig(): RealtimeVoiceConfig;
|
|
41
|
+
export declare function resolveRealtimeVoiceConfig(overrides?: Pick<RealtimeVoiceAgentConfig, "backend" | "xaiBaseURL">): RealtimeVoiceConfig;
|
|
37
42
|
/**
|
|
38
43
|
* Resolve the semantic end-of-utterance (EOU) turn-detection settings.
|
|
39
44
|
*
|
|
@@ -68,6 +68,9 @@ export function resolveBrainDefaults() {
|
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
const DEFAULT_REALTIME_MODEL = "gemini-live-2.5-flash";
|
|
71
|
+
// xAI's documented flagship Grok Voice id. Never exercised against the real
|
|
72
|
+
// service; the plugin's own default is `grok-voice-think-fast-1.0`.
|
|
73
|
+
const DEFAULT_XAI_REALTIME_MODEL = "grok-voice-3.0";
|
|
71
74
|
const DEFAULT_REALTIME_LOCATION = "global";
|
|
72
75
|
const DEFAULT_RESPONSE_MODALITY = "AUDIO";
|
|
73
76
|
const DEFAULT_SYSTEM_PROMPT = "You are a concise voice assistant for a merchant dashboard. Keep replies short and spoken.";
|
|
@@ -75,25 +78,86 @@ const DEFAULT_GREETING = "Briefly greet the merchant and ask how you can help wi
|
|
|
75
78
|
const DEFAULT_LIGHTHOUSE_URL = "http://localhost:5173";
|
|
76
79
|
const DEFAULT_MCP_PATH = "/ai/mcp/v2";
|
|
77
80
|
/**
|
|
78
|
-
*
|
|
79
|
-
* the
|
|
81
|
+
* The backend named by `VOICE_REALTIME_BACKEND`: `gemini` or `xai`, matched
|
|
82
|
+
* case-insensitively, with blank or absent meaning `gemini`. The error names the
|
|
83
|
+
* variable and the choices and repeats nothing from the environment.
|
|
84
|
+
*/
|
|
85
|
+
function readRealtimeBackend() {
|
|
86
|
+
const value = readEnv("VOICE_REALTIME_BACKEND", "gemini").toLowerCase();
|
|
87
|
+
if (value === "gemini" || value === "xai") {
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
throw new Error('VOICE_REALTIME_BACKEND must be "gemini" or "xai" (leave it unset for gemini)');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A backend passed in code. The type already limits it, but a plain-JavaScript
|
|
94
|
+
* caller is not held by the type, and a typo would otherwise take the xai
|
|
95
|
+
* branch and fail on a missing key. The error repeats nothing it was given.
|
|
96
|
+
*/
|
|
97
|
+
function requireKnownBackend(value) {
|
|
98
|
+
if (value === "gemini" || value === "xai") {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
throw new Error('The realtime voice backend must be "gemini" or "xai"');
|
|
102
|
+
}
|
|
103
|
+
function isWebSocketUrl(value) {
|
|
104
|
+
try {
|
|
105
|
+
const { protocol } = new URL(value);
|
|
106
|
+
return protocol === "ws:" || protocol === "wss:";
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** A ws or wss URL. The error names the setting and does not repeat the value. */
|
|
113
|
+
function requireWebSocketUrl(value, name) {
|
|
114
|
+
if (!isWebSocketUrl(value)) {
|
|
115
|
+
throw new Error(`${name} must be a ws:// or wss:// URL`);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
/** `XAI_REALTIME_BASE_URL`, which must be a ws or wss URL when set. */
|
|
120
|
+
function readXaiBaseUrl() {
|
|
121
|
+
const value = readOptionalEnv("XAI_REALTIME_BASE_URL");
|
|
122
|
+
return value === undefined
|
|
123
|
+
? undefined
|
|
124
|
+
: requireWebSocketUrl(value, "XAI_REALTIME_BASE_URL");
|
|
125
|
+
}
|
|
126
|
+
/** A caller-supplied `xaiBaseURL` gets the same check as the variable; blank means unset, so the variable applies. */
|
|
127
|
+
function resolveXaiBaseUrl(override) {
|
|
128
|
+
const value = override?.trim();
|
|
129
|
+
return value ? requireWebSocketUrl(value, "xaiBaseURL") : readXaiBaseUrl();
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the realtime (speech-to-speech) voice configuration from the
|
|
133
|
+
* environment.
|
|
80
134
|
*
|
|
81
135
|
* Every value has a safe default so a worker can run with no realtime-specific
|
|
82
|
-
* env at all.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
136
|
+
* env at all. `VOICE_REALTIME_BACKEND` picks the model family (`gemini`, the
|
|
137
|
+
* default, or `xai`); `backend` here overrides it and then the environment's
|
|
138
|
+
* value is not read. A non-blank `xaiBaseURL` here likewise replaces
|
|
139
|
+
* `XAI_REALTIME_BASE_URL` after the same ws or wss check.
|
|
140
|
+
* `VOICE_LIVE_MODEL` overrides the backend's default model.
|
|
141
|
+
* Vertex `project` is resolved from `VERTEX_PROJECT`, then the service-account
|
|
142
|
+
* project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then `GOOGLE_CLOUD_PROJECT_ID`;
|
|
143
|
+
* project, location and modality only matter to the gemini backend. The MCP URL
|
|
144
|
+
* is `VOICE_MCP_URL` if set, otherwise `${LIGHTHOUSE_URL}/ai/mcp/v2`.
|
|
86
145
|
*/
|
|
87
|
-
export function resolveRealtimeVoiceConfig() {
|
|
146
|
+
export function resolveRealtimeVoiceConfig(overrides = {}) {
|
|
147
|
+
const backend = overrides.backend === undefined
|
|
148
|
+
? readRealtimeBackend()
|
|
149
|
+
: requireKnownBackend(overrides.backend);
|
|
88
150
|
const lighthouseUrl = readEnv("LIGHTHOUSE_URL", DEFAULT_LIGHTHOUSE_URL);
|
|
89
151
|
const mcpUrl = readEnv("VOICE_MCP_URL", `${lighthouseUrl}${DEFAULT_MCP_PATH}`);
|
|
90
152
|
return {
|
|
153
|
+
backend,
|
|
91
154
|
project: readOptionalEnv("VERTEX_PROJECT") ??
|
|
92
155
|
readOptionalEnv("GOOGLE_AUTH_BREEZE_PROJECT_ID") ??
|
|
93
156
|
readOptionalEnv("GOOGLE_CLOUD_PROJECT_ID"),
|
|
94
157
|
location: readEnv("VERTEX_LOCATION", DEFAULT_REALTIME_LOCATION),
|
|
95
|
-
model: readEnv("VOICE_LIVE_MODEL", DEFAULT_REALTIME_MODEL),
|
|
158
|
+
model: readEnv("VOICE_LIVE_MODEL", backend === "xai" ? DEFAULT_XAI_REALTIME_MODEL : DEFAULT_REALTIME_MODEL),
|
|
96
159
|
voice: readOptionalEnv("VOICE_S2S_VOICE"),
|
|
160
|
+
xaiBaseURL: resolveXaiBaseUrl(overrides.xaiBaseURL),
|
|
97
161
|
responseModality: readEnv("VOICE_RESPONSE_MODALITY", DEFAULT_RESPONSE_MODALITY).toUpperCase(),
|
|
98
162
|
systemPrompt: readEnv("VOICE_SYSTEM_PROMPT", DEFAULT_SYSTEM_PROMPT),
|
|
99
163
|
greeting: readEnv("VOICE_GREETING", DEFAULT_GREETING),
|
|
@@ -15,6 +15,7 @@ export { mintJoinToken } from "./tokens.js";
|
|
|
15
15
|
export { createVoiceRoom, dispatchVoiceAgent } from "./roomDispatch.js";
|
|
16
16
|
export { defineVoiceAgent } from "./voiceAgent.js";
|
|
17
17
|
export { defineRealtimeVoiceAgent } from "./realtimeVoiceAgent.js";
|
|
18
|
+
export { createRealtimeLlm } from "./realtimeModel.js";
|
|
18
19
|
export { startVoiceAgentWorker, startRealtimeVoiceAgentWorker, installVoiceWorkerProcessGuards, } from "./voiceAgentWorker.js";
|
|
19
20
|
export { ensureVertexAdc, clearGeminiApiKeyEnv } from "./vertexAuth.js";
|
|
20
21
|
export { readCallContextFromRoom } from "./roomContext.js";
|
|
@@ -15,6 +15,7 @@ export { mintJoinToken } from "./tokens.js";
|
|
|
15
15
|
export { createVoiceRoom, dispatchVoiceAgent } from "./roomDispatch.js";
|
|
16
16
|
export { defineVoiceAgent } from "./voiceAgent.js";
|
|
17
17
|
export { defineRealtimeVoiceAgent } from "./realtimeVoiceAgent.js";
|
|
18
|
+
export { createRealtimeLlm } from "./realtimeModel.js";
|
|
18
19
|
export { startVoiceAgentWorker, startRealtimeVoiceAgentWorker, installVoiceWorkerProcessGuards, } from "./voiceAgentWorker.js";
|
|
19
20
|
export { ensureVertexAdc, clearGeminiApiKeyEnv } from "./vertexAuth.js";
|
|
20
21
|
export { readCallContextFromRoom } from "./roomContext.js";
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP tools →
|
|
2
|
+
* MCP tools → realtime function tools bridge (realtime mode).
|
|
3
3
|
*
|
|
4
|
-
* In speech-to-speech mode
|
|
5
|
-
* server's tools are registered as
|
|
6
|
-
* NeuroLink tools. This connects to the MCP
|
|
7
|
-
* (`x-auth-token` + base64 `x-context`), sanitizes
|
|
8
|
-
* Gemini-safe subset
|
|
9
|
-
*
|
|
4
|
+
* In speech-to-speech mode the realtime model (Gemini Live, or xAI Grok Voice)
|
|
5
|
+
* owns the tool loop, so an external MCP server's tools are registered as
|
|
6
|
+
* LiveKit function tools rather than NeuroLink tools. This connects to the MCP
|
|
7
|
+
* server as the calling user (`x-auth-token` + base64 `x-context`), sanitizes
|
|
8
|
+
* each tool's schema into the Gemini-safe subset (kept for both backends), wires
|
|
9
|
+
* WRITE-labeled (destructive) tools through a HITL confirmation, and forwards
|
|
10
|
+
* tool start/result events to the browser bridge.
|
|
10
11
|
*
|
|
11
12
|
* `@livekit/agents` and `@modelcontextprotocol/sdk` are imported dynamically so
|
|
12
13
|
* the core package does not require them unless the realtime agent is used.
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP tools →
|
|
2
|
+
* MCP tools → realtime function tools bridge (realtime mode).
|
|
3
3
|
*
|
|
4
|
-
* In speech-to-speech mode
|
|
5
|
-
* server's tools are registered as
|
|
6
|
-
* NeuroLink tools. This connects to the MCP
|
|
7
|
-
* (`x-auth-token` + base64 `x-context`), sanitizes
|
|
8
|
-
* Gemini-safe subset
|
|
9
|
-
*
|
|
4
|
+
* In speech-to-speech mode the realtime model (Gemini Live, or xAI Grok Voice)
|
|
5
|
+
* owns the tool loop, so an external MCP server's tools are registered as
|
|
6
|
+
* LiveKit function tools rather than NeuroLink tools. This connects to the MCP
|
|
7
|
+
* server as the calling user (`x-auth-token` + base64 `x-context`), sanitizes
|
|
8
|
+
* each tool's schema into the Gemini-safe subset (kept for both backends), wires
|
|
9
|
+
* WRITE-labeled (destructive) tools through a HITL confirmation, and forwards
|
|
10
|
+
* tool start/result events to the browser bridge.
|
|
10
11
|
*
|
|
11
12
|
* `@livekit/agents` and `@modelcontextprotocol/sdk` are imported dynamically so
|
|
12
13
|
* the core package does not require them unless the realtime agent is used.
|
|
@@ -52,7 +53,7 @@ export function mcpResultToText(result) {
|
|
|
52
53
|
return text || "Tool returned no content.";
|
|
53
54
|
}
|
|
54
55
|
/**
|
|
55
|
-
* The function-result text returned to
|
|
56
|
+
* The function-result text returned to the model when the user rejects a HITL tool.
|
|
56
57
|
* Scope the rejection to THIS attempt — a "never retry" wording made the model
|
|
57
58
|
* refuse the tool permanently even when the user later asked for it again.
|
|
58
59
|
*/
|
|
@@ -163,7 +164,7 @@ export async function buildRealtimeMcpTools(params) {
|
|
|
163
164
|
try {
|
|
164
165
|
// Third argument is RequestOptions; the SDK aborts the in-flight
|
|
165
166
|
// request when `timeout` elapses. Without it a stalled server holds
|
|
166
|
-
// the turn open indefinitely —
|
|
167
|
+
// the turn open indefinitely — the model waits on the function
|
|
167
168
|
// result, so the user hears nothing at all rather than an error.
|
|
168
169
|
const result = await client.callTool({
|
|
169
170
|
name: mcpTool.name,
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
* See docs/features/livekit-voice-agent.md.
|
|
18
18
|
*/
|
|
19
19
|
import type { JobContext } from "@livekit/agents";
|
|
20
|
-
import type { RealtimeCallMemory, RealtimeVoiceMemoryConfig } from "../../types/index.js";
|
|
21
|
-
/**
|
|
22
|
-
|
|
20
|
+
import type { RealtimeCallMemory, RealtimeVoiceBackend, RealtimeVoiceMemoryConfig } from "../../types/index.js";
|
|
21
|
+
/**
|
|
22
|
+
* Verify the call's binding and prepare its recorder; `undefined` means "no
|
|
23
|
+
* memory for this call". `call` names the model and the realtime backend the
|
|
24
|
+
* call runs on, which the turn spans carry as `model` and `provider`; the
|
|
25
|
+
* backend defaults to `gemini`, the original and default one.
|
|
26
|
+
*/
|
|
27
|
+
export declare function setUpRealtimeCallMemory(ctx: Pick<JobContext, "room" | "addShutdownCallback">, config: RealtimeVoiceMemoryConfig | undefined, call?: {
|
|
28
|
+
model?: string;
|
|
29
|
+
backend?: RealtimeVoiceBackend;
|
|
30
|
+
}): Promise<RealtimeCallMemory | undefined>;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* See docs/features/livekit-voice-agent.md.
|
|
18
18
|
*/
|
|
19
19
|
import { context, trace } from "@opentelemetry/api";
|
|
20
|
+
import { AIProviderName } from "../../constants/enums.js";
|
|
20
21
|
import { setLangfuseContext } from "../../services/server/ai/observability/instrumentation.js";
|
|
21
22
|
import { observe, observeTrace } from "../../telemetry/observation.js";
|
|
22
23
|
import { tracers } from "../../telemetry/tracers.js";
|
|
@@ -24,8 +25,13 @@ import { logger } from "../../utils/logger.js";
|
|
|
24
25
|
import { VOICE_MEMORY_SECRET_ENV, verifyVoiceMemoryBinding, } from "./memoryBinding.js";
|
|
25
26
|
import { createVoiceTurnRecorder } from "./turnRecorder.js";
|
|
26
27
|
const DEFAULT_FLUSH_BUDGET_MS = 800;
|
|
28
|
+
/** The provider a backend's turn spans are attributed to: Gemini Live runs on Vertex AI, Grok Voice on xAI. */
|
|
29
|
+
const TRACE_PROVIDER = {
|
|
30
|
+
gemini: AIProviderName.VERTEX,
|
|
31
|
+
xai: AIProviderName.XAI,
|
|
32
|
+
};
|
|
27
33
|
function traceTurn(turn, call) {
|
|
28
|
-
const { sessionId, userId, roomName, model } = call;
|
|
34
|
+
const { sessionId, userId, roomName, model, provider } = call;
|
|
29
35
|
try {
|
|
30
36
|
setLangfuseContext({
|
|
31
37
|
sessionId,
|
|
@@ -55,7 +61,7 @@ function traceTurn(turn, call) {
|
|
|
55
61
|
input: [{ role: "user", content: turn.userMessage }],
|
|
56
62
|
output: [{ role: "assistant", content: turn.aiResponse }],
|
|
57
63
|
level,
|
|
58
|
-
metadata: { provider
|
|
64
|
+
metadata: { provider },
|
|
59
65
|
});
|
|
60
66
|
generation.end(endTime);
|
|
61
67
|
const events = turn.events ?? [];
|
|
@@ -80,8 +86,14 @@ function traceTurn(turn, call) {
|
|
|
80
86
|
});
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Verify the call's binding and prepare its recorder; `undefined` means "no
|
|
91
|
+
* memory for this call". `call` names the model and the realtime backend the
|
|
92
|
+
* call runs on, which the turn spans carry as `model` and `provider`; the
|
|
93
|
+
* backend defaults to `gemini`, the original and default one.
|
|
94
|
+
*/
|
|
95
|
+
export async function setUpRealtimeCallMemory(ctx, config, call = {}) {
|
|
96
|
+
const { model, backend = "gemini" } = call;
|
|
85
97
|
if (!config) {
|
|
86
98
|
return undefined;
|
|
87
99
|
}
|
|
@@ -116,7 +128,13 @@ export async function setUpRealtimeCallMemory(ctx, config, model) {
|
|
|
116
128
|
write: async (turn) => {
|
|
117
129
|
if (!tracedTurns.has(turn.turnId)) {
|
|
118
130
|
tracedTurns.add(turn.turnId);
|
|
119
|
-
traceTurn(turn, {
|
|
131
|
+
traceTurn(turn, {
|
|
132
|
+
sessionId,
|
|
133
|
+
userId,
|
|
134
|
+
roomName: ctx.room.name,
|
|
135
|
+
model,
|
|
136
|
+
provider: TRACE_PROVIDER[backend],
|
|
137
|
+
});
|
|
120
138
|
}
|
|
121
139
|
const result = await neurolink.recordConversationTurn({
|
|
122
140
|
sessionId,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The realtime model factory: builds the LiveKit speech-to-speech model for the
|
|
3
|
+
* configured backend.
|
|
4
|
+
*
|
|
5
|
+
* `gemini` is Gemini Live on Vertex through `@livekit/agents-plugin-google`;
|
|
6
|
+
* `xai` is Grok Voice through `@livekit/agents-plugin-xai`, which speaks the
|
|
7
|
+
* OpenAI realtime protocol to `wss://api.x.ai/v1`. Both plugins and
|
|
8
|
+
* `@google/genai` are imported dynamically so the core package does not require
|
|
9
|
+
* them unless the realtime agent is used.
|
|
10
|
+
*
|
|
11
|
+
* The xai backend has only been exercised against a stub WebSocket server. It
|
|
12
|
+
* has never talked to the real xAI service.
|
|
13
|
+
*
|
|
14
|
+
* See docs/features/livekit-voice-agent.md.
|
|
15
|
+
*/
|
|
16
|
+
import type { RealtimeLlm, RealtimeVoiceConfig } from "../../types/index.js";
|
|
17
|
+
/**
|
|
18
|
+
* Fail early, naming the variable, when `XAI_API_KEY` is blank or absent.
|
|
19
|
+
*
|
|
20
|
+
* The plugin reads the key itself and falls back to `OPENAI_API_KEY` when its
|
|
21
|
+
* own is missing, which would send an OpenAI key to the xAI host, so a missing
|
|
22
|
+
* key must stop here. The message never carries a value.
|
|
23
|
+
*/
|
|
24
|
+
export declare function requireXaiApiKey(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Build the realtime model for `cfg.backend`.
|
|
27
|
+
*
|
|
28
|
+
* The gemini branch builds exactly what the agent has always built: Vertex AI,
|
|
29
|
+
* the configured model, transcripts of both sides, and project, location,
|
|
30
|
+
* voice and modality when set. The xai branch needs `XAI_API_KEY` in the
|
|
31
|
+
* environment and throws before touching the plugin when it is missing.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createRealtimeLlm(cfg: RealtimeVoiceConfig): Promise<RealtimeLlm>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The realtime model factory: builds the LiveKit speech-to-speech model for the
|
|
3
|
+
* configured backend.
|
|
4
|
+
*
|
|
5
|
+
* `gemini` is Gemini Live on Vertex through `@livekit/agents-plugin-google`;
|
|
6
|
+
* `xai` is Grok Voice through `@livekit/agents-plugin-xai`, which speaks the
|
|
7
|
+
* OpenAI realtime protocol to `wss://api.x.ai/v1`. Both plugins and
|
|
8
|
+
* `@google/genai` are imported dynamically so the core package does not require
|
|
9
|
+
* them unless the realtime agent is used.
|
|
10
|
+
*
|
|
11
|
+
* The xai backend has only been exercised against a stub WebSocket server. It
|
|
12
|
+
* has never talked to the real xAI service.
|
|
13
|
+
*
|
|
14
|
+
* See docs/features/livekit-voice-agent.md.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Fail early, naming the variable, when `XAI_API_KEY` is blank or absent.
|
|
18
|
+
*
|
|
19
|
+
* The plugin reads the key itself and falls back to `OPENAI_API_KEY` when its
|
|
20
|
+
* own is missing, which would send an OpenAI key to the xAI host, so a missing
|
|
21
|
+
* key must stop here. The message never carries a value.
|
|
22
|
+
*/
|
|
23
|
+
export function requireXaiApiKey() {
|
|
24
|
+
const key = process.env.XAI_API_KEY;
|
|
25
|
+
if (typeof key !== "string" || key.trim().length === 0) {
|
|
26
|
+
throw new Error("XAI_API_KEY is not set; the xai realtime backend reads its key from that environment variable");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function createGeminiRealtimeLlm(cfg) {
|
|
30
|
+
const google = await import("@livekit/agents-plugin-google");
|
|
31
|
+
const { Modality } = await import("@google/genai");
|
|
32
|
+
const modality = Object.values(Modality).find((value) => value === cfg.responseModality);
|
|
33
|
+
const modelOptions = {
|
|
34
|
+
vertexai: true,
|
|
35
|
+
model: cfg.model,
|
|
36
|
+
// Emit text transcripts of BOTH sides; LiveKit forwards them to the room.
|
|
37
|
+
inputAudioTranscription: {},
|
|
38
|
+
outputAudioTranscription: {},
|
|
39
|
+
};
|
|
40
|
+
if (modality !== undefined) {
|
|
41
|
+
modelOptions.modalities = [modality];
|
|
42
|
+
}
|
|
43
|
+
if (cfg.project) {
|
|
44
|
+
modelOptions.project = cfg.project;
|
|
45
|
+
}
|
|
46
|
+
if (cfg.location) {
|
|
47
|
+
modelOptions.location = cfg.location;
|
|
48
|
+
}
|
|
49
|
+
if (cfg.voice) {
|
|
50
|
+
modelOptions.voice = cfg.voice;
|
|
51
|
+
}
|
|
52
|
+
return new google.realtime.RealtimeModel(modelOptions);
|
|
53
|
+
}
|
|
54
|
+
async function createXaiRealtimeLlm(cfg) {
|
|
55
|
+
requireXaiApiKey();
|
|
56
|
+
const xai = await import("@livekit/agents-plugin-xai");
|
|
57
|
+
// Keys are added only when set. The plugin spreads these options over its
|
|
58
|
+
// own defaults, so a key holding `undefined` would erase the default: a
|
|
59
|
+
// missing `baseURL` would send the xAI key to OPENAI_BASE_URL or
|
|
60
|
+
// api.openai.com, and no `apiKey` option is passed at all because the plugin
|
|
61
|
+
// reads XAI_API_KEY itself. The plugin forwards `baseURL` although its
|
|
62
|
+
// declared option type leaves it out.
|
|
63
|
+
const modelOptions = { model: cfg.model };
|
|
64
|
+
if (cfg.voice) {
|
|
65
|
+
modelOptions.voice = cfg.voice;
|
|
66
|
+
}
|
|
67
|
+
if (cfg.xaiBaseURL) {
|
|
68
|
+
modelOptions.baseURL = cfg.xaiBaseURL;
|
|
69
|
+
}
|
|
70
|
+
return new xai.realtime.RealtimeModel(modelOptions);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the realtime model for `cfg.backend`.
|
|
74
|
+
*
|
|
75
|
+
* The gemini branch builds exactly what the agent has always built: Vertex AI,
|
|
76
|
+
* the configured model, transcripts of both sides, and project, location,
|
|
77
|
+
* voice and modality when set. The xai branch needs `XAI_API_KEY` in the
|
|
78
|
+
* environment and throws before touching the plugin when it is missing.
|
|
79
|
+
*/
|
|
80
|
+
export async function createRealtimeLlm(cfg) {
|
|
81
|
+
switch (cfg.backend) {
|
|
82
|
+
case "gemini":
|
|
83
|
+
return createGeminiRealtimeLlm(cfg);
|
|
84
|
+
case "xai":
|
|
85
|
+
return createXaiRealtimeLlm(cfg);
|
|
86
|
+
default:
|
|
87
|
+
throw new Error('The realtime backend must be "gemini" or "xai"');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LiveKit Agents agent definition — realtime (
|
|
2
|
+
* LiveKit Agents agent definition — realtime (speech-to-speech).
|
|
3
3
|
*
|
|
4
4
|
* `defineRealtimeVoiceAgent` returns the agent object placed as the default
|
|
5
5
|
* export of a worker entry file. Unlike the cascaded `defineVoiceAgent`
|
|
6
6
|
* (Silero VAD → STT → NeuroLink → TTS), here a single realtime model (Gemini
|
|
7
|
-
* Live on Vertex) does STT + reasoning + TTS + turn
|
|
7
|
+
* Live on Vertex, or xAI Grok Voice) does STT + reasoning + TTS + turn
|
|
8
|
+
* detection over one connection. The backend is chosen by `VOICE_REALTIME_BACKEND`
|
|
9
|
+
* or the `backend` option; Gemini Live is the default.
|
|
8
10
|
*
|
|
9
|
-
* `@livekit/agents`,
|
|
10
|
-
* `@google/genai` are imported dynamically so the core
|
|
11
|
-
* them unless the realtime agent is used. Type-only
|
|
11
|
+
* `@livekit/agents`, the backend's LiveKit plugin (see `realtimeModel.ts`),
|
|
12
|
+
* `@livekit/rtc-node`, and `@google/genai` are imported dynamically so the core
|
|
13
|
+
* package does not require them unless the realtime agent is used. Type-only
|
|
14
|
+
* imports are erased at build.
|
|
12
15
|
*
|
|
13
16
|
* See docs/features/livekit-voice-agent.md.
|
|
14
17
|
*/
|
|
15
18
|
import type { JobContext } from "@livekit/agents";
|
|
16
19
|
import type { RealtimeVoiceAgentConfig } from "../../types/index.js";
|
|
17
20
|
/**
|
|
18
|
-
* Define a realtime (
|
|
21
|
+
* Define a realtime (speech-to-speech) LiveKit voice agent: Gemini Live by
|
|
22
|
+
* default, xAI Grok Voice with `VOICE_REALTIME_BACKEND=xai` or `backend: "xai"`.
|
|
19
23
|
*
|
|
20
24
|
* Place the result as the default export of the worker entry file and launch it
|
|
21
25
|
* with `startRealtimeVoiceAgentWorker`. With no `config` everything is resolved
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LiveKit Agents agent definition — realtime (
|
|
2
|
+
* LiveKit Agents agent definition — realtime (speech-to-speech).
|
|
3
3
|
*
|
|
4
4
|
* `defineRealtimeVoiceAgent` returns the agent object placed as the default
|
|
5
5
|
* export of a worker entry file. Unlike the cascaded `defineVoiceAgent`
|
|
6
6
|
* (Silero VAD → STT → NeuroLink → TTS), here a single realtime model (Gemini
|
|
7
|
-
* Live on Vertex) does STT + reasoning + TTS + turn
|
|
7
|
+
* Live on Vertex, or xAI Grok Voice) does STT + reasoning + TTS + turn
|
|
8
|
+
* detection over one connection. The backend is chosen by `VOICE_REALTIME_BACKEND`
|
|
9
|
+
* or the `backend` option; Gemini Live is the default.
|
|
8
10
|
*
|
|
9
|
-
* `@livekit/agents`,
|
|
10
|
-
* `@google/genai` are imported dynamically so the core
|
|
11
|
-
* them unless the realtime agent is used. Type-only
|
|
11
|
+
* `@livekit/agents`, the backend's LiveKit plugin (see `realtimeModel.ts`),
|
|
12
|
+
* `@livekit/rtc-node`, and `@google/genai` are imported dynamically so the core
|
|
13
|
+
* package does not require them unless the realtime agent is used. Type-only
|
|
14
|
+
* imports are erased at build.
|
|
12
15
|
*
|
|
13
16
|
* See docs/features/livekit-voice-agent.md.
|
|
14
17
|
*/
|
|
@@ -16,6 +19,7 @@ import { z } from "zod";
|
|
|
16
19
|
import { logger } from "../../utils/logger.js";
|
|
17
20
|
import { resolveRealtimeVoiceConfig } from "./config.js";
|
|
18
21
|
import { ensureVertexAdc, clearGeminiApiKeyEnv } from "./vertexAuth.js";
|
|
22
|
+
import { createRealtimeLlm, requireXaiApiKey } from "./realtimeModel.js";
|
|
19
23
|
import { readCallContextFromRoom } from "./roomContext.js";
|
|
20
24
|
import { attachRealtimeEventBridge } from "./realtimeEventBridge.js";
|
|
21
25
|
import { buildRealtimeMcpTools } from "./realtimeMcpTools.js";
|
|
@@ -119,7 +123,10 @@ async function installRealtimeJobLifecycle(ctx, cfg) {
|
|
|
119
123
|
}
|
|
120
124
|
/** Merge caller overrides over the env-resolved realtime config. */
|
|
121
125
|
function resolveConfig(overrides) {
|
|
122
|
-
const base = resolveRealtimeVoiceConfig(
|
|
126
|
+
const base = resolveRealtimeVoiceConfig({
|
|
127
|
+
backend: overrides.backend,
|
|
128
|
+
xaiBaseURL: overrides.xaiBaseURL,
|
|
129
|
+
});
|
|
123
130
|
return {
|
|
124
131
|
...base,
|
|
125
132
|
project: overrides.project ?? base.project,
|
|
@@ -134,7 +141,8 @@ function resolveConfig(overrides) {
|
|
|
134
141
|
};
|
|
135
142
|
}
|
|
136
143
|
/**
|
|
137
|
-
* Define a realtime (
|
|
144
|
+
* Define a realtime (speech-to-speech) LiveKit voice agent: Gemini Live by
|
|
145
|
+
* default, xAI Grok Voice with `VOICE_REALTIME_BACKEND=xai` or `backend: "xai"`.
|
|
138
146
|
*
|
|
139
147
|
* Place the result as the default export of the worker entry file and launch it
|
|
140
148
|
* with `startRealtimeVoiceAgentWorker`. With no `config` everything is resolved
|
|
@@ -147,16 +155,26 @@ export function defineRealtimeVoiceAgent(config = {}) {
|
|
|
147
155
|
const onLog = config.onLog;
|
|
148
156
|
const memoryConfig = config.memory;
|
|
149
157
|
async function entry(ctx) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
158
|
+
const isGemini = cfg.backend === "gemini";
|
|
159
|
+
if (isGemini) {
|
|
160
|
+
// The Gemini Live WS authenticates to Vertex via ADC; materialise it and
|
|
161
|
+
// force ADC (not an API key) auth before any realtime connection.
|
|
162
|
+
ensureVertexAdc();
|
|
163
|
+
clearGeminiApiKeyEnv();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
requireXaiApiKey();
|
|
167
|
+
}
|
|
154
168
|
logger.info("realtime.config", {
|
|
169
|
+
backend: cfg.backend,
|
|
155
170
|
model: cfg.model,
|
|
156
|
-
|
|
157
|
-
|
|
171
|
+
...(isGemini
|
|
172
|
+
? { location: cfg.location, project: cfg.project ?? null }
|
|
173
|
+
: {}),
|
|
158
174
|
voice: cfg.voice ?? null,
|
|
159
|
-
|
|
175
|
+
...(isGemini
|
|
176
|
+
? { modality: cfg.responseModality }
|
|
177
|
+
: { customBaseURL: cfg.xaiBaseURL !== undefined }),
|
|
160
178
|
toolsEnabled: cfg.toolsEnabled,
|
|
161
179
|
mcpUrl: cfg.mcpUrl,
|
|
162
180
|
});
|
|
@@ -190,17 +208,20 @@ export function defineRealtimeVoiceAgent(config = {}) {
|
|
|
190
208
|
}
|
|
191
209
|
logger.info("realtime.room.joined", {
|
|
192
210
|
room: ctx.room.name ?? "unknown",
|
|
211
|
+
backend: cfg.backend,
|
|
193
212
|
model: cfg.model,
|
|
194
|
-
location: cfg.location,
|
|
213
|
+
...(isGemini ? { location: cfg.location } : {}),
|
|
195
214
|
ms: sinceConnect(),
|
|
196
215
|
});
|
|
197
216
|
// Decide whether this call may be remembered BEFORE the lifecycle installs
|
|
198
217
|
// its process reaper, so the shutdown flush is registered first.
|
|
199
|
-
const memory = await setUpRealtimeCallMemory(ctx, memoryConfig,
|
|
218
|
+
const memory = await setUpRealtimeCallMemory(ctx, memoryConfig, {
|
|
219
|
+
model: cfg.model,
|
|
220
|
+
backend: cfg.backend,
|
|
221
|
+
});
|
|
200
222
|
await installRealtimeJobLifecycle(ctx, cfg);
|
|
201
223
|
const { voice } = await import("@livekit/agents");
|
|
202
|
-
const
|
|
203
|
-
const { Modality } = await import("@google/genai");
|
|
224
|
+
const realtimeLlm = await createRealtimeLlm(cfg);
|
|
204
225
|
const bridge = await attachRealtimeEventBridge({
|
|
205
226
|
room: ctx.room,
|
|
206
227
|
hitlTimeoutMs: cfg.hitlTimeoutMs,
|
|
@@ -254,28 +275,10 @@ export function defineRealtimeVoiceAgent(config = {}) {
|
|
|
254
275
|
}
|
|
255
276
|
}
|
|
256
277
|
}
|
|
257
|
-
const modality = Object.values(Modality).find((value) => value === cfg.responseModality);
|
|
258
|
-
const modelOptions = {
|
|
259
|
-
vertexai: true,
|
|
260
|
-
model: cfg.model,
|
|
261
|
-
// Emit text transcripts of BOTH sides; LiveKit forwards them to the room.
|
|
262
|
-
inputAudioTranscription: {},
|
|
263
|
-
outputAudioTranscription: {},
|
|
264
|
-
};
|
|
265
|
-
if (modality !== undefined) {
|
|
266
|
-
modelOptions.modalities = [modality];
|
|
267
|
-
}
|
|
268
|
-
if (cfg.project) {
|
|
269
|
-
modelOptions.project = cfg.project;
|
|
270
|
-
}
|
|
271
|
-
if (cfg.location) {
|
|
272
|
-
modelOptions.location = cfg.location;
|
|
273
|
-
}
|
|
274
|
-
if (cfg.voice) {
|
|
275
|
-
modelOptions.voice = cfg.voice;
|
|
276
|
-
}
|
|
277
278
|
const session = new voice.AgentSession({
|
|
278
|
-
|
|
279
|
+
// `RealtimeLlm` is declared structurally so the published declarations
|
|
280
|
+
// name no optional plugin; both backends return a `RealtimeModel`.
|
|
281
|
+
llm: realtimeLlm,
|
|
279
282
|
});
|
|
280
283
|
// The conversation so far, so a call that appends to an open conversation
|
|
281
284
|
// starts knowing it. Empty (and omitted) when there is nothing to seed.
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Pure, dependency-free, and assertion-free — values arrive as `unknown` and are
|
|
11
11
|
* narrowed with the `isRecord` guard. Safe to reuse for any Gemini tool path.
|
|
12
|
+
* The xai realtime backend uses it too: the output is plain JSON Schema, and
|
|
13
|
+
* keeping it avoids sending xAI `$ref` or union shapes it has not been tried with.
|
|
12
14
|
*/
|
|
13
15
|
/**
|
|
14
16
|
* Rebuild a JSON Schema node into the Gemini-safe subset. Returns a fresh object
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Pure, dependency-free, and assertion-free — values arrive as `unknown` and are
|
|
11
11
|
* narrowed with the `isRecord` guard. Safe to reuse for any Gemini tool path.
|
|
12
|
+
* The xai realtime backend uses it too: the output is plain JSON Schema, and
|
|
13
|
+
* keeping it avoids sending xAI `$ref` or union shapes it has not been tried with.
|
|
12
14
|
*/
|
|
13
15
|
const GEMINI_TYPES = new Set([
|
|
14
16
|
"string",
|
|
@@ -3635,9 +3635,9 @@
|
|
|
3635
3635
|
{"objectID":"ca876274dff5d751bc09c60e4de804f44917d9e0599697c2cd17362e3e6148bd","title":"Advanced Integrations","url":"/docs/features#advanced-integrations","content":"| Integration | Description |\n| -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |\n| LiteLLM Integration | Access 100+ models across a broad range of AI providers via LiteLLM routing with unified interface. |\n| SageMaker Integration | Deploy and call custom endpoints directly from NeuroLink CLI/SDK with full control. |\n| Memory | Per-user condensed memory with S3/Redis/SQLite storage and LLM-powered condensation. |\n| Enterprise Proxy | Configure outbound policies and compliance posture for corporate environments. |\n| Configuration Management | Manage environments, regions, and credentials safely across deployments. |","hierarchy":{"lvl0":"Features","lvl1":"Feature Guides","lvl2":"Advanced Integrations","lvl3":""}},
|
|
3636
3636
|
{"objectID":"0d74f47963b8a59cf58901d344f8d1e987d3bb286cb012ed22278f415c051d3f","title":"Advanced Features","url":"/docs/features#advanced-features","content":"| Feature | Description |\n| ----------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |\n| 🏭 Factory Pattern Architecture | Unified provider interface with automatic fallbacks and type-safe implementations. |\n| 🗄️ Conversation Memory | Deep dive into memory management and Redis integration. |\n| 🔧 Custom Middleware | Build request/response hooks for logging, filtering, and custom processing. |\n| ⚡ Performance Optimization | Caching, connection pooling, and latency optimization strategies. |\n| 📊 Telemetry & Observability | OpenTelemetry integration for distributed tracing and monitoring. |\n| 🧪 Testing Guide | Provider-agnostic testing, mocking, and quality assurance strategies. |\n| 📊 Analytics & Evaluation | Usage tracking, cost monitoring, and quality scoring for AI responses. |\n| ⚡ Streaming | Real-time token streaming with provider-specific optimizations. |\n| Thinking Configuration | Configure extended thinking levels for supported models (Anthropic, Gemini 2.5+). |\n| Structured Output | JSON schema-based structured output with provider-specific formatting. |\n| Text-to-Speech (TTS) | Basic TTS support via Google Cloud TTS (Neural2, Wavenet, Standard voices). |","hierarchy":{"lvl0":"Features","lvl1":"Feature Guides","lvl2":"Advanced Features","lvl3":""}},
|
|
3637
3637
|
{"objectID":"9c1dd453df8293552c84f52e9dd08ca4cd003afe799a2561dc4101aa9c2a304a","title":"See Also","url":"/docs/features#see-also","content":"Getting Started - Quick start and installation\nCLI Reference - Command-line interface documentation\nSDK Reference - TypeScript API documentation\nEnterprise Guides - Production deployment patterns\nTutorials - Step-by-step implementation guides\nExamples - Real-world code samples","hierarchy":{"lvl0":"Features","lvl1":"Feature Guides","lvl2":"See Also","lvl3":""}},
|
|
3638
|
-
{"objectID":"83f5b0de036a8a17898acf6e57b876b24b8eab4db20af614b766816e4da47913","title":"LiveKit Voice Agent — Real-Time Voice over WebRTC","url":"/docs/features/livekit-voice-agent","content":"LiveKit Voice Agent — Real-Time Voice over WebRTC\n\nA WebRTC voice agent that uses LiveKit for the real-time media loop and NeuroLink as the brain (LLM, tools, memory).\n\nTable of Contents\nProblem Statement & Solution\nArchitecture Overview\nDeployment Topologies (Cloud & Self-Hosted)\nCore Components\nHow NeuroLink Owns the Brain\nRuntime Flow\nUsage Example\nSource Layout\nConfiguration\nTuning the Voice Loop (VAD, Turn Detection, Interruption, Language)\nConversation Memory\nImplementation Plan\nOperational Behavior\nError Handling & Troubleshooting\nExtensibility Roadmap\n\nProblem Statement & Solution\n\nThe Challenge\n\nThe original NeuroLink voice agent (see voice-agent.md) runs a browser-to-server loop over a WebSocket. That design works, but a WebSocket transport carries structural limits for real-time audio:\nTCP head-of-line blocking and no jitter buffer cause choppy audio on lossy networks\nno built-in acoustic echo cancellation — the assistant can be transcribed by its own mic input\nraw PCM is ~8–10× the bandwidth of a compressed codec, and all of it flows through the application server\nvoice-activity detection runs on the application server's event loop, capping per-process concurrency\nSvelteKit and similar frameworks cannot accept the WebSocket upgrade without a custom server entry\n\nThe Solution\n\nThe LiveKit voice agent moves the transport to WebRTC via LiveKit, while keeping NeuroLink as the brain. LiveKit (an open-source WebRTC platform with a managed cloud and a self-hostable server) provides the parts that are hard to build correctly:\nWebRTC transport with echo cancellation, jitter buffering, packet-loss concealment, and Opus compression\nvoice-activity detection, turn detection, and interruption handling\na worker/job model that runs each call in its own process for isolation and horizontal scaling\n\nNeuroLink remains responsible for the conversation itself:\nthe LLM (any NeuroLink provider — Bedrock/Claude, OpenAI, Gemini, etc.)\ntool calling (MCP and registered tools), decided and executed inside neurolink.stream()\nconversation memory, keyed by a stable conversationId\n\nKey Benefits\nProduction-grade real-time audio without building media plumbing\nNeuroLink stays the brain — generate()/stream(), tools, and memory are unchanged\nWorker-per-call scaling provided by the LiveKit Agents runtime\nCloud or self-hosted with identical application code\nProvider-agnostic brain layer that can later back other transports\n\nArchitecture Overview\n\nSystem Flow Diagram\n\nDivision of Responsibility\n\n| Concern | Owner |\n| ------------------------------------------- | ----------------------------------- |\n| WebRTC transport, AEC, jitter, Opus | LiveKit |\n| VAD, turn detection, interruption | LiveKit Agents |\n| Worker-per-call process isolation & scaling | LiveKit Agents |\n| STT / TTS | LiveKit plugins (configurable) |\n| LLM, tool-calling, memory | NeuroLink |\n| Conversation history source of truth | NeuroLink memory (conversationId) |\n\nDeployment Topologies (Cloud & Self-Hosted)\n\nThe application code is identical across topologies; only LIVEKIT_URL and credentials change.\n\nTopology A — LiveKit Cloud (managed)\nRooms are created automatically on LiveKit's servers on first join.\nThe worker connects outbound to Cloud and receives dispatched Jobs over that connection — no inbound exposure or tunneling is required, even in local development.\nBilling is per participant-minute (a free Build tier is suitable for development).\nUse when: fastest setup, minimal media ops, dev/staging, or production without running media infrastructure.\n\nTopology B — Self-Hosted LiveKit (in-house)\nThe livekit-server (open source) runs on your own infrastructure (for example, Kubernetes behind your ingress/service mesh).\nMedia stays inside your network; there is no per-minute media fee — you pay only for compute and bandwidth.\nUse when: cost control at scale, data-residency/compliance requirements, or full control over the media path.\n\nLocal Development\nConsole mode: the worker runs standalone using the host machine's microphone and speakers — no LiveKit server and no browser required. Best for iterating on the brain loop.\nLocal server: livekit-server --dev (placeholder credentials, no external dependencies) with the browser and worker on localhost.\nCloud from local: point local LIVEKIT_URL at a Cloud project. Because the worker connects outbound, Cloud can dispatch Jobs to a locally-running worker without tunneling.\n\nCore Components\nLiveKit Agents Worker\n\nA long-lived Node process built on @livekit/agents. It registers with the LiveKit server under an agentName (for example, neurolink-voice). For each room, LiveKit dispatches a Job, which the runtime runs in its
|
|
3638
|
+
{"objectID":"83f5b0de036a8a17898acf6e57b876b24b8eab4db20af614b766816e4da47913","title":"LiveKit Voice Agent — Real-Time Voice over WebRTC","url":"/docs/features/livekit-voice-agent","content":"LiveKit Voice Agent — Real-Time Voice over WebRTC\n\nA WebRTC voice agent that uses LiveKit for the real-time media loop and NeuroLink as the brain (LLM, tools, memory).\n\nTable of Contents\nProblem Statement & Solution\nArchitecture Overview\nDeployment Topologies (Cloud & Self-Hosted)\nCore Components\nHow NeuroLink Owns the Brain\nRuntime Flow\nUsage Example\nSource Layout\nConfiguration\nTuning the Voice Loop (VAD, Turn Detection, Interruption, Language)\nConversation Memory\nRealtime Speech-to-Speech Backends (Gemini Live and xAI Grok Voice)\nImplementation Plan\nOperational Behavior\nError Handling & Troubleshooting\nExtensibility Roadmap\n\nProblem Statement & Solution\n\nThe Challenge\n\nThe original NeuroLink voice agent (see voice-agent.md) runs a browser-to-server loop over a WebSocket. That design works, but a WebSocket transport carries structural limits for real-time audio:\nTCP head-of-line blocking and no jitter buffer cause choppy audio on lossy networks\nno built-in acoustic echo cancellation — the assistant can be transcribed by its own mic input\nraw PCM is ~8–10× the bandwidth of a compressed codec, and all of it flows through the application server\nvoice-activity detection runs on the application server's event loop, capping per-process concurrency\nSvelteKit and similar frameworks cannot accept the WebSocket upgrade without a custom server entry\n\nThe Solution\n\nThe LiveKit voice agent moves the transport to WebRTC via LiveKit, while keeping NeuroLink as the brain. LiveKit (an open-source WebRTC platform with a managed cloud and a self-hostable server) provides the parts that are hard to build correctly:\nWebRTC transport with echo cancellation, jitter buffering, packet-loss concealment, and Opus compression\nvoice-activity detection, turn detection, and interruption handling\na worker/job model that runs each call in its own process for isolation and horizontal scaling\n\nNeuroLink remains responsible for the conversation itself:\nthe LLM (any NeuroLink provider — Bedrock/Claude, OpenAI, Gemini, etc.)\ntool calling (MCP and registered tools), decided and executed inside neurolink.stream()\nconversation memory, keyed by a stable conversationId\n\nKey Benefits\nProduction-grade real-time audio without building media plumbing\nNeuroLink stays the brain — generate()/stream(), tools, and memory are unchanged\nWorker-per-call scaling provided by the LiveKit Agents runtime\nCloud or self-hosted with identical application code\nProvider-agnostic brain layer that can later back other transports\n\nArchitecture Overview\n\nSystem Flow Diagram\n\nDivision of Responsibility\n\n| Concern | Owner |\n| ------------------------------------------- | ----------------------------------- |\n| WebRTC transport, AEC, jitter, Opus | LiveKit |\n| VAD, turn detection, interruption | LiveKit Agents |\n| Worker-per-call process isolation & scaling | LiveKit Agents |\n| STT / TTS | LiveKit plugins (configurable) |\n| LLM, tool-calling, memory | NeuroLink |\n| Conversation history source of truth | NeuroLink memory (conversationId) |\n\nDeployment Topologies (Cloud & Self-Hosted)\n\nThe application code is identical across topologies; only LIVEKIT_URL and credentials change.\n\nTopology A — LiveKit Cloud (managed)\nRooms are created automatically on LiveKit's servers on first join.\nThe worker connects outbound to Cloud and receives dispatched Jobs over that connection — no inbound exposure or tunneling is required, even in local development.\nBilling is per participant-minute (a free Build tier is suitable for development).\nUse when: fastest setup, minimal media ops, dev/staging, or production without running media infrastructure.\n\nTopology B — Self-Hosted LiveKit (in-house)\nThe livekit-server (open source) runs on your own infrastructure (for example, Kubernetes behind your ingress/service mesh).\nMedia stays inside your network; there is no per-minute media fee — you pay only for compute and bandwidth.\nUse when: cost control at scale, data-residency/compliance requirements, or full control over the media path.\n\nLocal Development\nConsole mode: the worker runs standalone using the host machine's microphone and speakers — no LiveKit server and no browser required. Best for iterating on the brain loop.\nLocal server: livekit-server --dev (placeholder credentials, no external dependencies) with the browser and worker on localhost.\nCloud from local: point local LIVEKIT_URL at a Cloud project. Because the worker connects outbound, Cloud can dispatch Jobs to a locally-running worker without tunneling.\n\nCore Components\nLiveKit Agents Worker\n\nA long-lived Node process built on @livekit/agents. It registers with the LiveKit server under an agentName (for example, neurolink-voice). For each room, LiveKit dispatches a Job, which the runtime runs in its ","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"","lvl3":""}},
|
|
3639
3639
|
{"objectID":"18162c78ef8435753a2a960c9aaacf301d553ede68ef97184f924649726c9b59","title":"LiveKit Voice Agent — Real-Time Voice over WebRTC","url":"/docs/features/livekit-voice-agent#livekit-voice-agent-real-time-voice-over-webrtc","content":"A WebRTC voice agent that uses LiveKit for the real-time media loop and NeuroLink as the brain (LLM, tools, memory).","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl3":""}},
|
|
3640
|
-
{"objectID":"1f733cc14879f19626ff1c687558c535be79cba162237c90533f45426f7d98cc","title":"Table of Contents","url":"/docs/features/livekit-voice-agent#table-of-contents","content":"Problem Statement & Solution\nArchitecture Overview\nDeployment Topologies (Cloud & Self-Hosted)\nCore Components\nHow NeuroLink Owns the Brain\nRuntime Flow\nUsage Example\nSource Layout\nConfiguration\nTuning the Voice Loop (VAD, Turn Detection, Interruption, Language)\nConversation Memory\nImplementation Plan\nOperational Behavior\nError Handling & Troubleshooting\nExtensibility Roadmap","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Table of Contents","lvl3":""}},
|
|
3640
|
+
{"objectID":"1f733cc14879f19626ff1c687558c535be79cba162237c90533f45426f7d98cc","title":"Table of Contents","url":"/docs/features/livekit-voice-agent#table-of-contents","content":"Problem Statement & Solution\nArchitecture Overview\nDeployment Topologies (Cloud & Self-Hosted)\nCore Components\nHow NeuroLink Owns the Brain\nRuntime Flow\nUsage Example\nSource Layout\nConfiguration\nTuning the Voice Loop (VAD, Turn Detection, Interruption, Language)\nConversation Memory\nRealtime Speech-to-Speech Backends (Gemini Live and xAI Grok Voice)\nImplementation Plan\nOperational Behavior\nError Handling & Troubleshooting\nExtensibility Roadmap","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Table of Contents","lvl3":""}},
|
|
3641
3641
|
{"objectID":"91eb3bc5f2e53ccf0662d056db82c3261cd9995803814a4bd489a21b509d5ac8","title":"The Challenge","url":"/docs/features/livekit-voice-agent#the-challenge","content":"The original NeuroLink voice agent (see voice-agent.md) runs a browser-to-server loop over a WebSocket. That design works, but a WebSocket transport carries structural limits for real-time audio:\nTCP head-of-line blocking and no jitter buffer cause choppy audio on lossy networks\nno built-in acoustic echo cancellation — the assistant can be transcribed by its own mic input\nraw PCM is ~8–10× the bandwidth of a compressed codec, and all of it flows through the application server\nvoice-activity detection runs on the application server's event loop, capping per-process concurrency\nSvelteKit and similar frameworks cannot accept the WebSocket upgrade without a custom server entry","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"The Challenge","lvl3":""}},
|
|
3642
3642
|
{"objectID":"1cb9d49f576a65ba650f409acd589e1409d749a9e793c8007fcdd653049227cd","title":"The Solution","url":"/docs/features/livekit-voice-agent#the-solution","content":"The LiveKit voice agent moves the transport to WebRTC via LiveKit, while keeping NeuroLink as the brain. LiveKit (an open-source WebRTC platform with a managed cloud and a self-hostable server) provides the parts that are hard to build correctly:\nWebRTC transport with echo cancellation, jitter buffering, packet-loss concealment, and Opus compression\nvoice-activity detection, turn detection, and interruption handling\na worker/job model that runs each call in its own process for isolation and horizontal scaling\n\nNeuroLink remains responsible for the conversation itself:\nthe LLM (any NeuroLink provider — Bedrock/Claude, OpenAI, Gemini, etc.)\ntool calling (MCP and registered tools), decided and executed inside neurolink.stream()\nconversation memory, keyed by a stable conversationId","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"The Solution","lvl3":""}},
|
|
3643
3643
|
{"objectID":"8c89b1ddf6fdca318ba346bdddbd0dc7a7d0e750cf4eeffdde705f11e43e7dc7","title":"Key Benefits","url":"/docs/features/livekit-voice-agent#key-benefits","content":"Production-grade real-time audio without building media plumbing\nNeuroLink stays the brain — generate()/stream(), tools, and memory are unchanged\nWorker-per-call scaling provided by the LiveKit Agents runtime\nCloud or self-hosted with identical application code\nProvider-agnostic brain layer that can later back other transports","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Key Benefits","lvl3":""}},
|
|
@@ -3676,6 +3676,14 @@
|
|
|
3676
3676
|
{"objectID":"804a64dd17c69cb8d4af469682d01d12f00bec2d6869a5faef30865da99f176d","title":"Speech provider selection","url":"/docs/features/livekit-voice-agent#speech-provider-selection","content":"STT and TTS plugins are chosen per agent and configured by environment credentials.\nSTT: soniox, deepgram. TTS: cartesia, elevenlabs.\nOnly set voice / model if your account supports them; otherwise omit those\n fields to use the plugin's own defaults.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Speech provider selection","lvl3":""}},
|
|
3677
3677
|
{"objectID":"5589375c3b5b8ee71271f32386ef6b0f878bc88f7097a13ee7d8ef03b694655e","title":"Conversation Memory","url":"/docs/features/livekit-voice-agent#conversation-memory","content":"The agent remembers earlier turns automatically when the NeuroLink instance you\nbuild inside createNeuroLink has conversation memory enabled. History is the\nagent's source of truth — LiveKit's own transcript context is not used for\ngeneration.\n\nHow it behaves:\nKeyed per call. Each room/call is an isolated conversation; the id is\n derived from the room name. Override the prefix with conversationIdPrefix\n (default \"voice\").\nIn-memory by default; Redis for persistence. Set REDIS_URL to use a\n shared store that survives worker restarts and is shared across worker\n replicas — important because each call runs in its own job process.\nWorks across turns within the session. The user can say \"my name is Alex\"\n and later ask \"what's my name?\" and the agent recalls it.\n\nMemory persists only when the instance is configured with\nconversationMemory.enabled. Without it, each turn is independent.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Conversation Memory","lvl3":""}},
|
|
3678
3678
|
{"objectID":"e6ef692117fe2ad968f2bb5bd1692e50783a069a189d3dbfd6ac49f71ef84c73","title":"Remembering a realtime call in the user's own conversation","url":"/docs/features/livekit-voice-agent#remembering-a-realtime-call-in-the-users-own-conversation","content":"A speech-to-speech call (defineRealtimeVoiceAgent) never goes through\nstream() or generate(), so by default nothing it says is stored. Opt in with\nmemory and the call's turns are written, through\nNeuroLink.recordConversationTurn, to the same conversation memory typed chat\nuses, appended to the conversation the person already has open. The model is\nalso seeded with that conversation's recent history when the call starts, so it\nknows what was said before.\n\nWhose conversation is never taken from the client. The host that creates\nthe room signs a binding and puts it in the room metadata next to authToken\nand mcpContext. The worker stores nothing unless it verifies.\nThe signature is HMAC-SHA256 over [\"neurolink.voice.memory.v1\", room,\nuserId, sessionId, exp] (a JSON array, so no choice of values can make two\n bindings collide), base64url-encoded. It covers the room name, so a binding\n copied into another room does not verify. exp is Unix seconds; the default\n lifetime is two hours, the cap 24 hours.\nThe shared secret is at least 16 characters, read from\n NEUROLINK_VOICE_MEMORY_SECRET unless memory.secret is set on the worker.\nsessionId matches ^[A-Za-z0-9_-]{1,128}$ (no :, which is part of the\n stored key). userId is 1 to 256 characters with no whitespace or control\n characters; : is allowed because chat user ids contain it.\nFail closed. No secret, no binding, a malformed or expired binding, or a\n bad signature: the call runs normally, nothing is stored, and a warning\n names the reason (never the ids).\nA client may name the conversation to append to. The host accepts the name\n only when neurolink.conversationExists({ sessionId, userId }) is true for\n the verified user. Otherwise it mints a new voice-<uuid>.\n\nWhat is stored. Each finished exchange becomes a user message and an\nassistant message with the ids ${turnId}:user and ${turnId}:assistant, where\nturnId is the realtime session's assistant message id, and\nmetadata.modality: \"voice\".","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Remembering a realtime call in the user's own conversation","lvl3":""}},
|
|
3679
|
+
{"objectID":"2b5f572d39aad7e08324d267d06f5dcbc4e90af9c0c31f1d536c84cc4243c5c1","title":"Realtime Speech-to-Speech Backends (Gemini Live and xAI Grok Voice)","url":"/docs/features/livekit-voice-agent#realtime-speech-to-speech-backends-gemini-live-and-xai-grok-voice","content":"defineRealtimeVoiceAgent runs one realtime model that does speech-to-text,\nreasoning, text-to-speech and turn detection over a single connection. Gemini\nLive on Vertex is the default. xAI's Grok Voice is a second backend, reached\nthrough LiveKit's xAI plugin.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Realtime Speech-to-Speech Backends (Gemini Live and xAI Grok Voice)","lvl3":""}},
|
|
3680
|
+
{"objectID":"3ea0de3512723dda2eba65763b09a6e3eff2ded8a4d0aa2fa41581d46b88e6c7","title":"Status","url":"/docs/features/livekit-voice-agent#status","content":"Tested against a stub WebSocket server only. The stub listens on\n 127.0.0.1. It shows what the LiveKit xAI plugin sends to whatever it is\n pointed at, and how this package reads the OpenAI-style events it is given.\n It shows nothing about how xAI answers.\nNo request has been made to xAI by this project. No xAI key was available\n when the backend was written.\nNot established here: behaviour against the live service, latency,\n accuracy, and every xAI benchmark. Nothing in this section says the xai\n backend works with the real service. Treat it as untested against xAI until\n someone runs a call with a real key.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Status","lvl3":""}},
|
|
3681
|
+
{"objectID":"6676179c44d375115fe98161f9377ba40357b82eebf4a99b7b330ede9721e8c3","title":"Choosing the backend","url":"/docs/features/livekit-voice-agent#choosing-the-backend","content":"| Variable | Backend | Meaning |\n| --------------------------------------------------------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| VOICE_REALTIME_BACKEND | both | gemini or xai. Any other value throws at startup, naming the variable and the two choices and repeating nothing from the environment. |\n| VOICE_LIVE_MODEL | both | Realtime model id. Default gemini-live-2.5-flash for gemini, grok-voice-3.0 for xai. |\n| VOICE_S2S_VOICE | both | Voice name.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Choosing the backend","lvl3":""}},
|
|
3682
|
+
{"objectID":"632f3752058fa7c7c6d48b02af1e8d5212b7daa55001e8cc931227404009278f","title":"Dependencies","url":"/docs/features/livekit-voice-agent#dependencies","content":"The xai backend needs @livekit/agents-plugin-xai and\n@livekit/agents-plugin-openai. Both are optional dependencies of this package,\nso they are installed with its other optional dependencies (an install that\nomits optional dependencies has to add them by hand), and both resolved to\n1.4.6 in its lockfile. The xAI plugin asks for @livekit/agents 1.4.6 exactly,\nwhich is why that package moved from 1.4.5 to 1.4.6 with it. The options the\nGemini backend builds are unchanged by the move, but no live Gemini call was\nrun after it.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Dependencies","lvl3":""}},
|
|
3683
|
+
{"objectID":"a890643483526cae888fdc083535c83d5313ddef29c4a24f6d2771e94dcf3f71","title":"What the xai backend does","url":"/docs/features/livekit-voice-agent#what-the-xai-backend-does","content":"The LiveKit xAI plugin extends the OpenAI realtime model. Against a stub it\nconnects to <base URL>/realtime?model=<id> with Authorization: Bearer\n<XAI_API_KEY> (the key is never put in the URL) and its first event is a\nsession.update that carries the model, the voice, output_modalities:\n[\"audio\"], server-side voice-activity detection (threshold 0.5, 300 ms prefix\npadding, 200 ms silence, response creation and interruption on) and an input\ntranscription setting. The plugin's default endpoint is wss://api.x.ai/v1.\n\nTwo things in that list are the plugin's and are not verified against xAI:\nthe plugin sends gpt-4o-mini-transcribe, an OpenAI model name, as the input\n transcription model;\nthe plugin's own default model is grok-voice-think-fast-1.0. NeuroLink\n defaults to grok-voice-3.0, the flagship id xAI's documentation lists\n (together with the alias grok-voice-latest and a deprecated\n grok-voice-2.0). The plugin passes the id through; whether the service\n accepts it through the plugin has not been tested.\n\nA missing key stops the call before it connects. If XAI_API_KEY is unset\nthe plugin falls back to OPENAI_API_KEY, and a stub server showed that\nfallback sending the OpenAI key as the Bearer token to the configured xAI URL.\n(An empty value does not fall back: the plugin throws its own OpenAI API key is\nrequired error. A whitespace-only value is sent as the key.) NeuroLink\ntherefore refuses an unset, empty or whitespace-only key: it throws\nXAI_API_KEY is not set ... when the agent starts and again in\ncreateRealtimeLlm, and no connection is made.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"What the xai backend does","lvl3":""}},
|
|
3684
|
+
{"objectID":"b4b0a14667e072deb333f9771c062361a617e33da9a0142087c0ba9502d3e4f7","title":"How each part of the agent behaves on xai","url":"/docs/features/livekit-voice-agent#how-each-part-of-the-agent-behaves-on-xai","content":"| Part | On xai |\n| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Vertex ADC and GEMINI_API_KEY clearing | Not run. The xai backend never touches the Gemini environment. |\n| Data-channel event bridge, HITL, room context | Unchanged. They work on the LiveKit room and the agent session, not on a model. |\n| Session events (user transcript, assistant text, turn done, usage) | Unchanged code. Against a stub, OpenAI-style transcript and usage events reach the shapes the agent reads. |\n| MCP tool bridge and schema sanitizer | Unchanged. The sanitizer produces plain JSON Schema (the OpenAI-style API takes full JSON Schema), and the plugin forwards it as is. Keeping it is the safer choice: it drops $ref and unions that xAI has not been tried with. |\n| Conversation memory (recording and seeding) | Recording is shared by both backends.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"How each part of the agent behaves on xai","lvl3":""}},
|
|
3685
|
+
{"objectID":"f2c4f00431a110b4ec6b67653e157a21d79cc3d52751253260a13758eec90e66","title":"Still unknown without a real key","url":"/docs/features/livekit-voice-agent#still-unknown-without-a-real-key","content":"Whether xAI accepts the model ids above through the plugin; whether it accepts\nthe input transcription setting; whether it accepts the sanitized tool schemas\nand underscore tool names; the event names and ordering xAI really sends (the\nplugin line pinned here, 1.4.6, predates later xAI-specific handling in newer\nplugin releases); whether the seeded history is accepted; and how interruption,\ntruncation and tool calls behave. A live smoke test with a key supplied by the\nowner is the missing step.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Still unknown without a real key","lvl3":""}},
|
|
3686
|
+
{"objectID":"9eb38592a55bd3da33ee8a936b4648d18645a49909579f1fccb19844c8263ac7","title":"Troubleshooting","url":"/docs/features/livekit-voice-agent#troubleshooting","content":"VOICE_REALTIME_BACKEND must be \"gemini\" or \"xai\": fix the value, or unset it\n for gemini.\nXAI_API_KEY is not set: set it in the worker's environment. The check also\n rejects a whitespace-only value.\nXAI_REALTIME_BASE_URL must be a ws:// or wss:// URL: use a WebSocket URL,\n not an https:// one.\nA call that will not start on the xai backend: confirm that VOICE_LIVE_MODEL\n and VOICE_S2S_VOICE hold xAI values and not leftover Gemini ones, since both\n variables are shared.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Troubleshooting","lvl3":""}},
|
|
3679
3687
|
{"objectID":"7ffe7e32a7c2baea6c491d743617448eb70eeb4afe439960050efdc3ead18c48","title":"Implementation Plan","url":"/docs/features/livekit-voice-agent#implementation-plan","content":"The integration is built and validated in phases. Each phase is independently testable.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Implementation Plan","lvl3":""}},
|
|
3680
3688
|
{"objectID":"d4c9885eea3272e250fa1a0dbc1af9503b1a24f0217a82dd0c30d93bf6c5df31","title":"Phase 0 — Console-mode spike (no infrastructure)","url":"/docs/features/livekit-voice-agent#phase-0-console-mode-spike-no-infrastructure","content":"Build a minimal agent (Silero VAD + Deepgram STT + ElevenLabs TTS + llmNode → neurolink.stream()) and run it in console mode using the host machine's mic/speakers. Validates the NeuroLink brain loop, conversationId history, and a tool call — with no LiveKit server and no browser. Requires only STT/TTS and LLM credentials.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Phase 0 — Console-mode spike (no infrastructure)","lvl3":""}},
|
|
3681
3689
|
{"objectID":"9dddf98f952af643b5357bdefbcb5554a3ff2d9c33a16b0c432243fd68db5569","title":"Phase 1 — NeuroLink LiveKit module","url":"/docs/features/livekit-voice-agent#phase-1-neurolink-livekit-module","content":"Implement brain.ts, voiceAgent.ts, voiceAgentWorker.ts, tokens.ts, config.ts; add the @juspay/neurolink/livekit export and optional/peer dependencies. The worker factory accepts an external NeuroLink instance so a host application's registered tools are available. Wire abort-on-interrupt. Verify build, type-check, and lint.","hierarchy":{"lvl0":"Features","lvl1":"LiveKit Voice Agent — Real-Time Voice over WebRTC","lvl2":"Phase 1 — NeuroLink LiveKit module","lvl3":""}},
|
|
@@ -3987,7 +3995,7 @@
|
|
|
3987
3995
|
{"objectID":"2f01481e1bd920ff04b7d26da5bc86efababd09cbd1f763a1f119fb7d1443582","title":"Serverless and short-lived processes","url":"/docs/features/observability#serverless-and-short-lived-processes","content":"Spans are batched. Flush before the process freezes or exits:\n\nOr send each span as it ends:\n\nshutdownOpenTelemetry() is terminal for the process: telemetry cannot restart afterwards. While other NeuroLink instances are still alive it only flushes.","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Serverless and short-lived processes","lvl3":""}},
|
|
3988
3996
|
{"objectID":"721428a68bdcef28a5437443fe5a57565c6aa3b324a48ccc39728e5d7ff25e2d","title":"Health","url":"/docs/features/observability#health","content":"isHealthy and hasProcessor are true only while a Langfuse exporter is registered, whether NeuroLink owns the provider or runs beside a host provider. With a host provider, the exporter still needs createExporterSpanProcessor() on that provider to receive spans.","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Health","lvl3":""}},
|
|
3989
3997
|
{"objectID":"68b0ab87bc1ff79e31454e9550e76c174e5a1a4628f3c9de0e74a7dc6e07bdd2","title":"Proxy","url":"/docs/features/observability#proxy","content":"The proxy records the pool account label (not an email address), sets userId and sessionId from x-neurolink-user-id and x-neurolink-session-id, names traces proxy:<model>, and records request and response bodies, usage, cost and time to first token. See the Telemetry Guide and Claude Proxy Observability.","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Proxy","lvl3":""}},
|
|
3990
|
-
{"objectID":"818b18185bdd4b8b7003f170da3113452c78895619ec762279322c8b2b587801","title":"Voice (LiveKit)","url":"/docs/features/observability#voice-livekit","content":"Pipeline mode traces through stream() and carries the voice tag. Realtime mode records one trace per conversation turn, with transcripts as input and output and tool calls as tool records, and only when voice memory is enabled. Full transcripts are sent to Langfuse by default; use mask for sensitive data.","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Voice (LiveKit)","lvl3":""}},
|
|
3998
|
+
{"objectID":"818b18185bdd4b8b7003f170da3113452c78895619ec762279322c8b2b587801","title":"Voice (LiveKit)","url":"/docs/features/observability#voice-livekit","content":"Pipeline mode traces through stream() and carries the voice tag. Realtime mode records one trace per conversation turn, with transcripts as input and output and tool calls as tool records, and only when voice memory is enabled. The realtime generation record carries the model id and the provider of the realtime backend (vertex for Gemini Live, xai for Grok Voice). Full transcripts are sent to Langfuse by default; use mask for sensitive data.","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Voice (LiveKit)","lvl3":""}},
|
|
3991
3999
|
{"objectID":"6ddae5fd4f96e8c2a1c9d924054685440eb46353183984ff0b8d3167e12071b8","title":"Troubleshooting","url":"/docs/features/observability#troubleshooting","content":"| Symptom | Check |\n| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Nothing reaches Langfuse | Both keys set and langfuse not false; getLangfuseHealthStatus(); call flushOpenTelemetry() on exit |\n| Spans missing with own OTEL setup | Add createExporterSpanProcessor() to your provider and pass the Langfuse keys to NeuroLink (or set LANGFUSE_*), or use new LangfuseSpanProcessor() with its own keys; register the processor before the first NeuroLink call |\n| Wrong host | Variable is LANGFUSE_BASE_URL, not LANGFUSE_BASEURL |\n| Traces stop after shutdown | shutdownOpenTelemetry() cannot be undone in the same process; use flushOpenTelemetry() |","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Troubleshooting","lvl3":""}},
|
|
3992
4000
|
{"objectID":"c26071897c848ba9cf00651234ebd459171008740460f6075451e866420e5881","title":"Migrating from the previous version","url":"/docs/features/observability#migrating-from-the-previous-version","content":"This release is breaking.","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Migrating from the previous version","lvl3":""}},
|
|
3993
4001
|
{"objectID":"310e635468902de6eca8789d9ce5905c4ecb08a665b802119f33a6ae78a6f429","title":"Configuration fields","url":"/docs/features/observability#configuration-fields","content":"| Removed | Replacement |\n| ------------------------------------------------------------- | ----------------------------------------------------------------------- |\n| langfuse.enabled | Set the keys to enable; langfuse: false disables |\n| langfuse.userId, langfuse.sessionId | Per-call context.userId, context.sessionId, or setLangfuseContext |\n| useExternalTracerProvider, autoDetectExternalProvider | Automatic: a registered global provider is detected |\n| skipLangfuseSpanProcessor | Register your own provider; NeuroLink then adds nothing |\n| mediaUploadEnabled, LANGFUSE_MEDIA_UPLOAD_ENABLED | Removed; the exporter does not upload media |\n| autoDetectOperationName, operationName, traceNameFormat | context.traceName |\n| customAttributes | context.metadata |\n| conversationId, requestId on the config | context.conversationId, context.requestId (stored as metadata) |\n| NEUROLINK_TELEMETRY_ENABLED | Removed; the LANGFUSE_* keys enable tracing |\n| LANGFUSE_BASEURL hint | LANGFUSE_BASE_URL |","hierarchy":{"lvl0":"Features","lvl1":"Observability Guide","lvl2":"Configuration fields","lvl3":""}},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.4.0",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002/call) for routing, tool selection and context budgeting. MCP-native (4 transports), voice TTS/STT/realtime, RAG, agents, memory, compaction, 9 observability exporters. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · TypeSafe Jev and more.",
|
|
6
6
|
"author": {
|
|
@@ -115,6 +115,7 @@
|
|
|
115
115
|
"test:forced-tool-declared": "pnpm exec tsx test/continuous-test-suite-forced-tool-declared.ts",
|
|
116
116
|
"test:span-content-redaction": "pnpm exec tsx test/continuous-test-suite-span-content-redaction.ts",
|
|
117
117
|
"test:voice-turn-memory": "pnpm exec tsx test/continuous-test-suite-voice-turn-memory.ts",
|
|
118
|
+
"test:realtime-voice": "pnpm exec tsx test/continuous-test-suite-realtime-voice.ts",
|
|
118
119
|
"test:error-classification-e2e": "pnpm exec tsx test/continuous-test-suite-error-classification-e2e.ts",
|
|
119
120
|
"test:error-classifier-contract": "pnpm exec tsx test/continuous-test-suite-error-classifier-contract.ts",
|
|
120
121
|
"test:bedrock-inference-profile": "pnpm exec tsx test/continuous-test-suite-bedrock-inference-profile.ts",
|
|
@@ -235,7 +236,7 @@
|
|
|
235
236
|
"// 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)": "",
|
|
236
237
|
"test:tool-routing": "pnpm exec tsx test/continuous-test-suite-tool-routing.ts",
|
|
237
238
|
"test:tool-routing-semantic": "pnpm exec tsx test/continuous-test-suite-tool-routing-semantic.ts",
|
|
238
|
-
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:registry && 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:mcp-direct-name-repair && pnpm run test:mcp-breaker-resolved-errors && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:image-exif && 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 && pnpm run test:retired-model-defaults && pnpm run test:cli-fallback && pnpm run test:embedding-response-validation && pnpm run test:xiaomi-mimo-pilot && pnpm run test:rag-bounds && pnpm run test:longcat-pilot && pnpm run test:runinfra-pilot && pnpm run test:catalog-output-ceiling && pnpm run test:package-declarations",
|
|
239
|
+
"test:unit": "pnpm run test:bugfixes && pnpm run test:mcp:infra && pnpm run test:mcp:registry && 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:mcp-direct-name-repair && pnpm run test:mcp-breaker-resolved-errors && pnpm run test:archive:security && pnpm run test:office:security && pnpm run test:image-exif && 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 && pnpm run test:retired-model-defaults && pnpm run test:cli-fallback && pnpm run test:embedding-response-validation && pnpm run test:xiaomi-mimo-pilot && pnpm run test:rag-bounds && pnpm run test:longcat-pilot && pnpm run test:runinfra-pilot && pnpm run test:catalog-output-ceiling && pnpm run test:package-declarations && pnpm run test:realtime-voice",
|
|
239
240
|
"// 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)": "",
|
|
240
241
|
"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 && pnpm run test:model-not-found-retryable",
|
|
241
242
|
"// 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)": "",
|
|
@@ -560,14 +561,16 @@
|
|
|
560
561
|
"@hono/node-server": "^1.19.15",
|
|
561
562
|
"@koa/cors": "^5.0.0",
|
|
562
563
|
"@koa/router": "^15.3.1",
|
|
563
|
-
"@livekit/agents": "^1.4.
|
|
564
|
+
"@livekit/agents": "^1.4.6",
|
|
564
565
|
"@livekit/agents-plugin-cartesia": "^1.4.5",
|
|
565
566
|
"@livekit/agents-plugin-deepgram": "^1.4.5",
|
|
566
567
|
"@livekit/agents-plugin-elevenlabs": "^1.4.5",
|
|
567
568
|
"@livekit/agents-plugin-google": "^1.4.5",
|
|
568
569
|
"@livekit/agents-plugin-livekit": "^1.4.5",
|
|
570
|
+
"@livekit/agents-plugin-openai": "^1.4.6",
|
|
569
571
|
"@livekit/agents-plugin-silero": "^1.4.5",
|
|
570
572
|
"@livekit/agents-plugin-soniox": "^1.4.5",
|
|
573
|
+
"@livekit/agents-plugin-xai": "^1.4.6",
|
|
571
574
|
"@livekit/rtc-node": "^0.13.29",
|
|
572
575
|
"@picovoice/cobra-node": "^3.0.2",
|
|
573
576
|
"bullmq": "^5.52.2",
|