feat(sarvam): add bulbul:v4-flash TTS - #7335
dhruvladia-sarvam wants to merge 7 commits into
Conversation
The API rejects `bulbul:v4` and only accepts `bulbul:v4-flash`. Pin that wire name, use the v4-flash speaker catalogue and /ws/v2, and forward Sarvam's websocket status codes so 4xx failures are not retried. Co-authored-by: Cursor <cursoragent@cursor.com>
…ockets update_options wrote the new model before validating the retained speaker, so a rejected switch left an unusable model/speaker pair, and stored pitch, pace, loudness and temperature were never re-checked against the new model's tighter v4-flash bounds. Validate a candidate copy and commit it only once every model-dependent limit passes. model and send_completion_event are pinned in the handshake URL, so invalidate the pool when either changes rather than letting a socket keep the old endpoint. Tag the config debug log for PII redaction, and drop two tests that asserted retry behaviour this branch does not implement. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Pooled sockets keep old endpoint / Model switches retain invalid settings / Rejected updates corrupt model state — all three shared one root cause in Synthesis config bypasses PII redaction — the debug log extra is now WebSocket errors expose customer content — not changing this one. Putting the raw provider frame in the message is pre-existing behaviour on both this websocket path and the REST path above it, and surfacing Sarvam's verbatim error is the point of the change: it is what lets a caller correlate a failure with Sarvam's server-side logs via Also dropped two tests that asserted |
The websocket error log put Sarvam's message in the log body and in an untagged error_message extra, so a collector could not redact provider text that may echo customer content. Use a static body and tag the field lk.pii.error_message, matching the raw frame already tagged beside it. Co-authored-by: Cursor <cursoragent@cursor.com>
The pool handshakes sockets from the TTS's current options while each stream carries its own snapshot, so a stream constructed before update_options sent a config frame for the previous model over a socket opened for the new one, and v4-flash is served on a different path. Record the options behind each socket and have the stream take the model-coupled set from them. Language, sample rate and codec ride in the config frame and the emitter is already initialized from them, so the stream keeps its own. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Queued streams mix model versions — correct, and the root cause was that the handshake and the config frame read different sources: the pool builds sockets in Worth noting this one predates the pool invalidation: Provider errors bypass log redaction — fixed. That log put Sarvam's message in the body via an f-string and in an untagged |
_adopt_handshake_opts copied the socket's whole option snapshot, so reusing a still-valid pooled socket reverted a config-only update_options: speaker, pace and the other tuning fields are sent per segment rather than pinned in the handshake. Only model is carried by both, so align that and leave the rest to the stream's snapshot. Co-authored-by: Cursor <cursoragent@cursor.com>
|
I also confirmed |
APIStatusError.__str__ renders both message and body, and the framework logs that with %s before each retry, so a Sarvam error frame placed in either field reached the log where no collector can redact it. Carry only the status code and request_id on the exception; the frame is already recorded in full on the lk.pii.* tagged log record beside it. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Nothing provider-written goes on the exception now. It carries Worth flagging for maintainers separately: |
Summary
bulbul:v4-flashto the Sarvam TTS plugin with its own speaker catalogue, parameter bounds, IN22 language codes, and/text-to-speech/ws/v2pinning. The API rejectsbulbul:v4; that string is not accepted as an alias. Default model staysbulbul:v3.code(or theNNN:message prefix) intoAPIStatusErrorso 4xx failures are not retried, and keep the raw Sarvam frame in the message/body plusrequest_id.Test plan
uv run pytest tests/test_plugin_sarvam_tts.py --unit(13 new/updated v4-flash and error-mapping tests pass; 2 pre-existingmax_retry==0assertions still fail and are unrelated)ruff check/ruff format/mypyclean on the pluginstatus_code=422, retryable=Falsewith request idbulbul:v4-flashonce beta access is restored on the keyMade with Cursor