From 3b797ada9659f6bfd434851e1e36ce7f4914f85c Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Mon, 23 Feb 2026 21:02:06 -0800 Subject: [PATCH] fix: address latest Greptile review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove fastapi import from SDK-layer file; check for status_code/detail attrs instead to identify guardrail-block exceptions vs programming errors - Add store_message() before continue in transcription interception so transcription events are logged in the non-provider_config path - Inject create_response=false on session.created in provider_config path (Gemini etc.) to match the OpenAI path — prevents LLM auto-responding before guardrail runs on VAD-detected turns --- .../litellm_core_utils/realtime_streaming.py | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/litellm/litellm_core_utils/realtime_streaming.py b/litellm/litellm_core_utils/realtime_streaming.py index 89087ec150c..b42bf9e5711 100644 --- a/litellm/litellm_core_utils/realtime_streaming.py +++ b/litellm/litellm_core_utils/realtime_streaming.py @@ -160,26 +160,23 @@ class RealTimeStreaming: request_data={"user_api_key_dict": self.user_api_key_dict}, input_type="request", ) - except (ValueError, Exception) as e: - # Only treat HTTPException (guardrail block) and ValueError as intentional blocks. - # Re-raise unexpected programming errors so they surface in logs. - from fastapi import HTTPException - - if not isinstance(e, (HTTPException, ValueError)): + except Exception as e: + # Re-raise unexpected errors (no status_code/detail = programming bug, not a block). + # HTTPException and guardrail-raised exceptions have a status_code or detail attr. + is_guardrail_block = hasattr(e, "status_code") or isinstance(e, ValueError) + if not is_guardrail_block: verbose_logger.exception( "[realtime guardrail] unexpected error in apply_guardrail: %s", e ) raise - # Extract the human-readable error from HTTPException detail dict, - # falling back to str(e) for ValueError. - try: - detail = e.detail # type: ignore[attr-defined] - safe_msg = ( - detail.get("error") - if isinstance(detail, dict) - else str(detail) - ) or "I'm sorry, that request was blocked by the content filter." - except AttributeError: + # Extract the human-readable error from the detail dict (HTTPException) + # or fall back to str(e) for plain ValueError. + detail = getattr(e, "detail", None) + if isinstance(detail, dict): + safe_msg = detail.get("error") or str(e) + elif detail is not None: + safe_msg = str(detail) + else: safe_msg = str(e) or "I'm sorry, that request was blocked by the content filter." # Cancel any in-flight response before speaking the warning. # This handles the race where create_response fired before we could intercept. @@ -253,6 +250,23 @@ class RealTimeStreaming: if isinstance(transformed_response, list) else [transformed_response] ) + for event in events: + ## GUARDRAIL: inject create_response=false on session.created + if isinstance(event, dict) and event.get("type") == "session.created": + if self._has_realtime_guardrails(): + await self.backend_ws.send( + json.dumps( + { + "type": "session.update", + "session": { + "turn_detection": { + "type": "server_vad", + "create_response": False, + } + }, + } + ) + ) for event in events: event_str = json.dumps(event) ## GUARDRAIL: run on transcription events in provider_config path too @@ -308,6 +322,8 @@ class RealTimeStreaming: == "conversation.item.input_audio_transcription.completed" ): transcript = event_obj.get("transcript", "") + ## LOGGING — must happen before continue below + self.store_message(raw_response) # Forward transcript to client so user sees what they said await self.websocket.send_text(raw_response) blocked = await self.run_realtime_guardrails(