Router Intro

Routers are important when working with AI. You can use our routers when setting up an agent. You can also use them independently of the agent, which is what these docs show.

We recommend using a router and naming it after the use case in your app. Names like healthcare_transcriptions, ivy_voice_ai or other project specific names are good. There are a few reasons why you want routers with use case based names like this:

  • The cost and performance of models change rapidly. In the past at Stream we've spent tens of thousands of additional dollars just because updating to a new model took time.
  • AI providers don't have perfect uptime. They are all dealing with rapid growth and limited GPU availability, so when a vendor goes down you want a fallback.
  • The router supports model specific overwrites, so you don't lose much flexibility.

Note that your named router can also point to any of our predefined routes, such as llm-fast or en-low-latency.

Router example

One YAML file sets up transcription, voice and language for a dental office's voice agent:

routers/dentist_ai.yaml
name: dentist_ai

stt:
  # Deepgram Flux first, then whatever is healthiest in the English low latency group.
  providers: [deepgram/flux-general-en, en-low-latency]
  keyterms: [crown, root canal, periodontal, Invisalign]
  # Patient audio only goes to providers that neither train on it nor keep it.
  data_policy:
    allow_training: false
    retention: none

tts:
  target: en-low-latency
  voice: custom:front_desk

llm:
  providers: [openai/gpt-5-mini, llm-fast]
  reasoning_effort: low

Sync it. The sync command picks up the YAML file in the routers folder, and configs are written by name, so running it twice edits rather than duplicates:

curl -fsSL https://getstream.io/cli/beta/install.sh | bash # the agents commands are in the beta CLI
getstream agents sync --router=dentist_ai

Then open it by name. Each modality uses the defaults from the file:

from vision_agents.plugins import stream

router = stream.Client().router("dentist_ai")

async with router.stt.realtime() as live:
    ...

When a new model is better or cheaper, change this file and sync again. Every call that uses dentist_ai follows, without a code change.

Predefined routes

Name any of these as a target, or inside a providers list. A route tries its preferred model first while it is healthy, and the rest of the group catches it when it is not.

Stream's picks, one set per modality:

Route Modality What it is for
stt-fast STT A live call. Deepgram Flux first, then Cartesia Ink 2, Inworld and Nemotron.
stt-accurate STT Hearing every word. Muse first, which also labels speakers, then ElevenLabs Scribe, Ink 2 and Nemotron.
tts-fast TTS The quickest live voices. Cartesia Sonic 3.6 first, then the rest of the fast tier.
tts-quality TTS The live voices listeners rate best. ElevenLabs v4 first, then Cartesia Sonic 3.6, Inworld, Breeze and ElevenLabs v4 Turbo.
llm-conversational LLM Simple, short turns. Gemma 4 26B-A4B first, then Gemma 4 31B on Cerebras and GPT-5.6 Luna.
llm-fast LLM The quickest strong answer in any language. Gemini 3.8 Flash first, then the rest of the fast tier.
llm-smart LLM Hard questions an agent must get right, at the cost of speed. GPT-5.6 Sol first.
llm-thinking LLM Skills: the part of a turn the talking model hands off. GPT-6 Sol first.
vlm LLM Questions about images. GPT-6 Luna first.
sts-fast STS The quickest speech-to-speech conversation. Gemini 3.8 Live first.
sts-vision STS A speech-to-speech conversation the model can watch as well as hear. GPT-Realtime-2 first.

Groups by language and speed, with the same names in every modality:

Route Languages What it is for
en-low-latency English Live, quickest to the first word.
multilingual-low-latency Any Live, quickest to the first word.
en-high-accuracy English The best quality, even if it is slower.
multilingual-high-accuracy Any The best quality, even if it is slower.
en-recorded English Whole recordings rather than a live stream. STT and TTS only.
multilingual-recorded Any Whole recordings rather than a live stream. STT and TTS only.

GET /v1/{modality}/routes lists the routes a deployment offers, and GET /v1/{modality}/routes/{target} shows which models a route resolves to right now.

Data policy

Every provider in the catalogue declares what happens to what it is sent: trains_on_data (yes, no or unknown) and retention (none, a duration such as 30d, unspecified or unknown). A config can require a policy:

stt:
  data_policy:
    allow_training: false # only providers that said they do not train on it
    retention: none # only providers that keep nothing

A request is then only routed to providers that meet it. unknown and unspecified meet nothing, because a vendor who has said nothing is not a vendor who has said no. If no provider qualifies, the request is refused rather than sent somewhere that does not qualify.

Daily limits

An app caps what one end user may spend in a UTC day with quota in its policy. Send it with a server-side token:

PUT /v1/policies/app
{
  "quota": {
    "messages_per_user_per_day": 500,
    "tokens_per_user_per_day": 10000000
  }
}
  • quota is for an app. An organization policy that sets it is refused with a 400.
  • 0 is no cap. A number you leave out takes the deployment's default.
  • The PUT replaces the whole policy, so send the rest of the policy too. A PUT without quota clears it, and the deployment's default applies again.

An end user with nothing left gets a 429 rate_limited with a Retry-After header: the seconds until the limit resets. On a socket that is already open, the refusal is an error frame.

An HTTP response to an end user's request carries three headers while a limit applies. A turn on an open socket or call carries none.

Header Value
X-Quota-Limit The limit nearest to being spent, in messages or tokens
X-Quota-Remaining What is left of that limit
X-Quota-Reset The unix time of the next UTC midnight

The router counts each user id and each address against the limit. The one nearest to being spent decides. End users behind one proxy share an address.

The router counts an end user's own turns, also in a session a backend opened for them. It never counts a backend's own turns, or answers from mock/* models.

Learn more