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.
| Factory | Options | Key (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, body | AGENT_LLM_API_KEY, else OPENAI_API_KEY, else none |
kugelaudio.stt(...) | language, turnDetection, turnDetectionLevel, boostedPhrases, dictionaryIds, projectId | KUGELAUDIO_API_KEY |
azure.stt(...) | language, region, segmentationMs | AZURE_SPEECH_KEY (AZURE_SPEECH_REGION default region, else eastus) |
deepgram.stt(...) | model, language | DEEPGRAM_API_KEY |
elevenlabs.stt(...) / elevenlabs.tts(...) | model / voice, model | ELEVEN_API_KEY |
openai.stt(...) / openai.tts(...) | model, language / voice, model | OPENAI_API_KEY |
kugelaudio.tts(...) | voice, model, language, dictionaryIds, projectId | KUGELAUDIO_API_KEY |
cartesia.tts(...) | voice, model, language | CARTESIA_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:
| Variable | Default (ms) | What it does |
|---|---|---|
VOICE_ENDPOINTING_MIN_MS | 300 | The shortest pause that can end a turn |
VOICE_ENDPOINTING_MAX_MS | 3000 | The longest pause before a turn ends |
VOICE_SILENCE_FOLLOWUP_MS | 6000 | How long the agent waits in silence before it checks in |
Next steps
- Testing: check the agent from the terminal.
- Mid-call changes: switch voice, language or model during a call.
- Pronunciation: teach it your product’s names.