Streaming TTS works without a provider pin, and the WebSocket text lifecycle has one owner

Unpinned streaming TTS requests no longer return 503, the documented WebSocket frame sequence now produces audio on every streaming provider, bare provider names work in X-Speko-Allow, and interimResults finally controls whether partial transcripts are sent.

POST /v1/audio/speech/stream and POST /v1/synthesize/stream previously returned 503 no_streaming_tts_provider for any request that set no X-Speko-Allow header, whenever the API key's routing chain pinned a provider that cannot stream. The chain now orders selection only through candidates that can stream, and falls back to the ranked board when none of them can.

On the /v1/synthesize/stream WebSocket, the documented sequence of a config frame carrying text followed by {"type":"end"} produced no audio. The vendor handshake and the caller's end frame each closed the text stream, so the second close was rejected. A single owner now tracks the text lifecycle, {"type":"end"} is idempotent, and text pushed before a provider reports readiness is buffered instead of dropped or repeated.

Incremental text on a provider that defers until readiness previously spoke the utterance twice. It is now sent exactly once.

{"type":"end"} is required to finish an utterance on the WebSocket transport. A caller that opened with all of its text in the config frame and never sent an end frame was previously completed by the handshake on one provider; that accident is gone, and sending the documented end frame is safe to do unconditionally.

X-Speko-Allow accepts a bare provider name as documented. Previously only the display name matched, so a value such as smallest returned 503 no_candidate while smallest:lightning_v3.1 routed.

The streaming transcription config frame accepts interim_results as an alias for interimResults, and the flag is now enforced by the router: interimResults false returns final transcripts only, regardless of what the upstream provider chooses to send. Ready and error frames are never withheld.

GET /v1/routing/preview returns the standard JSON error envelope with code invalid_query when the query string is missing stage or carries an invalid value, instead of a text/plain body.

The OpenAPI description for GET /v1/transcribe/stream documented a first frame the handler rejects. It now records the required type field, the 16 kHz mono 16-bit signed little-endian PCM input format, the end frame, and the transcript frame shape.

Action: No migration is required for the chunked-HTTP transports. Callers of the /v1/synthesize/stream WebSocket must send {"type":"end"} to finish an utterance; it is idempotent, so sending it unconditionally is safe. Callers that filtered partial transcripts client-side can now set interimResults false instead.