Repository navigation
feat(sarvam): add realtime speech to text on /v1/realtime - #42479
pythongiant wants to merge 3 commits into
Conversation
Sarvam's realtime STT WebSocket speaks its own protocol, so a BaseRealtimeConfig translates it to and from OpenAI realtime transcription events: audio_input, config.update and the manual endpointing controls on the way up, session.begin, VAD events, partial and final transcripts on the way down. Sarvam fixes encoding and sample_rate when the socket opens, so the rate is read from the deployment api_base and a client session that declares a different one is rejected rather than transcribed as garbage. language_code is not connection bound: the socket opens on Sarvam's adaptive `auto` and the client's language is applied with a config.update, which Sarvam honors at the next utterance boundary. Billing uses the audio_duration_s Sarvam reports at session.end, and falls back to the audio the proxy forwarded when a client drops the socket before that arrives. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sarvam prices speech to text in INR per hour and this registry is USD only, so the entries carried a converted rate plus the FX rate and the date it was taken. That ages badly and reads like noise next to every other entry. The entries now describe the models without a rate. Sessions still log the audio duration Sarvam bills, so an operator can apply their own conversion, or register a per-second rate to have LiteLLM cost it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
Model support now reads the model registry instead of a list in this file, so a new Sarvam realtime model ships as a cost map entry with mode audio_transcription on /v1/realtime. Sarvam opens the socket on its own defaults and acknowledges each config.update separately, so its config.updated becomes an OpenAI session.updated. Without it a client that configures the session after session.created never learns which language and turn detection took effect. A session.update that leaves turn_detection out is a partial update and keeps the endpointing the session already runs on. Reverting to VAD there silently turned a manual client's audio buffer commits into no-ops. The subscription key travels as a connection header, so cleartext is refused unless the host is a loopback address, and an IPv6 host keeps the brackets the URL needs to stay parseable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All five review findings fixed in 9334169: registry-driven model support, session.updated on config.updated, endpointing preserved across partial updates, loopback-only cleartext, IPv6 brackets. CodeQL fall-through returns made explicit. |
|
|
||
| from pydantic import JsonValue | ||
|
|
||
| import litellm |
TLDR
Problem this solves:
How it solves it:
model_listdeployment on/v1/realtimeUser Flow
Before: a developer pointing an OpenAI realtime client at a Sarvam speech to text deployment gets the socket closed before any audio is transcribed
sarvam-sttdeployment forsarvam/saaras:v3-realtimeand restarts the proxyUnsupported model: saaras:v3-realtimeAfter: the same client streams audio and receives transcripts, and the session is billed
sarvam-sttdeployment and restarts the proxysession.updatenaming the transcription model and languagesession.createddescribing a transcription session at 16 kHz, thensession.updatedonce the language they asked for takes effectinput_audio_buffer.appendframes and seeinput_audio_buffer.speech_started, a run of transcriptiondeltaevents, theninput_audio_buffer.speech_stoppedconversation.item.input_audio_transcription.completedevent carries the full transcriptRelevant issues
Affected release
Linear ticket
Pre-Submission checklist
uv run pytest tests/test_litellm/<your_test_file>.py -vScreenshots / Proof of Fix
Shared setup, used for both runs.
sarvam_config.yaml:Proxy, in one shell:
realtime_client.py, a plain OpenAI realtime transcription client. WebSockets cannot be driven with curl, so this is the smallest client that sends the same frames an OpenAI SDK would. It streams the repo'stests/audio_tests/gettysburg.wavresampled to 16 kHz:Both runs hit the live Sarvam API with a real subscription key and bill real money.
Before (071cb49)
After (9334169)
session.updatecomes back assession.updated, audio streams, and the transcript arrives:Type
🆕 New Feature
Caveats (if any)
Sample rate is fixed when the socket opens, because Sarvam only accepts 16000 or 8000 and only as a connect time query parameter. The default is 16 kHz and a deployment can pick 8 kHz with
api_base: wss://api.sarvam.ai?sample_rate=8000. Cleartextws://andhttp://api_base values are refused unless they point at a loopback address, since the subscription key travels as a connection header. A client session that declares a different rate, including the 24 kHz that OpenAI realtime clients commonly default to, is refused rather than transcribed as noise. The proxy does not resample.Deltas are append only. Sarvam revises partial transcripts mid utterance, and OpenAI realtime has no way to retract a delta, so a revision that drops words emits nothing and the authoritative text arrives in the completed event. The transcript a client accumulates from deltas can therefore be shorter than the completed transcript.
Sessions log a duration, not a cost. Sarvam prices speech to text in INR per hour and this registry is USD only, so the entries carry no rate and sessions cost $0 out of the box. Each session logs the audio seconds Sarvam bills for, so an operator can apply their own conversion downstream, or register a per-second USD rate for that model to have LiteLLM cost it. Deployment level
input_cost_per_secondwill not do it: the realtime cost path resolves pricing by model name and never sees the router deployment id.