diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 9c222f03c8c..54e97457218 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -148,10 +148,12 @@ class DeepSeekChatConfig(OpenAIGPTConfig): DeepSeek vision models accept image_url content blocks in user messages (https://api-docs.deepseek.com/guides/vision), so those content lists are forwarded as-is, with any search_results text - appended as a trailing text block. Every other message keeps the - historical string collapse (which also folds search_results text - into string content); a list with no extractable text stays - unchanged, matching what DeepSeek historically received. + appended as a trailing text block. Tool message image lists are kept + too, so the parent transform can move their images into a user + message. Every other message keeps the historical string collapse + (which also folds search_results text into string content); a list + with no extractable text stays unchanged, matching what DeepSeek + historically received. """ forward_images: Final = any( isinstance(message.get("content"), list) for message in messages @@ -187,13 +189,13 @@ class DeepSeekChatConfig(OpenAIGPTConfig): def _is_vision_forwardable_content(self, message: AllMessageValues, content: Sequence[object]) -> bool: """ - True only for a user message whose content list holds well-formed + True only for a user or tool message whose content list holds well-formed text and image_url blocks with at least one image; a block missing its payload falls back to the string collapse instead of crashing or reaching the wire malformed. The model capability gate lives in the caller. """ - if message.get("role") != "user": + if message.get("role") not in ("user", "tool"): return False if not all(self._is_forwardable_block(block) for block in content): return False diff --git a/tests/unit/llms/deepseek/chat/test_deepseek_chat_transformation.py b/tests/unit/llms/deepseek/chat/test_deepseek_chat_transformation.py index 160ed3950b3..a93c26d720f 100644 --- a/tests/unit/llms/deepseek/chat/test_deepseek_chat_transformation.py +++ b/tests/unit/llms/deepseek/chat/test_deepseek_chat_transformation.py @@ -295,6 +295,33 @@ class TestDeepSeekVisionMultimodalContent: assert result[0]["content"] == "what is in this image?" + def test_tool_result_image_reaches_vision_model(self): + tool_message = {**self._image_message(role="tool"), "tool_call_id": "call_1"} + image_block = tool_message["content"][1] + + result = self.config._transform_messages([tool_message], model=self.VISION_MODEL) + + tool_result, image_carrier = result + assert tool_result["role"] == "tool" + assert tool_result["tool_call_id"] == "call_1" + assert image_block not in tool_result["content"] + assert image_carrier["role"] == "user" + assert image_block in image_carrier["content"] + + def test_tool_result_image_carrier_placed_directly_after_tool_result(self): + for next_message in ( + {"role": "user", "content": "so what do you see?"}, + {"role": "assistant", "content": "I see an image."}, + ): + tool_message = {**self._image_message(role="tool"), "tool_call_id": "call_1"} + image_block = tool_message["content"][1] + + result = self.config._transform_messages([tool_message, next_message], model=self.VISION_MODEL) + + assert [m["role"] for m in result] == ["tool", "user", next_message["role"]] + assert image_block in result[1]["content"] + assert result[2] == next_message + def test_audio_block_collapsed_even_on_vision_model(self): messages = [ {