Pipecat

Services

Stage Class Route Routable
Speech-to-text OpenAISTTService /v1/audio/transcriptions 11 of 13
LLM OpenAILLMService /v1/chat/completions 13 of 15
Text-to-speech OpenAITTSService /v1/audio/speech 15 of 15

Setup

mkdir speko-pipecat && cd speko-pipecat
uv init --bare --python 3.11
uv add "pipecat-ai[openai,silero,webrtc,runner]~=1.6.0" "python-dotenv>=1.0,<2"
export SPEKO_API_KEY=sk_live_...

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}]}

Bot

"""Minimal Pipecat WebRTC bot using Speko's OpenAI-compatible API."""

from __future__ import annotations

import os

from dotenv import load_dotenv
from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import LLMRunFrame
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.worker import PipelineParams, PipelineWorker
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import (
    LLMContextAggregatorPair,
    LLMUserAggregatorParams,
)
from pipecat.runner.types import RunnerArguments
from pipecat.runner.utils import create_transport
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.services.openai.stt import OpenAISTTService
from pipecat.services.openai.tts import OpenAITTSService
from pipecat.transcriptions.language import Language
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.workers.runner import WorkerRunner


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")


transport_params = {
    "webrtc": lambda: TransportParams(
        audio_in_enabled=True,
        audio_out_enabled=True,
    ),
}


def language_from_env() -> Language | str:
    """Use the enum for common languages and preserve custom BCP-47 tags."""

    try:
        return Language(SPEKO_LANGUAGE)
    except ValueError:
        return SPEKO_LANGUAGE


async def run_bot(
    transport: BaseTransport,
    runner_args: RunnerArguments,
) -> None:
    stt = OpenAISTTService(
        api_key=SPEKO_API_KEY,
        base_url=SPEKO_BASE_URL,
        settings=OpenAISTTService.Settings(
            model="auto",
            language=language_from_env(),
        ),
    )
    llm = OpenAILLMService(
        api_key=SPEKO_API_KEY,
        base_url=SPEKO_BASE_URL,
        default_headers={"X-Speko-Objective": SPEKO_OBJECTIVE},
        settings=OpenAILLMService.Settings(
            model="auto",
            system_instruction="You are a helpful assistant in a voice conversation.",
        ),
    )
    tts = OpenAITTSService(
        api_key=SPEKO_API_KEY,
        base_url=SPEKO_BASE_URL,
        # Speko returns 24 kHz mono PCM for routed TTS.
        sample_rate=24000,
        settings=OpenAITTSService.Settings(
            model="auto",
            voice="alloy",
        ),
    )

    context = LLMContext()
    user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
        context,
        user_params=LLMUserAggregatorParams(
            vad_analyzer=SileroVADAnalyzer(),
        ),
    )

    pipeline = Pipeline(
        [
            transport.input(),
            stt,
            user_aggregator,
            llm,
            tts,
            transport.output(),
            assistant_aggregator,
        ]
    )
    worker = PipelineWorker(
        pipeline,
        params=PipelineParams(enable_metrics=True),
        idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
    )

    @transport.event_handler("on_client_connected")
    async def on_client_connected(transport: BaseTransport, client: object) -> None:
        del transport, client
        context.add_message(
            {
                "role": "developer",
                "content": "Introduce yourself in one short sentence.",
            }
        )
        await worker.queue_frames([LLMRunFrame()])

    @transport.event_handler("on_client_disconnected")
    async def on_client_disconnected(transport: BaseTransport, client: object) -> None:
        del transport, client
        await worker.cancel()

    runner = WorkerRunner(handle_sigint=runner_args.handle_sigint)
    await runner.add_workers(worker)
    await runner.run()


async def bot(runner_args: RunnerArguments) -> None:
    transport = await create_transport(runner_args, transport_params)
    await run_bot(transport, runner_args)


if __name__ == "__main__":
    from pipecat.runner.run import main

    main()

Run

uv run python bot.py -t webrtc
# Then open http://localhost:7860, click Connect, and start talking.

Override per request

import os

from pipecat.services.openai.llm import OpenAILLMService

llm = OpenAILLMService(
    api_key=os.environ["SPEKO_API_KEY"],
    base_url="https://api.speko.ai/v1",
    default_headers={"X-Speko-Objective": "latency"},
    settings=OpenAILLMService.Settings(model="auto"),
)

Pin one provider

import os

from pipecat.services.openai.tts import OpenAITTSService

tts = OpenAITTSService(
    api_key=os.environ["SPEKO_API_KEY"],
    base_url="https://api.speko.ai/v1",
    sample_rate=24000,
    # The id pins the provider. The voice stays a preset name, and the router
    # swaps in that provider's own default voice.
    settings=OpenAITTSService.Settings(model="cartesia:sonic-3.5", voice="alloy"),
)

Limits

A voice outside the 13 OpenAI presets The service yields an ErrorFrame before any request. Pin the provider in model instead.
No SileroVADAnalyzer in the aggregator This speech-to-text is segmented and produces nothing without one.
default_headers on the STT or TTS service Not carried: both build their own client. Those two stages read the key.
PipelineTask, PipelineRunner Deprecated since Pipecat 1.3 and removed in 2.0. The file above uses PipelineWorker.

Reference

Quickstart Routing headers, response headers, errors
LiveKit The same wiring in an AgentSession
Models The measured rows the ranking reads
Pipecat Framework documentation
One router for speech-to-text, LLM and text-to-speech.

Speko