The first thing that breaks a text-to-speech integration usually isn’t the model. It’s the file. OpenRouter’s tutorial on its speech endpoint, published September 11, 2026, opens with a detail worth internalizing: a successful request returns raw audio bytes, while a non-successful request returns JSON. If your code writes the response body straight to output.mp3, a 400 error becomes a corrupt audio file that fails somewhere else, later, with a worse error message.
The endpoint itself is deliberately boring. You POST to https://openrouter.ai/api/v1/audio/speech with an OpenRouter API key in the Authorization: Bearer header, and the request body carries model, input, and voice. The tutorial notes that voice is technically optional only when a provider documents a default, so treat it as required. response_format and speed are optional, but the endpoint defaults to PCM when you omit the format, and format support varies by model.
The three checks that keep bad responses out of your audio pipeline
The tutorial’s Python example does three things before it touches the filesystem, and they’re the pattern worth copying:
raise_for_status()so a 4xx or 5xx raises instead of silently returning an error body.- A
Content-Typecheck againstaudio/mpegbefore writing bytes. - Recording the
X-Generation-Idresponse header alongside the saved file.
The JavaScript example applies the same logic with response.ok, a content-type comparison, and response.body?.cancel() when the type is wrong. The cURL version uses --fail-with-body plus --dump-header, and the tutorial warns that because --output is set, the server’s JSON error body lands in output.mp3 — so on failure you should read it with cat output.mp3 and delete the file before retrying.
That last point is the whole lesson. The transport is easy; the failure mode is a file that looks like audio.
Streaming, and why the OpenAI SDK still works
OpenRouter’s endpoint follows the OpenAI Audio Speech API shape, so you can point the OpenAI Python client at https://openrouter.ai/api/v1 and use client.audio.speech.with_streaming_response.create(...) with response.stream_to_file(Path("output.mp3")). The tutorial notes this reads the response incrementally while saving, and that progressive playback needs a player that buffers incoming chunks.
If you already run agent tooling against OpenRouter, this is the same base-URL swap pattern covered in what a hosted sandboxed shell changes for how you build agentic tools — one key, one base URL, provider-specific behavior behind the same request shape.
Model and voice are a pair, not two independent knobs
Voice identifiers belong to specific models. The tutorial’s example starts with mistralai/voxtral-mini-tts-2603 and en_paul_neutral, then switches to x-ai/grok-voice-tts-1.0 with eve. The instruction is explicit: when you change providers, update the model and voice together, because each provider exposes its own IDs. Within a single model, changing the voice is a one-line edit.
Provider-specific options live under provider.options.<provider>. Microsoft MAI-Voice-2 accepts Azure voice names, a documented speed range of 0.5 to 2.0, and Azure style fields like style and styledegree. The tutorial cautions that unsupported providers may ignore speed, and that styles depend on the selected voice. It also states that no OpenAI speech model was in the live catalog as of September 2026, so the current model list is the thing to check before relying on a provider-specific field.
What production actually adds
For long text, the tutorial recommends splitting at sentence or paragraph boundaries, requesting segments in order, and combining the audio with format-aware tooling — which also returns the first segment sooner. Then it draws a retry line that matters for cost and correctness: retry 429, 502, 503, 524, and 529, honor Retry-After when present, otherwise use capped exponential backoff with a small attempt limit. Don’t retry 400, 401, or 402 until you fix the request, credentials, or credits.
Pricing is per character of input text and varies by model and provider, so check the current model page or Models API before estimating cost. The supplied tutorial text cuts off mid-sentence while describing remaining failures, so the full closing section isn’t available here.
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.
Sources
AI-assisted summary compiled from the sources above, reviewed by a human before publishing.
