From 8692e42a6eeb63c71436a43272b60cb7776c9749 Mon Sep 17 00:00:00 2001 From: andrewwhitecdw Date: Tue, 11 Aug 2026 10:07:45 -0500 Subject: [PATCH] fix: content-filter responses raise instead of normalizing ## Summary `_response()` raised `ValueError("LiteLLM returned no text content")` for any response with empty `content`, including content-filter responses where empty output is expected. This caused legitimate content-filter results to fail instead of being normalized. ## Root cause The guard at the end of `_response()` did not distinguish between a truly empty/invalid response and a `content_filter` finish reason, which by design returns no content. ## Fix Allow empty `content` when the finish reason is `content_filter`; keep the existing raise for all other empty-content cases so unexpected responses still fail fast. ```diff - if not content: - raise ValueError("LiteLLM returned no text content") + if not content and choice.finish_reason != "content_filter": + raise ValueError("LiteLLM returned no text content") ``` ## Testing Added `test_response_content_filter_empty_content` to `examples/experimental/litellm/tests/test_client.py` and verified the full non-e2e suite passes: ``` uv run --project examples/experimental/litellm --python 3.12 \ pytest examples/experimental/litellm/tests/test_client.py -m "not e2e" -v # 34 passed ``` ## Contributor guidelines - DCO sign-off included. - One focused commit per PR. Signed-off-by: andrewwhitecdw --- .../litellm/src/switchyard_litellm/client.py | 2 +- .../experimental/litellm/tests/test_client.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/examples/experimental/litellm/src/switchyard_litellm/client.py b/examples/experimental/litellm/src/switchyard_litellm/client.py index dc0286c1f..04cc9a9a3 100644 --- a/examples/experimental/litellm/src/switchyard_litellm/client.py +++ b/examples/experimental/litellm/src/switchyard_litellm/client.py @@ -290,7 +290,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 5e3654816..3fd821b7e 100644 --- a/examples/experimental/litellm/tests/test_client.py +++ b/examples/experimental/litellm/tests/test_client.py @@ -552,3 +552,25 @@ 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 types import SimpleNamespace + + from switchyard_litellm.client import _response + + response = SimpleNamespace( + id="chatcmpl-test", + model="openai/strong", + choices=[ + SimpleNamespace( + message=SimpleNamespace(content=None, tool_calls=None), + finish_reason="content_filter", + ) + ], + usage=None, + ) + result = _response(response) + assert result["outputs"][0]["content"] == [] + assert result["outputs"][0]["stop_reason"] == "content_filter"