LiveKit

Slots

Slot Class Route Routable
stt openai.STT /v1/audio/transcriptions 11 of 13
llm openai.LLM /v1/chat/completions 13 of 15
tts openai.TTS /v1/audio/speech 15 of 15

Setup

mkdir speko-livekit && cd speko-livekit
uv init --bare --python 3.11
uv add "livekit-agents[openai]~=1.6" "python-dotenv>=1.0,<2"
export SPEKO_API_KEY=sk_live_...
bun init -y && bun add @livekit/agents @livekit/agents-plugin-openai @livekit/rtc-node openai zod tsx

Catalog

curl -s https://api.speko.ai/v1/models
Response
{"data": [{"id": "deepgram:nova-3", "object": "model", "provider": "Deepgram",
           "model": "Nova-3", "api": "stt", "routable": true}]}

Agent

"""Minimal LiveKit Agents voice worker using Speko's OpenAI-compatible API."""

from __future__ import annotations

import os

from dotenv import load_dotenv
from livekit import agents
from livekit.agents import Agent, AgentServer, AgentSession, JobContext
from livekit.plugins import openai
from openai import AsyncOpenAI


load_dotenv()

SPEKO_API_KEY = os.getenv("SPEKO_API_KEY")
SPEKO_BASE_URL = "https://api.speko.ai/v1"
SPEKO_OBJECTIVE = os.getenv("SPEKO_OBJECTIVE", "balanced")
SPEKO_LANGUAGE = os.getenv("SPEKO_LANGUAGE", "en")

if not SPEKO_API_KEY:
    raise RuntimeError("SPEKO_API_KEY is required")


def speko_client() -> AsyncOpenAI:
    """Build one OpenAI-compatible client shared by all three stages."""

    return AsyncOpenAI(
        api_key=SPEKO_API_KEY,
        base_url=SPEKO_BASE_URL,
        default_headers={
            "X-Speko-Objective": SPEKO_OBJECTIVE,
            "X-Speko-Language": SPEKO_LANGUAGE,
        },
    )


server = AgentServer()


@server.rtc_session()
async def entrypoint(ctx: JobContext) -> None:
    client = speko_client()
    session = AgentSession(
        # Segmented STT is used here so the example works with Speko's
        # transcription route instead of opening a speech-to-speech socket.
        stt=openai.STT(
            client=client,
            model="auto",
            use_realtime=False,
            language=SPEKO_LANGUAGE,
        ),
        llm=openai.LLM(
            client=client,
            model="auto",
        ),
        # The Python LiveKit OpenAI TTS transport expects the raw PCM route.
        # Speko still selects the underlying provider when model is tts-1.
        tts=openai.TTS(
            client=client,
            model="tts-1",
            voice="alloy",
            response_format="pcm",
        ),
    )

    await session.start(
        Agent(
            instructions=(
                "You are a helpful voice assistant. "
                "Keep replies concise and conversational."
            )
        ),
        room=ctx.room,
    )
    await ctx.connect()
    await session.generate_reply(
        instructions="Greet the caller in one short sentence and offer help."
    )


if __name__ == "__main__":
    agents.cli.run_app(server)
import { fileURLToPath } from 'node:url';
import { type JobContext, ServerOptions, cli, defineAgent, voice } from '@livekit/agents';
import * as openai from '@livekit/agents-plugin-openai';

const apiKey = process.env.SPEKO_API_KEY!;
const baseURL = 'https://api.speko.ai/v1';

export default defineAgent({
  entry: async (ctx: JobContext) => {
    // No vad: 1.6 loads a bundled Silero when the option is absent, and
    // @livekit/local-inference ships with the framework.
    const session = new voice.AgentSession({
      stt: new openai.STT({ apiKey, baseURL, model: 'auto', language: 'en', useRealtime: false }),
      llm: new openai.LLM({ apiKey, baseURL, model: 'auto' }),
      // The Node plugin always posts response_format: "pcm" and never parses
      // SSE, so 'auto' is right here where the Python file needs 'tts-1'.
      tts: new openai.TTS({ apiKey, baseURL, model: 'auto', voice: 'alloy' }),
    });

    await session.start({
      agent: new voice.Agent({ instructions: 'Be concise and helpful.' }),
      room: ctx.room,
    });
    await ctx.connect();
  },
});

cli.runApp(new ServerOptions({ agent: fileURLToPath(import.meta.url) }));

Run

LanguageCommandNeeds
Python uv run python agent.py console Save the Python example as agent.py. Talks in the terminal; no LiveKit credentials.
TypeScript bunx tsx agent.ts dev Needs LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET.

Override per request

import os

from livekit.plugins import openai
from openai import AsyncOpenAI

client = AsyncOpenAI(
    api_key=os.environ["SPEKO_API_KEY"],
    base_url="https://api.speko.ai/v1",
    default_headers={"X-Speko-Objective": "latency", "X-Speko-Language": "es"},
)

stt = openai.STT(client=client, model="auto", language="es")
llm = openai.LLM(client=client, model="auto")
tts = openai.TTS(client=client, model="tts-1", voice="alloy", response_format="pcm")
import OpenAI from 'openai';
import * as openai from '@livekit/agents-plugin-openai';

const client = new OpenAI({
  apiKey: process.env.SPEKO_API_KEY,
  baseURL: 'https://api.speko.ai/v1',
  defaultHeaders: { 'X-Speko-Objective': 'latency', 'X-Speko-Language': 'es' },
});

const stt = new openai.STT({ client, model: 'auto', language: 'es' });
const llm = new openai.LLM({ client, model: 'auto' });
const tts = new openai.TTS({ client, model: 'auto', voice: 'alloy' });

Limits

model="auto" on openai.TTS, Python only The plugin parses SSE and hears silence. Use model="tts-1"; the router still picks the provider.
detect_language=True Sends an empty language field, which the router rejects with invalid_routing_header.
vad=None Re-arms the RuntimeError a segmented speech-to-text raises on the first user turn. Omit the argument.
use_realtime / useRealtime Opens the speech-to-speech socket instead of the transcription route this page configures.

Reference

Quickstart Routing headers, response headers, errors
Pipecat The same wiring in a Pipecat pipeline
Models The measured rows the ranking reads
LiveKit Agents Framework documentation
Node API reference Class signatures
One router for speech-to-text, LLM and text-to-speech.

Speko