mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
fix(realtime): tolerate non-dict turn_detection in guardrail injection
When a client sends a session.update whose turn_detection field is None or a non-dict value (e.g. "auto"), the guardrail injection used setdefault followed by item assignment on the returned value, raising TypeError. The inner except only caught JSONDecodeError/AttributeError, so the TypeError escaped to the outer Exception handler that wraps the entire client_ack loop, killing the connection. Replace non-dict turn_detection with a fresh dict carrying create_response=False so the guardrail still applies without crashing the loop.
This commit is contained in:
parent
76225f35b1
commit
38e822e41b
2 changed files with 63 additions and 3 deletions
|
|
@ -899,9 +899,11 @@ class RealTimeStreaming:
|
|||
):
|
||||
session = msg_obj.setdefault("session", {})
|
||||
if isinstance(session, dict):
|
||||
session.setdefault("turn_detection", {})[
|
||||
"create_response"
|
||||
] = False
|
||||
existing_td = session.get("turn_detection")
|
||||
if not isinstance(existing_td, dict):
|
||||
existing_td = {}
|
||||
existing_td["create_response"] = False
|
||||
session["turn_detection"] = existing_td
|
||||
message = json.dumps(msg_obj)
|
||||
self._guardrail_turn_detection_update_sent = True
|
||||
verbose_logger.debug(
|
||||
|
|
|
|||
|
|
@ -1368,3 +1368,61 @@ async def test_guardrail_turn_detection_injected_into_first_session_update_defer
|
|||
assert injected_turn_detection is not None
|
||||
assert injected_turn_detection["create_response"] is False
|
||||
assert streaming._guardrail_turn_detection_update_sent is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("existing_turn_detection", [None, "auto", 42, ["server_vad"]])
|
||||
async def test_guardrail_turn_detection_injection_tolerates_non_dict_value(
|
||||
existing_turn_detection,
|
||||
):
|
||||
"""Client-supplied non-dict turn_detection must not crash client_ack_messages."""
|
||||
client_ws = AsyncMock()
|
||||
client_ws.receive_text = AsyncMock(
|
||||
side_effect=[
|
||||
json.dumps({
|
||||
"type": "session.update",
|
||||
"session": {
|
||||
"modalities": ["text", "audio"],
|
||||
"turn_detection": existing_turn_detection,
|
||||
},
|
||||
}),
|
||||
ConnectionClosed(None, None),
|
||||
]
|
||||
)
|
||||
backend_ws = MagicMock()
|
||||
backend_ws.send = AsyncMock()
|
||||
|
||||
logging_obj = MagicMock()
|
||||
logging_obj.litellm_trace_id = "trace_1"
|
||||
logging_obj.async_success_handler = AsyncMock()
|
||||
logging_obj.success_handler = MagicMock()
|
||||
|
||||
provider_config = MagicMock()
|
||||
transformed_messages = []
|
||||
def mock_transform(msg, model, session_config):
|
||||
transformed_messages.append((msg, session_config))
|
||||
return [msg]
|
||||
provider_config.transform_realtime_request = MagicMock(side_effect=mock_transform)
|
||||
|
||||
streaming = RealTimeStreaming(
|
||||
websocket=client_ws,
|
||||
backend_ws=backend_ws,
|
||||
logging_obj=logging_obj,
|
||||
provider_config=provider_config,
|
||||
model="gemini-2.5-flash",
|
||||
)
|
||||
streaming._has_audio_transcription_guardrails = MagicMock(return_value=True) # type: ignore[method-assign]
|
||||
|
||||
await streaming.client_ack_messages()
|
||||
|
||||
assert len(transformed_messages) == 1
|
||||
transformed_msg, _ = transformed_messages[0]
|
||||
msg_obj = json.loads(transformed_msg)
|
||||
session_obj = msg_obj["session"]
|
||||
injected_turn_detection = (
|
||||
session_obj.get("turn_detection")
|
||||
or session_obj.get("audio", {}).get("input", {}).get("turn_detection")
|
||||
)
|
||||
assert isinstance(injected_turn_detection, dict)
|
||||
assert injected_turn_detection["create_response"] is False
|
||||
assert streaming._guardrail_turn_detection_update_sent is True
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue