Alert when a new TTS model id lands in the Sume router catalog

September and October brought new voice models. A 20-line script diffs GET /v1/tts-router/models against yesterday's ids and tells you when a row is added.

5 min readSume
All posts

Read the Sume router catalog on a schedule and compare it with the ids you saw last time. A new row is the signal that a model you asked about is now routable. The catalog is GET /v1/tts-router/models, and it returns data.models, each with an id, capabilities.max_characters and list pricing. Today the ids are sonic-3.6, sonic-3.5, sonic-3, sonic-latest and sonic-preview; the docs say later vendors arrive as new rows, not as a new surface.

Why a diff beats checking by hand

The past few weeks brought new speech models from several vendors: Microsoft published MAI-Voice-2.1 and a Flash variant, and Google moved Gemini 3.8 Flash TTS to general availability on Sep 22, 2026. None of those is in the Sume router today, and the docs list non-Sonic families as later rows. A daily diff means you learn about a new id from the catalog itself instead of from a forum post, and it works for ids you have not heard of.

Router catalog today and launches that are not in it, Sume docs, Microsoft and Google pages, read 2026-10-05
ItemIn the Sume router catalog?Source
sonic-3.6, sonic-3.5, sonic-3YesSume router docs
sonic-latest (alias for sonic-3.6)YesSume router docs
sonic-preview (beta)YesSume router docs
MAI-Voice-2.1 and FlashNot listedRouter docs list Sonic only
Gemini 3.8 Flash TTSNot listedRouter docs list Sonic only

The script

It stores the id list in a JSON file, fetches the catalog, prints any additions or removals and saves the new list. Run it from cron or a scheduled agent. It exits with code 1 when something changed so a scheduler can alert on the exit status.

import json, os, pathlib, requests

STATE = pathlib.Path("tts_router_ids.json")
r = requests.get("https://api.sume.com/v1/tts-router/models",
                 headers={"x-api-key": os.environ["SUME_API_KEY"]}, timeout=30)
r.raise_for_status()
rows = {m["id"]: m for m in r.json()["data"]["models"]}
old = set(json.loads(STATE.read_text())) if STATE.exists() else set()
new = set(rows)
for i in sorted(new - old):
    print("ADDED", i, rows[i]["capabilities"]["max_characters"], "chars max")
for i in sorted(old - new):
    print("REMOVED", i)
STATE.write_text(json.dumps(sorted(new)))
raise SystemExit(1 if (new != old and old) else 0)

What to do with an ADDED line

First fetch the single row with GET /v1/tts-router/models/{model_id} and read its constraints. Then run the 1-cent language and number test from your own script before you move any production job to it. A new id might be a beta channel, like sonic-preview, which the docs say can change without notice and rejects pro voice clones with voice_model_mismatch.

A REMOVED line is more urgent than an added one. Search your own code for the id and move those jobs to a named replacement before the next batch.

Where to run it

A cron entry once a day is plenty, since catalog changes are rare. Send the exit status to your alerting channel. Keep the JSON file under version control if you want an audit trail of when each id first appeared. The script uses only the standard library plus requests, and it needs the same API key you already use for TTS jobs, passed in the x-api-key header; never send both that header and a Bearer token on one request.

Edge cases the script handles

On the first run there is no state file, so the script prints every id as ADDED and exits 0, because it treats an empty baseline as a setup run, not an alert. It also compares sets, so a reordered catalog does not trigger anything. If the request fails, raise_for_status stops the run before the state file is touched, so a network blip never looks like a removal.

Add one more guard if you run it unattended: check that the response has at least one model before saving, so an empty body from a proxy cannot wipe your baseline.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume