xAI Grok Realtime
Speech-to-speech with GrokRealtimeLLMService, over xAI's Grok Voice Agent WebSocket.
Overview
GrokRealtimeLLMService: one WebSocket session with xAI's Grok Voice Agent API, which hears the user's audio and answers with audio. It replaces the STT, LLM and TTS services in a pipeline, and supports function calling, xAI's built-in tools (web search, X search, file search, remote MCP), transcripts of both sides, DTMF on SIP sessions and fixed spoken lines.
Upstream page
Pipecat's documentation for the same service.
Source
packages/xai/src/realtime/llm.ts and events.ts.
xAI docs
The Grok Voice Agent API, voices and tools.
Installation
bun add @pipedog/xaiimport { GrokRealtimeLLMService } from "@pipedog/xai/realtime/llm";
import { SessionProperties } from "@pipedog/xai/realtime/events";@pipedog/xai/realtime/events holds the session classes (SessionProperties,
AudioConfiguration, AudioInput, AudioOutput, InputAudioTranscription, TurnDetection,
Reasoning, SessionResumption), the tools (WebSearchTool, XSearchTool, FileSearchTool,
FunctionTool, McpTool) and the client and server event models.
Prerequisites
- An xAI account and an API key from the xAI console.
- Pass the key as
apiKey(required); it is sent as a bearer token. The examples read it fromXAI_API_KEY.
GrokRealtimeLLMService
Configuration
Prop
Type
Settings
Runtime-updatable settings, passed as settings: new GrokRealtimeLLMService.Settings({...})
(GrokRealtimeLLMSettings). Any setting you leave out keeps the default below.
| Setting | Type | Default | Description |
|---|---|---|---|
model | string | "grok-voice-latest" | Voice model. Fixed for the session: it goes in the connection URL. |
systemInstruction | string | null | null | Instructions. Kept in sync with sessionProperties.instructions. |
sessionProperties | SessionProperties | new SessionProperties() | Session config: voice (default "eve"), turnDetection (default server VAD), audio, tools, reasoning, resumption, pronunciation replace map. |
temperature, maxTokens, topP, topK, frequencyPenalty, presencePenalty, seed | number | null | null | Not used by the Voice Agent API. |
filterIncompleteUserTurns | boolean | false | Filter incomplete user turns. |
userTurnCompletionConfig | UserTurnCompletionConfig | null | null | Turn completion config used with filterIncompleteUserTurns. |
A sessionProperties in an update replaces the stored one as a whole. The top-level
systemInstruction wins over instructions inside it. Audio formats default to PCM at the
pipeline's rates.
Usage
import { LLMUpdateSettingsFrame } from "@pipedog/core/frames/frames";
import {
AudioConfiguration,
AudioInput,
type ConversationItem,
InputAudioTranscription,
SessionProperties,
TurnDetection,
WebSearchTool,
} from "@pipedog/xai/realtime/events";
import { GrokRealtimeLLMService } from "@pipedog/xai/realtime/llm";
const llm = new GrokRealtimeLLMService({
apiKey: process.env.XAI_API_KEY ?? "",
settings: new GrokRealtimeLLMService.Settings({
systemInstruction: "You are a helpful assistant in a voice conversation.",
sessionProperties: new SessionProperties({
voice: "rex",
turnDetection: new TurnDetection({ type: "server_vad", silenceDurationMs: 500 }),
audio: new AudioConfiguration({
input: new AudioInput({
transcription: new InputAudioTranscription({ model: "grok-transcribe" }),
}),
}),
tools: [new WebSearchTool()],
}),
}),
});
llm.addEventHandler(
"on_conversation_item_created",
async (_service, itemId: string, _item: ConversationItem) => {
console.log("conversation item created:", itemId);
},
);Speak a fixed line without involving the model:
// Speak a fixed line without the model (no response.create follows).
await llm.forceMessage("This call is being recorded.");Change the instructions mid-call with an LLMUpdateSettingsFrame. A change to
systemInstruction or sessionProperties is sent as a session.update; other fields are
ignored with a warning:
// Sends a session.update to the open session.
const update = new LLMUpdateSettingsFrame({
delta: new GrokRealtimeLLMService.Settings({
systemInstruction: "Answer in Spanish from now on.",
}),
});Event handlers
| Event | Description |
|---|---|
on_conversation_item_created | A conversation item was added. Called with the item id and the item. |
on_conversation_item_updated | A conversation item changed (a transcript arrived, or the response finished). Called with the item id and the item, or null. |
on_conversation_item_deleted | The server confirmed a deletion. Called with the item id. |
on_conversation_item_truncated | The server confirmed a truncation. Called with the item id, content index and audio end (ms). |
on_idle_timeout | turnDetection.idleTimeoutMs fired and the server is checking in. Called with the item id. |
on_dtmf_received | A DTMF digit arrived on a SIP session. Called with the KeypadEntry. |
on_mcp_event | An MCP tool discovery or call event. Called with the event type and the event. |
Notes
- Consumes
InputAudioRawFrame(sent once the session is configured),LLMContextFrame,LLMMessagesAppendFrameandLLMSetToolsFrame. EmitsTTSAudioRawFrame,TTSTextFrameandLLMTextFramefor the bot,InterimTranscriptionFrame(withgrok-transcribe) andTranscriptionFramefor the user, andInputDTMFFrameupstream for DTMF digits. - With server-side VAD (the default), the service broadcasts proposed user turn frames from the
server's speech events and asks the user aggregator for
ExternalUserTurnStrategies, so no local VAD is needed. On interruption it cancels the response and truncates the assistant audio to what was played. - For locally driven turns, set
turnDetection: nullinSessionPropertiesand add a VAD to the user aggregator; the service then commits the audio and asks for a response when the user stops speaking. - Intermediate tool results (
isFinal: false) go in as conversation messages beside the open call; the final result answers the call. forceMessage(),deleteConversationItem(),setAudioInputPaused(),sendClientEvent()andresetConversation()are available at runtime.
Runtime notes
| Runtime | Works | What it needs |
|---|---|---|
| Bun | ✅ yes | Nothing. |
| Node.js | ✅ yes | Nothing. |
| Deno | ✅ yes | Nothing. |
| Cloudflare Workers | ✅ yes | Nothing. |
Generated from pipecat.runtimes and pipecat.compat in packages/xai/package.json: ✅ means the compat smoke test passes on that runtime.
- The socket is opened with
websocketConnect()from core, with the key in theAuthorizationheader. Bun, Node and Deno send it from the globalWebSocket; on Cloudflare Workers core opens the socket throughfetch. - The deprecated
sessionPropertiesconstructor option and thepipecat.services.grok.realtimealiases are not ported: import from@pipedog/xai/realtime/*and passsessionPropertiesinsettings. - A failed response whose
status_detailsisn't a string is reported as JSON (upstream prints a Python dict).
pipedog is an unofficial community TypeScript port of Pipecat. It is not affiliated with or endorsed by Daily or the Pipecat project.
Portions adapted from the Pipecat documentation, Copyright (c) 2024–2025 Daily, BSD-2-Clause (see apps/docs/LICENSE-pipecat-docs).