@juspay/neurolink 13.3.0 → 13.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -25,6 +25,7 @@
25
25
  "timeouts": { "decideMs": 10000 },
26
26
  "decisionLimits": {
27
27
  "maxStateTokens": 922000,
28
+ "maxQuestions": 200,
28
29
  "advisory": true,
29
30
  "media": { "maxImages": 8, "video": false, "maxRequestBytes": 33554432 }
30
31
  },
@@ -67,7 +68,7 @@
67
68
  "evidence": {
68
69
  "rosterVerified": {
69
70
  "date": "2026-10-07",
70
- "method": "OpenAI's Decisions guide and gpt-6-luna model page saved 2026-10-07: gpt-6-luna is the only model the endpoint supports. Read from documentation; no live call"
71
+ "method": "OpenAI's Decisions guide and gpt-6-luna model page saved 2026-10-07: gpt-6-luna is the only model the endpoint supports. The 200-question cap was read on 2026-10-17 from the questions docstring (Ask 1–200 questions about the input) in OpenAI's Python SDK, src/openai/types/decision_create_params.py in github.com/openai/openai-python, which is generated from OpenAI's API definition; the Decisions guide itself states no figure. Read from documentation; no live call, and a request over 200 questions was not tried"
71
72
  },
72
73
  "liveMatrix": null,
73
74
  "addedInPR": "https://github.com/juspay/neurolink/pull/1996"
@@ -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) does STT,
428
- * reasoning, TTS, and turn detection — so there is no separate STT/TTS/VAD/EOU
429
- * config. `resolveRealtimeVoiceConfig` fills every field from `process.env`
430
- * (with defaults); `RealtimeVoiceAgentConfig` lets a caller override any of them.
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
- /** Vertex project id (from VERTEX_PROJECT / GOOGLE_AUTH_* / GOOGLE_CLOUD_PROJECT_ID). */
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 Gemini voice name; omit for the plugin default. */
459
+ /** Optional voice name for either backend; omit for the plugin default. */
440
460
  voice: string | undefined;
441
- /** Response modality: "AUDIO" (native S2S) or "TEXT" (half-cascade). */
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 Gemini function tools. */
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
- * Gemini waits on the function result, so the user gets silence rather than
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 (Gemini Live speech-to-speech) voice configuration from
28
- * the environment.
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. Vertex `project` is resolved from `VERTEX_PROJECT`, then the
32
- * service-account project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then
33
- * `GOOGLE_CLOUD_PROJECT_ID`. The MCP URL is `VOICE_MCP_URL` if set, otherwise
34
- * `${LIGHTHOUSE_URL}/ai/mcp/v2`.
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
- * Resolve the realtime (Gemini Live speech-to-speech) voice configuration from
79
- * the environment.
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. Vertex `project` is resolved from `VERTEX_PROJECT`, then the
83
- * service-account project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then
84
- * `GOOGLE_CLOUD_PROJECT_ID`. The MCP URL is `VOICE_MCP_URL` if set, otherwise
85
- * `${LIGHTHOUSE_URL}/ai/mcp/v2`.
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 → Gemini function tools bridge (realtime mode).
2
+ * MCP tools → realtime function tools bridge (realtime mode).
3
3
  *
4
- * In speech-to-speech mode Gemini owns the tool loop, so an external MCP
5
- * server's tools are registered as LiveKit/Gemini function tools rather than
6
- * NeuroLink tools. This connects to the MCP server as the calling user
7
- * (`x-auth-token` + base64 `x-context`), sanitizes each tool's schema into the
8
- * Gemini-safe subset, wires WRITE-labeled (destructive) tools through a HITL
9
- * confirmation, and forwards tool start/result events to the browser bridge.
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 → Gemini function tools bridge (realtime mode).
2
+ * MCP tools → realtime function tools bridge (realtime mode).
3
3
  *
4
- * In speech-to-speech mode Gemini owns the tool loop, so an external MCP
5
- * server's tools are registered as LiveKit/Gemini function tools rather than
6
- * NeuroLink tools. This connects to the MCP server as the calling user
7
- * (`x-auth-token` + base64 `x-context`), sanitizes each tool's schema into the
8
- * Gemini-safe subset, wires WRITE-labeled (destructive) tools through a HITL
9
- * confirmation, and forwards tool start/result events to the browser bridge.
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 Gemini when the user rejects a HITL tool.
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 — Gemini blocks on the function
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
- /** Verify the call's binding and prepare its recorder; `undefined` means "no memory for this call". */
22
- export declare function setUpRealtimeCallMemory(ctx: Pick<JobContext, "room" | "addShutdownCallback">, config: RealtimeVoiceMemoryConfig | undefined, model?: string): Promise<RealtimeCallMemory | undefined>;
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: "vertex" },
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
- /** Verify the call's binding and prepare its recorder; `undefined` means "no memory for this call". */
84
- export async function setUpRealtimeCallMemory(ctx, config, model) {
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, { sessionId, userId, roomName: ctx.room.name, model });
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 (Gemini Live speech-to-speech).
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 detection over one
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`, `@livekit/agents-plugin-google`, `@livekit/rtc-node`, and
10
- * `@google/genai` are imported dynamically so the core package does not require
11
- * them unless the realtime agent is used. Type-only imports are erased at build.
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 (Gemini Live S2S) LiveKit voice agent.
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