diff --git a/examples/experimental/litellm/src/switchyard_litellm/client.py b/examples/experimental/litellm/src/switchyard_litellm/client.py index dd83b5b0a..277fbe6fa 100644 --- a/examples/experimental/litellm/src/switchyard_litellm/client.py +++ b/examples/experimental/litellm/src/switchyard_litellm/client.py @@ -300,7 +300,7 @@ def _response(response: ModelResponse) -> dict[str, object]: "name": tool_call.function.name, "arguments": arguments, }) - if not content: + if not content and choice.finish_reason != "content_filter": raise ValueError("LiteLLM returned no text content") return { "id": response.id, diff --git a/examples/experimental/litellm/tests/test_client.py b/examples/experimental/litellm/tests/test_client.py index 157b35c05..0f7e6555a 100644 --- a/examples/experimental/litellm/tests/test_client.py +++ b/examples/experimental/litellm/tests/test_client.py @@ -15,6 +15,7 @@ from litellm.exceptions import ( ContextWindowExceededError as LiteLLMContextWindowExceededError, ) +from litellm.types.utils import Choices, Message, Usage from switchyard_litellm import LiteLLMSyClient from switchyard.libsy import ContextWindowExceededError @@ -582,3 +583,46 @@ async def test_cached_token_count_preserves_explicit_zero() -> None: await client.aclose() assert response["usage"]["cached_input_tokens"] == 0 + + +def test_response_content_filter_empty_content() -> None: + """Empty content with a content_filter finish reason must be normalized.""" + from switchyard_litellm.client import _response + + response = ModelResponse( + id="chatcmpl-test", + model="openai/strong", + choices=[ + Choices( + message=Message(content=None, tool_calls=None), + finish_reason="content_filter", + ) + ], + usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), + ) + result = _response(response) + outputs = result["outputs"] + assert isinstance(outputs, list) + output = outputs[0] + assert isinstance(output, dict) + assert output["content"] == [] + assert output["stop_reason"] == "content_filter" + + +def test_response_empty_content_without_content_filter_raises() -> None: + """Empty content with any other finish reason must still raise.""" + from switchyard_litellm.client import _response + + response = ModelResponse( + id="chatcmpl-test", + model="openai/strong", + choices=[ + Choices( + message=Message(content=None, tool_calls=None), + finish_reason="stop", + ) + ], + usage=Usage(prompt_tokens=0, completion_tokens=0, total_tokens=0), + ) + with pytest.raises(ValueError, match="LiteLLM returned no text content"): + _response(response)