Skip to content

Commit e33fd0d

Browse files
committed
Fix beta realtime session payloads
1 parent 7e3d8ab commit e33fd0d

2 files changed

Lines changed: 71 additions & 22 deletions

File tree

litellm/litellm_core_utils/realtime_streaming.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -260,23 +260,20 @@ async def _send_to_backend(self, message: str) -> None:
260260
await self.backend_ws.send(message) # type: ignore[union-attr, attr-defined]
261261

262262
def _make_disable_auto_response_message(self) -> str:
263-
"""Return a GA-shaped session.update that disables VAD auto-response.
264-
265-
The guardrail injection sites historically sent a beta-style payload
266-
with a flat ``turn_detection`` key. When the upstream operates in GA
267-
mode (no ``OpenAI-Beta: realtime=v1`` header), the backend requires
268-
the nested shape ``session.audio.input.turn_detection`` and a required
269-
``session.type`` field. This helper always produces the correct shape
270-
so both injection sites are GA-safe regardless of client protocol.
271-
"""
272-
session: Dict[str, Any] = {
273-
"type": "realtime",
274-
"audio": {
275-
"input": {
276-
"turn_detection": {"create_response": False},
277-
}
278-
},
279-
}
263+
"""Return a session.update that disables VAD auto-response."""
264+
if self._client_wants_beta:
265+
session: Dict[str, Any] = {
266+
"turn_detection": {"create_response": False},
267+
}
268+
else:
269+
session = {
270+
"type": "realtime",
271+
"audio": {
272+
"input": {
273+
"turn_detection": {"create_response": False},
274+
}
275+
},
276+
}
280277
return json.dumps({"type": "session.update", "session": session})
281278

282279
def _has_realtime_guardrails(self) -> bool:
@@ -813,11 +810,10 @@ async def client_ack_messages(self):
813810
self._pending_guardrail_message = None
814811
continue
815812

816-
# GA compatibility: remap beta-style session fields to GA shape.
817-
# Always applied so that beta-format clients work against a GA
818-
# backend. When _client_wants_beta is True the proxy also
819-
# translates GA events back to beta names on the return path.
820-
if msg_type == "session.update":
813+
# GA compatibility: remap beta-style session fields only when
814+
# the upstream is in GA mode. Beta upstreams expect the flat
815+
# session shape unchanged.
816+
if msg_type == "session.update" and not self._client_wants_beta:
821817
session = msg_obj.get("session", {})
822818
if isinstance(session, dict):
823819
session = self._remap_beta_session_to_ga(session)

tests/test_litellm/litellm_core_utils/test_realtime_streaming.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,59 @@ def test_make_disable_auto_response_message_produces_ga_shape():
121121
assert session["audio"]["input"]["turn_detection"]["create_response"] is False
122122

123123

124+
def test_make_disable_auto_response_message_produces_beta_shape_for_beta_clients():
125+
websocket = MagicMock()
126+
websocket.scope = {"headers": [(b"openai-beta", b"realtime=v1")]}
127+
backend_ws = MagicMock()
128+
logging_obj = MagicMock()
129+
streaming = RealTimeStreaming(websocket, backend_ws, logging_obj)
130+
131+
raw = streaming._make_disable_auto_response_message()
132+
msg = json.loads(raw)
133+
134+
assert msg["type"] == "session.update"
135+
session = msg["session"]
136+
assert session == {"turn_detection": {"create_response": False}}
137+
138+
139+
@pytest.mark.asyncio
140+
async def test_client_ack_messages_keeps_beta_session_shape_for_beta_clients():
141+
client_ws = MagicMock()
142+
client_ws.scope = {"headers": [(b"openai-beta", b"realtime=v1")]}
143+
session_update = json.dumps(
144+
{
145+
"type": "session.update",
146+
"session": {
147+
"modalities": ["audio", "text"],
148+
"voice": "alloy",
149+
"turn_detection": {"create_response": False},
150+
},
151+
}
152+
)
153+
client_ws.receive_text = AsyncMock(
154+
side_effect=[
155+
session_update,
156+
Exception("connection closed"),
157+
]
158+
)
159+
backend_ws = MagicMock()
160+
backend_ws.send = AsyncMock()
161+
logging_obj = MagicMock()
162+
logging_obj.pre_call = MagicMock()
163+
streaming = RealTimeStreaming(client_ws, backend_ws, logging_obj)
164+
165+
await streaming.client_ack_messages()
166+
167+
sent_to_backend = json.loads(backend_ws.send.call_args_list[0].args[0])
168+
session = sent_to_backend["session"]
169+
assert session["modalities"] == ["audio", "text"]
170+
assert session["voice"] == "alloy"
171+
assert session["turn_detection"] == {"create_response": False}
172+
assert "type" not in session
173+
assert "output_modalities" not in session
174+
assert "audio" not in session
175+
176+
124177
def test_translate_event_to_beta_renames_delta_types():
125178
ev = RealTimeStreaming._translate_event_to_beta(
126179
{"type": "response.output_audio.delta", "delta": "abc", "event_id": "e1"}

0 commit comments

Comments
 (0)