feat(realtime): add Meta Muse Voice transcription - #39395
ryan-crabbe-berri merged 7 commits into
Conversation
Greptile SummaryAdds Meta Muse Voice realtime transcription through LiteLLM’s shared realtime transport.
Confidence Score: 5/5The PR appears safe to merge because the previously reported transport concern has been resolved and no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/llms/meta/realtime/transformation.py | Implements Muse protocol configuration, PCM streaming, event normalization, turn tracking, and duration usage accounting through BaseRealtimeConfig. |
| litellm/litellm_core_utils/realtime_streaming.py | Adds raw binary forwarding and prevents transcription sessions from sending conversational response.create events without bypassing transcript guardrails. |
| litellm/llms/base_llm/realtime/transformation.py | Broadens provider transformation contracts to support binary messages and optional send pacing. |
| litellm/utils.py | Registers Meta’s realtime provider configuration with the existing provider manager. |
| tests/test_litellm/llms/meta/realtime/test_meta_realtime_transformation.py | Covers Muse configuration parsing, binary audio handling, pacing, provider events, turns, and usage behavior. |
| tests/test_litellm/litellm_core_utils/test_realtime_streaming.py | Verifies shared binary forwarding, transcription behavior, guardrail execution, and usage flushing. |
Reviews (7): Last reviewed commit: "fix(realtime): run transcription guardra..." | Re-trigger Greptile
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: 2 · PR risk: 0/10 |
|
Companion documentation PR: BerriAI/litellm-docs#1138 It documents |
|
@greptileai Please re-review the latest commit. The oversized base64 allocation finding is fixed in 298a729; the direct connector rationale is documented in the inline reply. |
298a729 to
51105c0
Compare
|
@greptileai re review |
|
bugbot run |
Replace the hand-rolled Meta realtime handler with a MetaRealtimeConfig that plugs into the shared realtime handler and RealTimeStreaming relay. Clients keep speaking the OpenAI realtime wire: session.update, input_audio_buffer.append/commit and the OpenAI transcription events. Unsupported transcription settings are logged and dropped, matching the Gemini realtime precedent, and the Meta-specific session.mode, keywords, language_bias, DIARIZATION and speaker extensions are removed. Drop the MODEL_API_KEY env var in favor of the standard META_API_KEY, remove the private-logging flag so spend logs record the transcript the same way other realtime models do, and add per-second pricing for muse-voice-transcribe-1.0. The relay now sends raw bytes from transform_realtime_request straight to the backend after pace_backend_send, and transcription sessions never trigger response.create.
51105c0 to
17fde7a
Compare
|
bugbot run |
Turns no longer wait behind each other in a FIFO queue, so an empty server_vad turn (speechStart then speechEnd with no transcript) cannot stall every later turn, and a PUSH_TO_TALK speechComplete now closes its turn without waiting for a speechEnd that never arrives. Each turn keeps its own idempotent emit state, so late or duplicate speechEnd, speechComplete and transcript frames are no-ops, and finished turns are remembered in a bounded map instead of a separate tombstone deque. The session.created ack and the sanitized error frame are now typed as members of OpenAIRealtimeEvents, which removes the typing.cast calls that the strict ruff budget flagged.
|
@greptileai re review |
|
bugbot run |
A duplicate speechStart for a turn that already stopped used to make that closed turn active again, so the next turnless PUSH_TO_TALK transcript was routed to the finished item and dropped.
|
@greptileai re review |
|
bugbot run |
…eechEnd Muse partials carry no turnId and belong to the most recent speechStart, and the docs say the model may keep post processing a turn after speechEnd until speechComplete. Releasing the active turn on speechEnd made any partial arriving in that window raise and get dropped in ENDPOINTING mode. The turn now stays active until its speechComplete or final transcript.
|
@greptileai re review. Pushed 4647cd1: a Muse turn now stays active for turnless partials after speechEnd until speechComplete. |
|
bugbot run |
…sions The provider_config path skipped run_realtime_guardrails for transcription sessions to avoid sending response.create, which also dropped every realtime_input_transcription guardrail: no violation error reached the client and on_violation / end_session_after_n_fails never fired. Run the guardrail for every completed transcript and only suppress response.create when the session has no assistant turn.
|
@greptileai re review. Pushed 0e435e4: transcription sessions now run realtime_input_transcription guardrails and only skip response.create. |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 0e435e4. Configure here.
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the contribution and to Ryan for the review!
c134fb7
into
BerriAI:litellm_internal_staging
TLDR
Problem this solves:
How it solves it:
meta/muse-voice-transcribe-1.0realtime routingUser Flow
Before: a developer cannot use Muse Voice through LiteLLM's realtime endpoint
meta/muse-voice-transcribe-1.0to the model list withapi_key: os.environ/META_API_KEYWSS /v1/realtime?model=meta/muse-voice-transcribe-1.0&intent=transcriptionerrorevent and closes with code1011and reasonUnsupported model: muse-voice-transcribe-1.0After: the same developer receives OpenAI-compatible transcription events from Muse
meta/muse-voice-transcribe-1.0to the model list withapi_key: os.environ/META_API_KEYWSS /v1/realtime?model=meta/muse-voice-transcribe-1.0&intent=transcriptionsession.created, send a GAsession.updatewithaudio/pcmat 16 or 24 kHz, and receivesession.updatedinput_audio_buffer.appendPCM16 chunks, theninput_audio_buffer.commit(push to talk) orinput_audio_buffer.end(server VAD)speech_started, transcription deltas,speech_stopped, and acompletedevent carryingusage: {"type": "duration", "seconds": ...}per turnitem_id, and a silent stretch never stalls the next utterance1000once Muse has no more segments, and the spend row shows the billed durationRelevant issues
Docs: BerriAI/litellm-docs#1138
Linear ticket
Pre-Submission checklist
tests/test_litellm/llms/meta/realtime,tests/test_litellm/realtime_api/test_main.py,tests/test_litellm/litellm_core_utils/test_realtime_streaming.py,tests/test_litellm/litellm_core_utils/test_realtime_errors.py: 255 passed)Screenshots / Proof of Fix
Shared setup:
python litellm/proxy/proxy_cli.py --config muse_config.yaml --port 4041 --detailed_debug --use_v2_migration_resolverwithLITELLM_LOCAL_MODEL_COST_MAP=TrueandMETA_API_KEYin the environmentws://localhost:4041/v1/realtime?model=meta/muse-voice-transcribe-1.0&intent=transcriptionwithAuthorization: Bearer sk-1234tests/e2e/llm_translation/realtime/fixtures/weather_question_24k.wav, mono PCM16 at 24 kHz, 1.35 swss://api.meta.ai/v1/asr/realtime, real Meta spendsession.updatefirst, then base64input_audio_buffer.appendframes of 100 ms each (turn_detectionisnullfor push to talk,{"type": "server_vad"}for server VAD):{"type": "session.update", "session": {"type": "transcription", "audio": {"input": { "format": {"type": "audio/pcm", "rate": 24000}, "transcription": {"model": "meta/muse-voice-transcribe-1.0"}, "turn_detection": null}}}}Before (7672399)
Push to talk, fixture once, then commit
session.updateaboveServer VAD, 2 s silence, fixture, 2 s silence, fixture, 2 s silence, then end
session.updateabove with"turn_detection": {"type": "server_vad"}After (0e435e4)
Push to talk, fixture once, then commit
session.updateabove, streamed the fixture as 14 append frames (64,766 bytes), then sentinput_audio_buffer.commitresponse_cost: 6.8e-05, which is 1.36 s at the recordedinput_cost_per_secondof 0.00005Server VAD, 2 s silence, fixture, 2 s silence, fixture, 2 s silence, then end
session.updateabove with"turn_detection": {"type": "server_vad"}, streamed 87 append frames (417,532 bytes, 8.7 s) at real time, then sentinput_audio_buffer.endresponse_cost: 0.000436, which is 8.72 s at 0.00005 per second: 6.8 s rode on the twocompletedevents and the trailing 1.92 s of silence was billed on session closeGuardrail on a transcription session
[realtime guardrail] BLOCKED transcript (violation 1)thenending session after violation 1; the backend never receivedresponse.create. Before 0e435e4 the transcription branch skipped the guardrail, so no error was sent and the session stayed openType
🆕 New Feature
Caveats (if any)
Medium
speechCompletecloses its turn immediately in both modes; a laterspeechEndfor that turn is a no-opitem_id, not in first-started orderLow
speechCompletewith text for an already stopped turn emits a barecompletedfor thatitem_idspeech_stoppedoncespeechStartfor a finished turn is ignored and never re-captures turnless transcriptsspeechEnduntil itsspeechComplete, so post-processing partials land on the rightitem_idrealtime_input_transcriptionguardrail on, the shared realtime guardrail code rewrites a client'sturn_detection: nullinto{"create_response": false}before Muse sees it, so a push to talk client runs in ENDPOINTING and Meta closes the socket with 1008Ingress below real-timeonce it stops streaming. Server VAD clients are unaffected and the injection predates this PRtests/e2e/land in a follow-up PRFinal Attestation