From b34023d6bcc46d7e9d220575f5fe80e2c7522625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E6=B6=B5=E7=9A=84=E4=BB=A3=E7=A0=81=E6=97=A5?= =?UTF-8?q?=E8=AE=B0?= Date: Thu, 17 Sep 2026 15:14:06 +0800 Subject: [PATCH] fix(llm): collapse multimodal content with the shared helper (#7527) For response_model calls the messages are flattened into one prompt for InternalInstructor with an f-string, so a multimodal content list reached the model as its Python repr. AGENTS.md's "Message Content" section says never to str() the content; use message_content_text() instead. --- lib/crewai/src/crewai/llm.py | 8 ++- .../llms/test_structured_output_multimodal.py | 62 +++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 lib/crewai/tests/llms/test_structured_output_multimodal.py diff --git a/lib/crewai/src/crewai/llm.py b/lib/crewai/src/crewai/llm.py index 4dda8a77b..0c95e3ddd 100644 --- a/lib/crewai/src/crewai/llm.py +++ b/lib/crewai/src/crewai/llm.py @@ -1263,6 +1263,7 @@ class LLM(BaseLLM): """ if response_model and self.is_litellm: from crewai.hooks.llm_hooks import model_call_hooks_dispatched + from crewai.utilities.agent_utils import message_content_text from crewai.utilities.internal_instructor import InternalInstructor messages = params.get("messages", []) @@ -1270,7 +1271,8 @@ class LLM(BaseLLM): raise ValueError("Messages are required when using response_model") combined_content = "\n\n".join( - f"{msg['role'].upper()}: {msg['content']}" for msg in messages + f"{msg['role'].upper()}: {message_content_text(msg)}" + for msg in messages ) instructor_instance = InternalInstructor( @@ -1420,6 +1422,7 @@ class LLM(BaseLLM): """ if response_model and self.is_litellm: from crewai.hooks.llm_hooks import model_call_hooks_dispatched + from crewai.utilities.agent_utils import message_content_text from crewai.utilities.internal_instructor import InternalInstructor messages = params.get("messages", []) @@ -1427,7 +1430,8 @@ class LLM(BaseLLM): raise ValueError("Messages are required when using response_model") combined_content = "\n\n".join( - f"{msg['role'].upper()}: {msg['content']}" for msg in messages + f"{msg['role'].upper()}: {message_content_text(msg)}" + for msg in messages ) instructor_instance = InternalInstructor( diff --git a/lib/crewai/tests/llms/test_structured_output_multimodal.py b/lib/crewai/tests/llms/test_structured_output_multimodal.py new file mode 100644 index 000000000..2f73879e9 --- /dev/null +++ b/lib/crewai/tests/llms/test_structured_output_multimodal.py @@ -0,0 +1,62 @@ +"""Structured output must collapse multimodal content with the shared helper.""" + +from unittest.mock import patch + +import pytest +from pydantic import BaseModel + +from crewai.llm import LLM + + +class Answer(BaseModel): + text: str + + +MULTIMODAL = [ + { + "role": "user", + "content": [ + {"type": "text", "text": "What is in this image?"}, + {"type": "image_url", "image_url": {"url": "http://example.com/x.png"}}, + ], + } +] + + +def _stub_llm(): + """An LLM with the instructor call captured, no network involved.""" + llm = object.__new__(LLM) + llm.is_litellm = True + llm._handle_emit_call_events = lambda **kwargs: None + captured: dict[str, str] = {} + + class FakeInstructor: + def __init__(self, *, content, model, llm): + captured["content"] = content + + def to_pydantic(self): + return Answer(text="ok") + + patcher = patch( + "crewai.utilities.internal_instructor.InternalInstructor", FakeInstructor + ) + return llm, captured, patcher + + +def test_sync_structured_output_collapses_multimodal_content(): + llm, captured, patcher = _stub_llm() + with patcher: + llm._handle_non_streaming_response( + params={"messages": MULTIMODAL}, response_model=Answer + ) + assert captured["content"] == "USER: What is in this image?" + + +@pytest.mark.asyncio +async def test_async_structured_output_collapses_multimodal_content(): + llm, captured, patcher = _stub_llm() + with patcher: + await llm._ahandle_non_streaming_response( + params={"messages": MULTIMODAL}, response_model=Answer + ) + assert captured["content"] == "USER: What is in this image?"