Summary: OpenRouter’s September 11, 2026 text-to-speech tutorial warns that a successful endpoint returns raw audio bytes while failures return JSON, so callers must check status and Content-Type before writing a file. The tutorial records X-Generation-Id, treats model and voice as a provider-bound pair, and advises cutting long input at sentence or paragraph boundaries. Retries apply to 429 and listed 5xx responses, while 400, 401, and 402 require fixing the request, credentials, or credits.
When a text-to-speech integration breaks, the model is rarely the culprit. The file is. OpenRouter’s tutorial on its speech endpoint, published September 11, 2026, leads with a detail worth committing to memory: a request that succeeds hands back raw audio bytes, while one that fails hands back JSON. Write the response body straight to output.mp3 and a 400 turns into a corrupt audio file that surfaces somewhere else, later, attached to a worse error message.
The endpoint itself is unremarkable by design. Send a POST to https://openrouter.ai/api/v1/audio/speech, put an OpenRouter API key in the Authorization: Bearer header, and fill the body with model, input, and voice. Per the tutorial, voice is optional only in the narrow case where a provider documents a default — treat it as required. response_format and speed may be left out, though omitting the format makes the endpoint fall back to PCM, and format support differs from one model to the next.
Three checks that keep bad responses out of your audio pipeline
Before the tutorial’s Python example writes anything to disk, it performs three steps, and that sequence is the part worth copying:
raise_for_status(), which turns a 4xx or 5xx into a raised exception rather than an error body that slips through silently.- A
Content-Typecomparison againstaudio/mpeg, made before any bytes get written. - A record of the
X-Generation-Idresponse header, kept alongside the saved file.
In JavaScript the same reasoning shows up as response.ok, a content-type comparison, and a call to response.body?.cancel() whenever the type comes back wrong. The cURL variant adds --fail-with-body along with --dump-header, plus a warning: because --output is in play, the server’s JSON error body is written into output.mp3. So on a failed run, read it with cat output.mp3 and delete the file before retrying.
That is the whole lesson in one line. Moving bytes around is the simple part; the failure mode to fear is a file that looks like audio.
Streaming, and why the OpenAI SDK still fits
Because the endpoint mirrors the shape of the OpenAI Audio Speech API, an OpenAI Python client can be aimed at https://openrouter.ai/api/v1, with calls to client.audio.speech.with_streaming_response.create(...) paired with response.stream_to_file(Path("output.mp3")). The tutorial points out that this reads the response piece by piece as it saves, and that playing audio progressively requires a player capable of buffering chunks as they arrive.
Anyone already running agent tooling against OpenRouter will recognize the pattern from what a hosted sandboxed shell changes for how you build agentic tools — one key, one base URL, and provider-specific behavior sitting behind an unchanged request shape.
Model and voice travel together, they are not separate knobs
A voice identifier means something only inside the model it belongs to. The tutorial’s example opens with mistralai/voxtral-mini-tts-2603 paired with en_paul_neutral, then moves to x-ai/grok-voice-tts-1.0 with eve. The instruction is explicit: when the provider changes, model and voice change in the same edit, because each provider publishes its own IDs. Stay inside a single model and swapping the voice costs one line.
Provider-specific options sit under provider.options.<provider>. Microsoft MAI-Voice-2 takes Azure voice names, a documented speed range of 0.5 to 2.0, and Azure style fields such as style and styledegree. The tutorial adds two cautions: providers that don’t support speed may ignore it, and which styles are available depends on the voice selected. It also states that as of September 2026 no OpenAI speech model sat in the live catalog, which makes the current model list the thing to check before leaning on a provider-specific field.
What production work adds on top
For long text, the tutorial recommends cutting at sentence or paragraph boundaries, requesting the segments in order, and reassembling the audio with format-aware tooling — an approach that also brings the first segment back sooner. It then draws a retry line that matters for both cost and correctness: retry 429, 502, 503, 524, and 529; honor Retry-After when it is present; otherwise use capped exponential backoff with a small attempt limit. Leave 400, 401, and 402 out of the loop until the request, the credentials, or the credits have been fixed.
Pricing runs per character of input text and varies by model and provider, so check the current model page or the Models API before estimating cost. The tutorial text supplied here cuts off mid-sentence while describing the remaining failures, so the full closing section isn’t available.
A reasonable next step: write the validation function first — status, content type, non-empty body, generation ID — and only then wire up synthesis. The audio is the easy part.
