@juspay/neurolink
Version:
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
134 lines (133 loc) • 5.7 kB
JavaScript
/**
* Environment resolution for the LiveKit voice agent.
*
* Reads LiveKit connection settings and LLM defaults from `process.env` with
* descriptive errors for missing required values. No type assertions: presence
* is verified with explicit string checks.
*
* See docs/features/livekit-voice-agent.md.
*/
const DEFAULT_LLM_PROVIDER = "bedrock";
const DEFAULT_LLM_MODEL = "claude-sonnet-4-6";
/** Read a required environment variable or throw a descriptive error. */
function requireEnv(name) {
const value = process.env[name];
if (typeof value !== "string" || value.trim().length === 0) {
throw new Error(`${name} is not set in environment (required for the LiveKit voice agent)`);
}
return value.trim();
}
/** Read an optional environment variable, falling back to a default. */
function readEnv(name, fallback) {
const value = process.env[name];
if (typeof value === "string" && value.trim().length > 0) {
return value.trim();
}
return fallback;
}
function readOptionalEnv(name) {
const value = process.env[name];
return typeof value === "string" && value.trim().length > 0
? value.trim()
: undefined;
}
function readNumberEnv(name, fallback) {
const raw = process.env[name];
if (typeof raw === "string" && raw.trim().length > 0) {
const parsed = Number(raw.trim());
if (Number.isFinite(parsed) && parsed > 0) {
return parsed;
}
}
return fallback;
}
/**
* Resolve LiveKit server connection settings from the environment.
*
* Requires `LIVEKIT_URL`, `LIVEKIT_API_KEY`, and `LIVEKIT_API_SECRET`. Works
* identically for LiveKit Cloud, a self-hosted server, or `livekit-server --dev`
* — only the values differ.
*/
export function resolveLiveKitServerConfig() {
return {
url: requireEnv("LIVEKIT_URL"),
apiKey: requireEnv("LIVEKIT_API_KEY"),
apiSecret: requireEnv("LIVEKIT_API_SECRET"),
};
}
/**
* Resolve the LLM provider/model defaults for the brain.
*
* Defaults to Bedrock / Claude; overridable via `VOICE_LLM_PROVIDER` and
* `VOICE_LLM_MODEL`.
*/
export function resolveBrainDefaults() {
return {
provider: readEnv("VOICE_LLM_PROVIDER", DEFAULT_LLM_PROVIDER),
model: readEnv("VOICE_LLM_MODEL", DEFAULT_LLM_MODEL),
};
}
const DEFAULT_REALTIME_MODEL = "gemini-live-2.5-flash";
const DEFAULT_REALTIME_LOCATION = "global";
const DEFAULT_RESPONSE_MODALITY = "AUDIO";
const DEFAULT_SYSTEM_PROMPT = "You are a concise voice assistant for a merchant dashboard. Keep replies short and spoken.";
const DEFAULT_GREETING = "Briefly greet the merchant and ask how you can help with their store today.";
const DEFAULT_LIGHTHOUSE_URL = "http://localhost:5173";
const DEFAULT_MCP_PATH = "/ai/mcp/v2";
/**
* Resolve the realtime (Gemini Live speech-to-speech) voice configuration from
* the environment.
*
* Every value has a safe default so a worker can run with no realtime-specific
* env at all. Vertex `project` is resolved from `VERTEX_PROJECT`, then the
* service-account project (`GOOGLE_AUTH_BREEZE_PROJECT_ID`), then
* `GOOGLE_CLOUD_PROJECT_ID`. The MCP URL is `VOICE_MCP_URL` if set, otherwise
* `${LIGHTHOUSE_URL}/ai/mcp/v2`.
*/
export function resolveRealtimeVoiceConfig() {
const lighthouseUrl = readEnv("LIGHTHOUSE_URL", DEFAULT_LIGHTHOUSE_URL);
const mcpUrl = readEnv("VOICE_MCP_URL", `${lighthouseUrl}${DEFAULT_MCP_PATH}`);
return {
project: readOptionalEnv("VERTEX_PROJECT") ??
readOptionalEnv("GOOGLE_AUTH_BREEZE_PROJECT_ID") ??
readOptionalEnv("GOOGLE_CLOUD_PROJECT_ID"),
location: readEnv("VERTEX_LOCATION", DEFAULT_REALTIME_LOCATION),
model: readEnv("VOICE_LIVE_MODEL", DEFAULT_REALTIME_MODEL),
voice: readOptionalEnv("VOICE_S2S_VOICE"),
responseModality: readEnv("VOICE_RESPONSE_MODALITY", DEFAULT_RESPONSE_MODALITY).toUpperCase(),
systemPrompt: readEnv("VOICE_SYSTEM_PROMPT", DEFAULT_SYSTEM_PROMPT),
greeting: readEnv("VOICE_GREETING", DEFAULT_GREETING),
toolsEnabled: readEnv("VOICE_S2S_TOOLS", "").toLowerCase() === "true",
mcpUrl,
emptyRoomGraceMs: readNumberEnv("VOICE_EMPTY_ROOM_GRACE_MS", 30_000),
joinDeadlineMs: readNumberEnv("VOICE_JOIN_DEADLINE_MS", 60_000),
hitlTimeoutMs: readNumberEnv("VOICE_HITL_TIMEOUT_MS", 45_000),
metricsIntervalMs: readNumberEnv("VOICE_METRICS_INTERVAL_MS", 10_000),
};
}
const EOU_TRUTHY = new Set(["1", "true", "english", "en", "on", "yes"]);
/**
* Resolve the semantic end-of-utterance (EOU) turn-detection settings.
*
* Opt-in via `LIVEKIT_EOU_TURN_DETECTION` (`1`/`true`/`english`/`en`/`on`/`yes`).
* When enabled, the English `@livekit/agents-plugin-livekit` EOU model decides
* whether the user's turn is truly over, layered on top of VAD silence — so
* natural mid-sentence pauses don't split one utterance. English-only; the
* model adds ~200MB RAM per worker and ~10ms per turn-end.
*
* `LIVEKIT_EOU_UNLIKELY_THRESHOLD` optionally overrides the model's confidence
* threshold (lower = end the turn more eagerly).
*/
export function resolveEouTurnDetection() {
const raw = process.env.LIVEKIT_EOU_TURN_DETECTION;
const enabled = typeof raw === "string" && EOU_TRUTHY.has(raw.trim().toLowerCase());
const thresholdRaw = process.env.LIVEKIT_EOU_UNLIKELY_THRESHOLD;
let unlikelyThreshold;
if (typeof thresholdRaw === "string" && thresholdRaw.trim().length > 0) {
const parsed = Number(thresholdRaw.trim());
if (Number.isFinite(parsed)) {
unlikelyThreshold = parsed;
}
}
return { enabled, unlikelyThreshold };
}