Continuations
By default, every WebSocket request is its own independent generation — send text + voice_id, get back audio, done. That’s fine for one-shot utterances, but text arriving incrementally (LLM token streams, live-typed captions) usually needs to land as one utterance, not a string of separately-generated fragments with a prosody reset between each.
Send the same context_id on a sequence of fragments to have them buffered, joined at natural sentence boundaries, and spoken as one continuous generation — each new fragment in the context is primed with the audio from the one before it, so pacing and intonation carry across chunk boundaries.
Endpoint support
Continuations are WebSocket-only:
When to use it
- Text is arriving incrementally from an LLM and you want to start speaking before the model has finished generating the full response, without a flat, reset-per-chunk cadence.
- You’re already using
continue+flush(the legacy buffer) for streamed input but want the server — not a fixed timer — to decide where sentences end. - You want fragments billed and rate-limited as one concurrent call, not one per chunk.
If you have the complete text up front, skip this — a single request already produces one continuous generation.
Parameters
context_id cannot be combined with flush or max_buffer_flush_ms — those belong to the older, timer-based buffering contract. Mixing them is a validation error. Close out a continuation with continue: false or context_close: true instead.
How buffering works
A fragment sent with context_id is released to the synthesizer as soon as any one of these is true:
- It arrives with
continue: false— no more input is coming. - The buffered text ends a sentence (terminal punctuation
. ? ! …, guarding against decimals, thousands separators, and abbreviations like “Mr.”) and is long enough that speaking it doesn’t sound abruptly clipped. max_buffer_delay_mselapses since the first still-buffered fragment — the clock doesn’t restart as more fragments arrive.- The buffer grows past the max chunk size — it’s split at the last sentence boundary or last space within that window.
Ending a continuation
Three ways to close a context, depending on why you’re closing it:
- Natural end of input — send
continue: falseon the last fragment. It may carry notextat all:{"context_id": "call-1", "voice_id": "meher", "continue": false}. - Immediate teardown — send
context_close: true. Use this when you know no more text is coming and want the server to drop the context’s carried state right away rather than at its idle timeout. May omittext/voice_id. - Barge-in —
cancel_request: truediscards whatever is currently buffered for the context without speaking it, but does not end the context itself; you can keep streaming into the samecontext_idafterward.
Closing the WebSocket connection also ends every open context on it (nothing buffered is flushed — there’s nothing left to bill or play).
If a context has nothing buffered when you send its closing frame (continue: false with no text, or context_close: true) — for example, everything already released earlier via a sentence boundary or max_buffer_delay_ms — the server sends back no frame at all for that message. Don’t block waiting on a response to the closing frame itself.
Response frames during a continuation
Verified against a live connection: status: "complete" behaves differently here than it does for a one-shot request.
- One
chunk/completepair per released segment, not one per context. Every time buffered text is released — on a sentence boundary, onmax_buffer_delay_ms, or on the frame that closes the context — you get its own run ofchunkframes followed by acomplete. A context fed in one long burst typically collapses to a single release (and a singlecomplete), but a context spread across multiple flushes emits multiplecompleteframes while the context is still open. completedoes not close the WebSocket while the context is open. Unlike a plain non-continuation request (see Response Format, wherecompleteis terminal and the server closes the connection), a mid-contextcompletejust marks that one release as done — the connection stays open and you can send more fragments on the samecontext_idafterward.session_idis stable for the whole connection;request_idchanges per released segment. Usesession_idif you need to correlate frames back to the connection; don’t assume onerequest_idspans an entire context.- No
context_idis echoed back on any response frame. If you multiplex more than onecontext_idon a single connection, track which context a frame belongs to by send order — don’t rely on the response to disambiguate. - No frame marks “the context is fully done.” A
completeafter a sentence-boundary ormax_buffer_delay_msrelease looks identical to one after your closing frame (continue: false/context_close: true). Don’t return on the firstcompleteyou see — drain frames until the connection goes idle (or you close it yourself once you’ve accounted for every fragment you sent).
Concurrency and billing
Fragments sharing one context_id on the same connection occupy a single concurrency slot, not one per fragment — chunking your input more finely doesn’t cost you additional concurrent-call capacity. Billing still applies per generated segment through the normal path.
Example
The three fragments above share context_id: "call-1", so they’re buffered and joined instead of spoken as three separately-paced generations — in practice this reliably collapses to a single release when fragments arrive close together, but the receive loop doesn’t assume that.
Related
- Streaming — WebSocket vs SSE, response frame shapes, and the legacy
continue+flushbuffer this feature supersedes for incremental input. - Word-level timestamps — another WebSocket-only opt-in feature, combinable with continuations.

