diff --git a/litellm/router_strategy/auto_router/__init__.py b/litellm/router_strategy/auto_router/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/litellm/router_strategy/auto_router/auto_router.py b/litellm/router_strategy/auto_router/auto_router.py index 6a786115193..922ffe90239 100644 --- a/litellm/router_strategy/auto_router/auto_router.py +++ b/litellm/router_strategy/auto_router/auto_router.py @@ -119,11 +119,14 @@ class AutoRouter(CustomLogger): auto_sync=self.auto_sync_value, ) - user_message: Dict[str, str] = messages[-1] - message_content: str = user_message.get("content", "") - route_choice: Optional[Union[RouteChoice, List[RouteChoice]]] = self.routelayer( - text=message_content - ) + user_message: Dict[str, Any] = messages[-1] + raw_content = user_message.get("content", "") + # Handle Anthropic-style content blocks: [{"type": "text", "text": "..."}] + if isinstance(raw_content, list): + message_content = " ".join(block.get("text", "") for block in raw_content if isinstance(block, dict)) + else: + message_content = str(raw_content) + route_choice: Optional[Union[RouteChoice, List[RouteChoice]]] = self.routelayer(text=message_content) verbose_router_logger.debug(f"route_choice: {route_choice}") if isinstance(route_choice, RouteChoice): model = route_choice.name or self.default_model diff --git a/tests/test_litellm/router_strategy/test_auto_router.py b/tests/test_litellm/router_strategy/test_auto_router.py index 78d128e0044..c0d7bc0ab46 100644 --- a/tests/test_litellm/router_strategy/test_auto_router.py +++ b/tests/test_litellm/router_strategy/test_auto_router.py @@ -181,3 +181,57 @@ class TestAutoRouter: # Assert assert result is None + @pytest.mark.asyncio + @patch("semantic_router.routers.SemanticRouter") + @patch("litellm.router_strategy.auto_router.litellm_encoder.LiteLLMRouterEncoder") + async def test_async_pre_routing_hook_with_anthropic_content_blocks( + self, + mock_encoder_class, + mock_semantic_router_class, + mock_router_instance, + mock_route_choice, + ): + """Test that Anthropic-style list content blocks are joined into a plain string (line 127). + + When a message's content is a list of typed dicts (e.g. Anthropic format), + only dict blocks are considered and their "text" values are joined with a space. + Non-dict entries in the list must be skipped. + """ + # Arrange + mock_loaded_router = MagicMock() + mock_loaded_router.routes = ["route1", "route2"] + mock_semantic_router_class.from_json.return_value = mock_loaded_router + + mock_routelayer = MagicMock() + mock_routelayer.return_value = mock_route_choice + mock_semantic_router_class.return_value = mock_routelayer + + auto_router = AutoRouter( + model_name="test-auto-router", + auto_router_config_path="test/path/router.json", + default_model="gpt-4o-mini", + embedding_model="text-embedding-model", + litellm_router_instance=mock_router_instance, + ) + + # Anthropic-style content blocks: list of dicts with a "type" and "text" key. + # Also includes a non-dict entry to verify it is skipped by isinstance(block, dict). + anthropic_content = [ + {"type": "text", "text": "What is the weather today?"}, + {"type": "text", "text": "In Paris."}, + {"type": "image_url", "image_url": {"url": "http://example.com/img.png"}}, # no "text" key + "unexpected string block", # non-dict — must be ignored + ] + messages = [{"role": "user", "content": anthropic_content}] + + # Act + result = await auto_router.async_pre_routing_hook( + model="test-model", + request_kwargs={}, + messages=messages, + ) + + # Assert: only dict blocks contribute; "text" defaults to "" when missing + mock_routelayer.assert_called_once_with(text="What is the weather today? In Paris. ") + assert result is not None + assert result.messages == messages