Inworld Realtime
Speech-to-speech with InworldRealtimeLLMService, over Inworld's Realtime API WebSocket.
Overview
InworldRealtimeLLMService: one WebSocket session with Inworld's Realtime API, which hears the user's audio and answers with audio. Under the hood Inworld runs STT, an LLM of your choice (for exampleopenai/gpt-4.1-mini) and Inworld TTS, with semantic server-side turn detection. It replaces the STT, LLM and TTS services in a pipeline, and supports function calling and transcripts of both sides.
Upstream page
Pipecat's documentation for the same service.
Source
packages/inworld/src/realtime/llm.ts and events.ts.
Inworld docs
The Realtime API over WebSocket.
Installation
bun add @pipedog/inworldimport { InworldRealtimeLLMService } from "@pipedog/inworld/realtime/llm";
import { SessionProperties } from "@pipedog/inworld/realtime/events";@pipedog/inworld/realtime/events holds the session classes (SessionProperties,
AudioConfiguration, AudioInput, AudioOutput, InputTranscription, TurnDetection,
PCMAudioFormat, ...) and the client and server event models.
Prerequisites
- An Inworld account and an API key from the Inworld portal.
- Pass the key as
apiKey(required). With the defaultauthType: "basic"it is sent asAuthorization: Basic <apiKey>(the Base64 key Inworld gives you). UseauthType: "bearer"for a client-side JWT. The examples read it fromINWORLD_API_KEY.
InworldRealtimeLLMService
Configuration
Prop
Type
Settings
Runtime-updatable settings, passed as settings: new InworldRealtimeLLMService.Settings({...})
(InworldRealtimeLLMSettings). Any setting you leave out keeps the default below.
| Setting | Type | Default | Description |
|---|---|---|---|
model | string | llmModel, or "openai/gpt-4.1-mini" | The LLM Inworld runs. Kept in sync with sessionProperties.model. |
systemInstruction | string | null | null | Instructions. Kept in sync with sessionProperties.instructions. |
temperature | number | null | null | Sampling temperature. Kept in sync with sessionProperties.temperature. |
sessionProperties | SessionProperties | see below | Session config: output modalities, audio input and output, tools. |
maxTokens, topP, topK, frequencyPenalty, presencePenalty, seed | number | null | null | Not used by the Realtime API. |
filterIncompleteUserTurns | boolean | false | Filter incomplete user turns. |
userTurnCompletionConfig | UserTurnCompletionConfig | null | null | Turn completion config used with filterIncompleteUserTurns. |
The default session has output modalities ["audio", "text"], PCM audio both ways (the rates
follow the pipeline's), transcription with sttModel (default "inworld/inworld-stt-1"),
semantic turn detection with low eagerness that creates and interrupts responses, and the
voice (default "Clive") and ttsModel (default "inworld-tts-2") for output.
A sessionProperties you pass replaces those defaults as a whole, so give a complete config.
Top-level model, systemInstruction and temperature win over the same fields inside it.
Usage
import { LLMUpdateSettingsFrame } from "@pipedog/core/frames/frames";
import {
AudioConfiguration,
AudioInput,
AudioOutput,
type ConversationItem,
InputTranscription,
PCMAudioFormat,
SessionProperties,
TurnDetection,
} from "@pipedog/inworld/realtime/events";
import { InworldRealtimeLLMService } from "@pipedog/inworld/realtime/llm";
// The shorthand options fill in the default session.
const llm = new InworldRealtimeLLMService({
apiKey: process.env.INWORLD_API_KEY ?? "",
llmModel: "openai/gpt-4.1-mini",
voice: "Sarah",
ttsModel: "inworld-tts-2",
settings: new InworldRealtimeLLMService.Settings({
systemInstruction: "You are a helpful assistant in a voice conversation.",
}),
});
llm.addEventHandler(
"on_conversation_item_created",
async (_service, itemId: string, _item: ConversationItem) => {
console.log("conversation item created:", itemId);
},
);With full control over the session:
// A full SessionProperties replaces the defaults wholesale: give a complete config.
const custom = new InworldRealtimeLLMService({
apiKey: process.env.INWORLD_API_KEY ?? "",
settings: new InworldRealtimeLLMService.Settings({
sessionProperties: new SessionProperties({
model: "openai/gpt-4.1-mini",
temperature: 0.7,
outputModalities: ["audio", "text"],
audio: new AudioConfiguration({
input: new AudioInput({
format: new PCMAudioFormat({ rate: 24000 }),
transcription: new InputTranscription({ model: "inworld/inworld-stt-1" }),
turnDetection: new TurnDetection({ type: "semantic_vad", eagerness: "low" }),
}),
output: new AudioOutput({
format: new PCMAudioFormat({ rate: 24000 }),
voice: "Sarah",
model: "inworld-tts-2",
}),
}),
}),
}),
});Change the instructions mid-call with an LLMUpdateSettingsFrame. A change to
sessionProperties, systemInstruction, model or temperature 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 InworldRealtimeLLMService.Settings({
systemInstruction: "Answer in Spanish from now on.",
temperature: 0.3,
}),
});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. |
Notes
- Consumes
InputAudioRawFrame(sent in about 60 ms chunks),LLMContextFrameandLLMSetToolsFrame. EmitsTTSAudioRawFrame,TTSTextFrameandLLMTextFramefor the bot, andInterimTranscriptionFrame/TranscriptionFramefor the user. - With Inworld's server-side turn detection (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.LLMContextAggregatorPairdetects that this is a realtime service. - For locally driven turns, set
turnDetection: nullinAudioInputand add a VAD to the user aggregator; the service then commits the audio and asks for a response when the user stops speaking. Don't run a local VAD with server-side turn detection on: both would send turn frames. - Tool results are sent as
function_call_outputitems. Intermediate results (isFinal: false) are dropped with a warning: Inworld's model calls the tool again instead of relaying them.cancelOnInterruption: falseisn't reliably supported by Inworld, and the service reports an error when it sees one.LLMMessagesAppendFrameisn't supported yet. setAudioInputPaused()pauses input at runtime,sendClientEvent()sends any Realtime client event, andresetConversation()reconnects with a fresh server-side conversation.
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/inworld/package.json: ✅ means the compat smoke test passes on that runtime.
- The socket is opened with
websocketConnect()from core, with the key in theAuthorizationheader and a fresh sessionkeyin the URL. Bun, Node and Deno send the header from the globalWebSocket; on Cloudflare Workers core opens the socket throughfetch. - 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).