diff --git a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py index 37be832d350e..27fa685eaac8 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py +++ b/litellm/proxy/guardrails/guardrail_hooks/tool_permission.py @@ -799,6 +799,11 @@ async def async_post_call_streaming_iterator_hook( verbose_proxy_logger.debug( "Tool Permission Guardrail: No tool uses found" ) + mock_response = MockResponseIterator( + model_response=assembled_model_response + ) + async for chunk in mock_response: + yield chunk return verbose_proxy_logger.debug( diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py index 716b4470d255..d31dbdd43483 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_tool_permission.py @@ -28,6 +28,7 @@ ChatCompletionMessageToolCall, Choices, ModelResponse, + ModelResponseStream, ) @@ -676,6 +677,49 @@ async def test_async_pre_call_hook_rewrite_mode_filters_legacy_functions(self): assert new_data["function_call"] == "none" assert new_data["tool_choice"] == "none" + @pytest.mark.asyncio + async def test_async_post_call_streaming_iterator_hook_plain_text_yields_chunks( + self, + ): + """Regression test: hook must re-emit chunks when LLM replies with plain text. + + Before the fix, the `if not tool_calls:` branch did a bare `return` inside + the async generator, which yielded nothing. Clients received only + `data: [DONE]` with no content. + """ + text_chunk = ModelResponseStream( + id="chatcmpl-plain-text", + created=1700000000, + model="gpt-4", + object="chat.completion.chunk", + choices=[], + ) + + async def _fake_stream(): + yield text_chunk + + assembled = ModelResponse( + choices=[Choices(message={"content": "Hello, world!"})] + ) + + with patch("litellm.main.stream_chunk_builder", return_value=assembled): + chunks = [] + async for chunk in self.guardrail.async_post_call_streaming_iterator_hook( + user_api_key_dict=UserAPIKeyAuth(), + response=_fake_stream(), + request_data={}, + ): + chunks.append(chunk) + + assert len(chunks) >= 1, ( + "Hook must yield at least one chunk for plain-text responses; " + "got none — bare return bug" + ) + assert chunks[0].choices[0].delta.content == "Hello, world!", ( + "Hook must preserve the original response content; " + f"got: {chunks[0].choices[0].delta.content!r}" + ) + def test_modify_response_with_permission_errors(self): # Setup a response with one tool_call tool_call = ChatCompletionMessageToolCall(