fix(bedrock): trigger Nova Sonic generation on response.create so realtime sessions stop hanging - #31924
Conversation
…ltime sessions stop hanging (LIT-2239)
Greptile SummaryThis PR fixes a hang in Nova Sonic v1 realtime text sessions by injecting a short pre-rendered "ready" audio utterance on
Confidence Score: 5/5Safe to merge; all changes are scoped to the Bedrock realtime handler, the fix is verified against the live API, and the new tests cover the complete set of scenarios introduced. The trigger-audio injection, END_TURN response.done mapping, graceful session close, and sample-rate rotation are all well-reasoned and match the live-API behaviour documented in the PR. Cleanup paths use independent suppress blocks so a failed flush never prevents stream closure. Tests are mock-only and cover the full matrix of new code paths including the regression scenarios from the bug report. No files require special attention.
|
| Filename | Overview |
|---|---|
| litellm/llms/bedrock/realtime/transformation.py | Core change: transform_response_create_event now emits a trigger audio block for text-only sessions; END_TURN contentEnd emits response.done; sample-rate rotation for G.711 clients; session_close_messages added. Logic is well-encapsulated and all edge cases are unit-tested. |
| litellm/llms/bedrock/realtime/handler.py | Extracted send_to_bedrock helper; graceful session close on disconnect using contextlib.suppress; None-stream guard in _forward_bedrock_to_client; client WebSocket closed in finally block. Cleanup ordering is sound. |
| litellm/llms/bedrock/realtime/trigger_audio.py | New file: gzip+base64 pre-rendered 16 kHz PCM blob with an lru_cache(maxsize=1) decoder. Decompression cost is paid only once per process lifetime. Data is inert audio bytes with no network calls or code execution. |
| tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_transformation.py | Comprehensive mock-only tests: trigger PCM content and byte-exact payload, second response.create reuse, client-audio no-op guard, sample-rate rotation (G.711 and PCM16), session close ordering, END_TURN vs PARTIAL_TURN response.done discrimination. |
| tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_handler.py | New handler-level mock tests using a stub_aws_models monkeypatch fixture: disconnect flushes session close sequence, pre-session disconnect sends nothing, close flush continues after partial send failure, stream close on flush failure, None stream read closes client WebSocket. No real network calls. |
| ruff-strict-budget.json | Budget file updated (limits adjusted to reflect new code); no functional change. |
Reviews (6): Last reviewed commit: "fix(bedrock): suppress bedrock close sen..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR fixes a Nova Sonic realtime hang by replacing the no-op
Confidence Score: 3/5The trigger audio content block inherits session-negotiated codec fields rather than hardcoding the pcm16 format of the trigger clip, which would send malformed audio declarations to Bedrock for non-pcm16 sessions. The audioInputConfiguration in transform_response_create_event uses self.input_media_type, self.input_sample_size_bits, and self.input_encoding from the negotiated session codec, while the trigger audio is always 16 kHz 16-bit linear PCM. For g711 sessions, Bedrock receives a content block declaring mulaw encoding but containing pcm16 bytes. All new tests use the default pcm16 config, leaving this path unverified. The handler refactoring and response.done logic are correct and well-covered. litellm/llws/bedrock/realtime/transformation.py — specifically transform_response_create_event, where the trigger audio contentStart inherits session codec parameters instead of hardcoded pcm16 values.
|
| Filename | Overview |
|---|---|
| litellm/llws/bedrock/realtime/transformation.py | Core logic change: response.create now injects a pcm16 trigger audio clip, but audioInputConfiguration inherits the session-negotiated codec, causing a format mismatch for non-pcm16 sessions. |
| litellm/llws/bedrock/realtime/handler.py | Clean refactoring: extracts send_to_bedrock closure, adds graceful session_close_messages on disconnect, moves ws.close() to finally block, handles None reads from Bedrock stream. |
| litellm/llws/bedrock/realtime/trigger_audio.py | New file: embeds a gzip-compressed base64-encoded pcm16 Polly utterance; lru_cache(maxsize=1) ensures single decompression per process. |
| tests/test_litellm/llws/bedrock/realtime/test_bedrock_realtime_transformation.py | Adds comprehensive mock-only unit tests for the new response.create trigger logic, session_close_messages ordering, and response.done emission. |
Reviews (2): Last reviewed commit: "fix(bedrock): trigger Nova Sonic generat..." | Re-trigger Greptile
|
Generated by Claude Code |
|
Generated by Claude Code |
|
Generated by Claude Code |
|
bugbot run Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Disconnect flush aborts mid-sequence
- Moved contextlib.suppress inside the for loop so a failed send of one Bedrock close event (e.g. contentEnd or promptEnd) no longer aborts the remaining promptEnd/sessionEnd events, with a new regression test that fails only on promptEnd and asserts sessionEnd still reaches Bedrock.
You can send follow-ups to the cloud agent here.
…tEnd/sessionEnd still flush
|
|
|
Generated by Claude Code |
|
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 40c8338. Configure here.
…ltime sessions stop hanging (BerriAI#31924) * fix(bedrock): trigger Nova Sonic generation on response.create so realtime sessions stop hanging (LIT-2239) * fix(bedrock): reopen audio content at client sample rate after trigger block * test(bedrock): cover realtime handler disconnect flush and stream-end guard * fix(bedrock): always close realtime input stream even if close flush fails * fix(lint): use contextlib.suppress in bedrock realtime cleanup to satisfy BLE001 budget * fix(bedrock): suppress bedrock close send errors per-message so promptEnd/sessionEnd still flush --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Relevant issues
Fixes the Nova Sonic realtime hang: on
bedrock/amazon.nova-sonic-v1:0via/v1/realtime, every text session hung forever becauseresponse.createwas translated to nothing, so the model never generated and Bedrock eventually raisedValidationException("The following prompts were not closed" or "Timed out waiting for input events") after the client gave upLinear ticket
LIT-2239
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewScreenshots / Proof of Fix
Run against a live proxy hitting the real Bedrock API in us-east-1 with
bedrock/amazon.nova-sonic-v1:0(mode: realtime); no mocks. The same minimal OpenAI realtime websocket client (session.update with text modality, one user text item, then response.create) was run once against the merge-base and once against this PR, each on a fresh proxyqa_client.py
The proxy config (
qa_config.yaml):Before (merge-base e141596)
Client output:
About a minute after the client gave up, Bedrock rejected the still-open prompt in proxy_before.log:
After (merged, staging tip c4f28ce containing PR head 40c8338)
Client output (74 consecutive
event: response.audio.deltalines elided for readability, nothing else changed):After the client disconnected, proxy_after.log shows a clean shutdown with no ValidationException at all:
Client to Bedrock forwarding ended: (1000, ''), then the graceful close messages ending in{"event": {"sessionEnd": {}}}, thenBedrock Realtime: Bedrock stream endedType
🐛 Bug Fix
Changes
The old
transform_response_create_eventreturned[]with a comment claiming Bedrock starts generating automatically. That is wrong for Nova Sonic v1: the model only starts generating after it detects user speech in an audio content block, so a text-only session (session.update, conversation.item.create, response.create) never produced a response. Verified against a live bidirectional stream: interactive USER text alone, text plus silent audio, and promptEnd all fail to trigger generation onamazon.nova-sonic-v1:0; promptEnd just closes the prompt without a response, and the prompt is rejected outright if it contains no audio content at allThe working pattern (the same one Pipecat's Nova Sonic integration uses for its assistant response trigger) is to speak to the model.
response.createnow opens the prompt's audio content block and streams a short pre-rendered spoken "ready" utterance (16kHz PCM generated with Amazon Polly, embedded intrigger_audio.py) padded with leading and trailing silence, which makes Nova Sonic run ASR, detect end of turn, and respond to the pending interactive text input. Sessions where the client streams its own audio (input_audio_buffer.append) are untouched; for those,response.createstays a no-op and Nova Sonic's built-in turn detection applies, tracked via aclient_audio_streamedflagOn the inbound side, Nova Sonic v1 never emits a
promptEndoutput event (verified live; the docs' terminal event iscompletionEnd, which v1 also does not send per turn), so clients previously never receivedresponse.doneeven when generation happened. The transformation now emitsresponse.doneand resets per-response state when acontentEndarrives withstopReason: END_TURN, and the inbound dispatch also acceptscompletionEndas an end-of-response signal. The handler additionally closes the session gracefully when the client disconnects (contentEnd for open audio, promptEnd, sessionEnd via the newsession_close_messages), which stops theValidationExceptionnoise Bedrock used to raise after every session, and it treats aNoneread from the Bedrock stream as a normal end of stream instead of crashing on anAttributeErrorFollowing review feedback, the transformation also tracks the sample rate each audio content block was declared with. If the trigger block was opened at 16kHz and the client later streams its own audio on a session configured for a different input rate (e.g. 8kHz G.711),
input_audio_buffer.appendcloses the trigger block, rotates to a new content name, and opens a fresh block declared at the client's configured rate, so client audio is never sent into a block with a mismatched sample rate. In the handler, the disconnect cleanup flushes the graceful close messages and closes the Bedrock input stream in independentcontextlib.suppress(Exception)blocks, so a failed flush can never skip the stream closeRegression tests in
tests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_transformation.pycover:response.createemitting the audio contentStart plus the exact trigger PCM, a secondresponse.createreusing the open audio content,response.createstaying a no-op beforesession.updateand when the client streams its own audio,session_close_messagesordering,response.doneemission onEND_TURN(and not onPARTIAL_TURN), and the sample-rate rotation (g711_ulaw reopens the block at 8kHz, pcm16 keeps reusing it). A newtests/test_litellm/llms/bedrock/realtime/test_bedrock_realtime_handler.pycovers the handler paths: a client disconnect flushes the graceful close sequence to Bedrock and closes the input stream, nothing is sent when no session was started, the input stream is still closed when the close flush itself fails, and aNoneread from the Bedrock stream ends the loop and closes the client WebSocket. All of these fail on the previous behaviorNote
Medium Risk
Changes bidirectional realtime protocol mapping and synthetic audio injection for Nova Sonic; behavior is nuanced but scoped to Bedrock realtime with dedicated regression tests.
Overview
Fixes Nova Sonic realtime text sessions that hung because
response.createpreviously produced no Bedrock input. For text-only flows (no client mic audio),response.createnow streams a short pre-rendered “ready” utterance plus silence viatrigger_audio.py, so the model detects speech and answers pending interactive text. If the client already sendsinput_audio_buffer.append,response.createstays a no-op.Inbound mapping now emits OpenAI
response.donewhen Bedrock sendscontentEndwithstopReason: END_TURN, and treatscompletionEndlikepromptEnd. On disconnect, the handler flushessession_close_messages(contentEnd/promptEnd/sessionEnd), handles aNoneBedrock read as normal EOS, and closes the client socket in afinallyblock.input_audio_buffer.appendcan close and reopen the audio block when the trigger used 16 kHz but the session’s input format differs (e.g. G.711 8 kHz).Regression coverage added in
test_bedrock_realtime_handler.pyand expanded transformation tests for trigger audio, session close, andEND_TURN.Reviewed by Cursor Bugbot for commit 828adff. Bugbot is set up for automated code reviews on this repo. Configure here.