Voice

Voice

Speech to text, the LLM and text to speech are props on <Agent>. Curb runs the call around them: turn-taking, interruptions, and speaking the reply sentence by sentence while the model writes. A new project runs all three on KugelAudio, so you can start talking with no setup.

The providers on this page, the other settings and their keys are listed in Providers and Configuration.

The LLM and the speech providers are props on <Agent>, built with the factories in curb/providers. Each returns a plain descriptor; the server resolves it and reads the key from the environment, so nothing secret is in the tree.

FactoryOptionsKey (env)
kugelaudio.llm(...)model: "qwen3.8-27b" (default) or "mistral-medium-3.5-128b"KUGELAUDIO_API_KEY
anthropic(model)ANTHROPIC_API_KEY
openai(model)OPENAI_API_KEY
openaiCompatible({ baseURL, model, ... })reasoningEffort, temperature, topP, bodyAGENT_LLM_API_KEY, else OPENAI_API_KEY, else none
kugelaudio.stt(...)language, turnDetection, turnDetectionLevel, boostedPhrases, dictionaryIds, projectIdKUGELAUDIO_API_KEY
azure.stt(...)language, region, segmentationMsAZURE_SPEECH_KEY (AZURE_SPEECH_REGION default region, else eastus)
deepgram.stt(...)model, languageDEEPGRAM_API_KEY
elevenlabs.stt(...) / elevenlabs.tts(...)model / voice, modelELEVEN_API_KEY
openai.stt(...) / openai.tts(...)model, language / voice, modelOPENAI_API_KEY
kugelaudio.tts(...)voice, model, language, dictionaryIds, projectIdKUGELAUDIO_API_KEY
cartesia.tts(...)voice, model, languageCARTESIA_API_KEY

Without a prop the defaults apply: kugelaudio.llm(), azure.stt() and kugelaudio.tts() (English). A kugelaudio.tts() without voice speaks the default voice of its language: 1072 for English, 1887 for German, and the English 1072 for any other language, so give such an agent a voice (defaultKugelVoice(language) from "curb/providers" returns the default). A chosen provider whose key is missing fails with an error naming the variable.

kugelaudio.llm() is KugelAudio’s LLM, hosted in the EU, on the same KUGELAUDIO_API_KEY as its speech, so an agent on KugelAudio needs no other key; a hosted agent gets that key from KugelAudio. Thinking is off on KugelAudio’s side, so the agent never speaks its reasoning, and nothing beyond the conversation and the tools is sent. It is served at KUGELAUDIO_API_BASE (default https://api.eu.kugelaudio.com) plus /v1. An organization gets it with hosted agents, in closed alpha; until then its turns answer 403 LLM_NOT_ENABLED.

To run your own model, see Your own LLM.

Speech to text

kugelaudio.stt() is Kugel Ear 1, KugelAudio’s own speech-to-text, on the same KUGELAUDIO_API_KEY as kugelaudio.tts(), so an agent on KugelAudio speech needs no second vendor key. Set language to what your callers speak (“de”, or “de,en” to detect among several, expecting the first): without it the service detects German or English, and a short German “Äh” or “Nein” can come out as English. KugelTurn, the end-of-turn model inside the service, decides when the caller has finished (turnDetectionLevel: "conservative" holds longer through pauses mid-thought). Names and product terms the recognizer should spell your way go in boostedPhrases:

<Agent
  stt={kugelaudio.stt({ language: "de", boostedPhrases: ["Curb", "Kugel Ear"] })}
  tts={kugelaudio.tts({ voice: 1887, language: "de" })}
>

Each caller turn becomes one transcript: the service’s more accurate second pass over the turn when it arrives within two seconds, else its first transcript. Supported languages: de, en, fr, it, es, pt, nl, pl, ro; any other code fails when the agent starts.

A different voice per skill

A <Skill> link can carry the same props. While that skill is the active one (the most recently read), its props override the agent’s, slot by slot:

import { Skill, Skills } from "curb";
import { kugelaudio } from "curb/providers";

export const skills = (
  <Skills>
    <Skill path="/billing" description="Invoices and payments.">Billing</Skill>
    <Skill path="/claims" description="Filing a claim." tts={kugelaudio.tts({ voice: 17 })}>Claims</Skill>
  </Skills>
);

Put such props on the link in the always-rendered <Skills> index, so they hold however the skill was reached. Props are reactive: render another value (from state, useAgentInfo, a skill change) and a running call switches to it. A speech provider is swapped from the agent’s next reply on (the reply being spoken finishes in the old voice); the LLM applies from the next turn. A re-render with an equal value changes nothing.

Turn-taking

Curb decides when the caller has finished. Three settings in .env change how it listens:

VariableDefault (ms)What it does
VOICE_ENDPOINTING_MIN_MS300The shortest pause that can end a turn
VOICE_ENDPOINTING_MAX_MS3000The longest pause before a turn ends
VOICE_SILENCE_FOLLOWUP_MS6000How long the agent waits in silence before it checks in

Next steps