1414This pattern can be replicated for other message formats (e.g., Anthropic).
1515"""
1616
17- from typing import TYPE_CHECKING , Any , Dict , List , Optional , Tuple , cast
17+ from typing import TYPE_CHECKING , Any , Dict , List , Optional , Tuple , Union , cast
1818
1919import litellm
2020from litellm ._logging import verbose_proxy_logger
2121from litellm .llms .base_llm .guardrail_translation .base_translation import BaseTranslation
22- from litellm .types .utils import Choices
22+ from litellm .types .utils import Choices , StreamingChoices
2323
2424if TYPE_CHECKING :
2525 from litellm .integrations .custom_guardrail import CustomGuardrail
26- from litellm .types .utils import ModelResponse
26+ from litellm .types .utils import ModelResponse , ModelResponseStream
2727
2828
2929class OpenAIChatCompletionsHandler (BaseTranslation ):
@@ -241,21 +241,79 @@ async def process_output_response(
241241
242242 return response
243243
244- def _has_text_content (self , response : "ModelResponse" ) -> bool :
244+ async def process_output_streaming_response (
245+ self ,
246+ response : "ModelResponseStream" ,
247+ guardrail_to_apply : "CustomGuardrail" ,
248+ litellm_logging_obj : Optional [Any ] = None ,
249+ user_api_key_dict : Optional [Any ] = None ,
250+ ) -> Any :
251+ """
252+ Process output streaming response by applying guardrails to text content.
253+
254+ Args:
255+ response: LiteLLM ModelResponseStream object
256+ guardrail_to_apply: The guardrail instance to apply
257+ litellm_logging_obj: Optional logging object
258+ user_api_key_dict: User API key metadata to pass to guardrails
259+
260+ Returns:
261+ Modified response with guardrail applied to content
262+
263+ Response Format Support:
264+ - String content: choice.message.content = "text here"
265+ - List content: choice.message.content = [{"type": "text", "text": "text here"}, ...]
266+ """
267+
268+ # Step 0: Check if response has any text content to process
269+ if not self ._has_text_content (response ):
270+ return response
271+
272+ texts_to_check : List [str ] = []
273+ images_to_check : List [str ] = []
274+ task_mappings : List [Tuple [int , Optional [int ]]] = []
275+ # Track (choice_index, content_index) for each text
276+
277+ # Step 1: Extract all text content and images from response choices
278+ for choice_idx , choice in enumerate (response .choices ):
279+
280+ self ._extract_output_text_and_images (
281+ choice = choice ,
282+ choice_idx = choice_idx ,
283+ texts_to_check = texts_to_check ,
284+ images_to_check = images_to_check ,
285+ task_mappings = task_mappings ,
286+ )
287+
288+ def _has_text_content (
289+ self , response : Union ["ModelResponse" , "ModelResponseStream" ]
290+ ) -> bool :
245291 """
246292 Check if response has any text content to process.
247293
248294 Override this method to customize text content detection.
249295 """
250- for choice in response .choices :
251- if isinstance (choice , litellm .Choices ):
252- if choice .message .content and isinstance (choice .message .content , str ):
253- return True
296+ from litellm .types .utils import ModelResponse , ModelResponseStream
297+
298+ if isinstance (response , ModelResponse ):
299+ for choice in response .choices :
300+ if isinstance (choice , litellm .Choices ):
301+ if choice .message .content and isinstance (
302+ choice .message .content , str
303+ ):
304+ return True
305+ elif isinstance (response , ModelResponseStream ):
306+ for choice in response .choices :
307+ if isinstance (choice , litellm .Choices ):
308+ if choice .message .content and isinstance (
309+ choice .message .content , str
310+ ):
311+ return True
254312 return False
255313
256314 def _extract_output_text_and_images (
257315 self ,
258- choice : Any ,
316+ choice : Union [ Choices , StreamingChoices ] ,
259317 choice_idx : int ,
260318 texts_to_check : List [str ],
261319 images_to_check : List [str ],
@@ -266,21 +324,29 @@ def _extract_output_text_and_images(
266324
267325 Override this method to customize text/image extraction logic.
268326 """
269- if not isinstance (choice , litellm .Choices ):
270- return
271-
272327 verbose_proxy_logger .debug (
273328 "OpenAI Chat Completions: Processing choice: %s" , choice
274329 )
275330
276- if choice .message .content and isinstance (choice .message .content , str ):
331+ # Determine content source based on choice type
332+ content = None
333+ if isinstance (choice , litellm .Choices ):
334+ content = choice .message .content
335+ elif isinstance (choice , litellm .StreamingChoices ):
336+ content = choice .delta .content
337+ else :
338+ # Unknown choice type, skip processing
339+ return
340+
341+ # Process content if it exists
342+ if content and isinstance (content , str ):
277343 # Simple string content
278- texts_to_check .append (choice . message . content )
344+ texts_to_check .append (content )
279345 task_mappings .append ((choice_idx , None ))
280346
281- elif choice . message . content and isinstance (choice . message . content , list ):
347+ elif content and isinstance (content , list ):
282348 # List content (e.g., multimodal response)
283- for content_idx , content_item in enumerate (choice . message . content ):
349+ for content_idx , content_item in enumerate (content ):
284350 # Extract text
285351 content_text = content_item .get ("text" )
286352 if content_text :
0 commit comments