From 330f36c5612580911aa1c86f15f03576ad017bd5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:39:17 +0000 Subject: [PATCH 1/3] fix(router): handle moderation text batch inputs --- litellm/router.py | 8 ++++++++ tests/unit/test_router/test_router.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/litellm/router.py b/litellm/router.py index 7993b5b921c..ec4705e6f14 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -14347,6 +14347,14 @@ class Router: from litellm.litellm_core_utils.prompt_templates.common_utils import request_contains_image_content from litellm.litellm_core_utils.prompt_templates.factory import resolve_structured_messages + input_data: Final = request_kwargs.get("input") + if ( + not messages + and isinstance(input_data, list) + and all(isinstance(item, str) for item in cast(list[object], input_data)) + ): + return False + structured: Final = resolve_structured_messages( messages=cast(list[dict[str, object]] | None, messages), # cast-ok: chat messages are plain dicts request_kwargs=cast(dict[str, object], request_kwargs), # cast-ok: the resolver only reads the mapping diff --git a/tests/unit/test_router/test_router.py b/tests/unit/test_router/test_router.py index 568a05e8ff4..caeec3c135c 100644 --- a/tests/unit/test_router/test_router.py +++ b/tests/unit/test_router/test_router.py @@ -30099,6 +30099,20 @@ def test_image_routing_drop_declared_text_only_legs_for_responses_input_image(): assert _candidate_ids(router, request_kwargs={"input": _IMAGE_RESPONSES_INPUT}) == ["vision"] +@pytest.mark.parametrize( + ("messages", "expected"), + [(None, ("vision", "declared-text")), (_IMAGE_CHAT, ("vision",))], + ids=["text-batch", "image-messages-take-precedence"], +) +def test_image_routing_handles_text_batch_input_without_overriding_messages( + messages: list[dict[str, object]] | None, expected: tuple[str, ...] +) -> None: + router: Final = _mixed_router(_VISION_LEG, _DECLARED_TEXT_LEG) + assert tuple( + _candidate_ids(router, messages=messages, request_kwargs={"input": ["safe text", "more safe text"]}) + ) == expected + + def test_image_routing_send_image_to_the_group_when_every_leg_is_declared_text_only(): other_text_leg = {**_DECLARED_TEXT_LEG, "model_info": {"id": "declared-text-2", "supports_vision": False}} router: Final = _mixed_router(_DECLARED_TEXT_LEG, other_text_leg) From 4dbcdefc2dce3fd1e606426526b5e7c4a2e15654 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:01:47 +0000 Subject: [PATCH 2/3] fix(router): avoid unchecked cast in text batch check --- litellm/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/router.py b/litellm/router.py index ec4705e6f14..1fc7ee3cb15 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -14351,7 +14351,7 @@ class Router: if ( not messages and isinstance(input_data, list) - and all(isinstance(item, str) for item in cast(list[object], input_data)) + and all(isinstance(item, str) for item in input_data) ): return False From 3458e4aae1081d6d154fd5df8bd8ec3570455b14 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:27:57 +0000 Subject: [PATCH 3/3] fix(router): validate text batch without cast --- litellm/router.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index 1fc7ee3cb15..6c5d47467d0 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -610,6 +610,7 @@ _SESSION_ADAPTER: Final = TypeAdapter(Mapping[str, object]) _MODEL_INFO_ADAPTER: Final = TypeAdapter(Mapping[str, object]) _LITELLM_PARAMS_ADAPTER: Final = TypeAdapter(Mapping[str, object]) _SILENT_MODEL_ADAPTER: Final = TypeAdapter(str | list[str]) +_TEXT_INPUT_BATCH_ADAPTER: Final = TypeAdapter(list[str]) _RESOLVED_RETRY_POLICY_ADAPTER: Final = TypeAdapter(RetryPolicy | None) _ROUTING_KWARGS_ADAPTER: Final[TypeAdapter[Mapping[str, object] | None]] = TypeAdapter(Mapping[str, object] | None) _FALLBACK_HOP_ADAPTER: Final = TypeAdapter(Mapping[str, object]) @@ -14348,12 +14349,13 @@ class Router: from litellm.litellm_core_utils.prompt_templates.factory import resolve_structured_messages input_data: Final = request_kwargs.get("input") - if ( - not messages - and isinstance(input_data, list) - and all(isinstance(item, str) for item in input_data) - ): - return False + if not messages: + try: + _TEXT_INPUT_BATCH_ADAPTER.validate_python(input_data, strict=True) + except ValidationError: + pass + else: + return False structured: Final = resolve_structured_messages( messages=cast(list[dict[str, object]] | None, messages), # cast-ok: chat messages are plain dicts