diff --git a/litellm/llms/hosted_vllm/chat/transformation.py b/litellm/llms/hosted_vllm/chat/transformation.py index 58ab297a198..ff76dc57bfe 100644 --- a/litellm/llms/hosted_vllm/chat/transformation.py +++ b/litellm/llms/hosted_vllm/chat/transformation.py @@ -21,6 +21,7 @@ from litellm.types.llms.openai import ( ChatCompletionToolCallFunctionChunk, ChatCompletionVideoObject, ChatCompletionVideoUrlObject, + OpenAIMessageContentListBlock, ) from ....utils import remove_additional_properties, remove_strict_from_schema @@ -142,6 +143,12 @@ class HostedVLLMChatConfig(OpenAIGPTConfig): return ChatCompletionVideoObject(type="video_url", video_url=ChatCompletionVideoUrlObject(url=file_data)) raise ValueError("file_id or file_data is required") + def _video_file_as_video_url(self, content_item: OpenAIMessageContentListBlock) -> OpenAIMessageContentListBlock: + if content_item.get("type") != "file": + return content_item + file_item: Final = cast(ChatCompletionFileObject, content_item) + return self._convert_file_to_video_url(file_item) if self._is_video_file(file_item) else content_item + @overload def _transform_messages( self, messages: list[AllMessageValues], model: str, is_async: Literal[True] @@ -224,14 +231,7 @@ class HostedVLLMChatConfig(OpenAIGPTConfig): elif message["role"] == "user": message_content = message.get("content") if message_content and isinstance(message_content, list): - replaced_content_items: list[tuple[int, ChatCompletionFileObject]] = [] - for idx, content_item in enumerate(message_content): - if content_item.get("type") == "file": - content_item = cast(ChatCompletionFileObject, content_item) - if self._is_video_file(content_item): - replaced_content_items.append((idx, content_item)) - for idx, content_item in replaced_content_items: - message_content[idx] = self._convert_file_to_video_url(content_item) + message["content"] = [self._video_file_as_video_url(item) for item in message_content] if is_async: return super()._transform_messages(messages, model, is_async=cast(Literal[True], True)) diff --git a/tests/unit/llms/hosted_vllm/chat/test_hosted_vllm_chat_transformation.py b/tests/unit/llms/hosted_vllm/chat/test_hosted_vllm_chat_transformation.py index c792d5dffcd..7fa8e84d422 100644 --- a/tests/unit/llms/hosted_vllm/chat/test_hosted_vllm_chat_transformation.py +++ b/tests/unit/llms/hosted_vllm/chat/test_hosted_vllm_chat_transformation.py @@ -1,9 +1,11 @@ import json from unittest.mock import MagicMock, patch +import httpx import pytest +import respx - +import litellm from litellm.constants import ( DEFAULT_REASONING_EFFORT_HIGH_THINKING_BUDGET, DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET, @@ -43,6 +45,34 @@ def test_hosted_vllm_chat_transformation_file_url(): ] +def test_hosted_vllm_video_file_conversion_leaves_caller_messages_untouched(): + video_file_part = {"type": "file", "file": {"file_id": "https://example.com/video.mp4", "format": "video/mp4"}} + messages = [{"role": "user", "content": [{"type": "text", "text": "Describe this video"}, video_file_part]}] + with respx.mock() as upstream: + route = upstream.post("http://vllm.test/v1/chat/completions").mock( + return_value=httpx.Response( + 200, + json={ + "id": "chatcmpl-1", + "object": "chat.completion", + "created": 1, + "model": "qwen-vl", + "choices": [ + {"index": 0, "message": {"role": "assistant", "content": "A cat."}, "finish_reason": "stop"} + ], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + }, + ) + ) + litellm.completion(model="hosted_vllm/qwen-vl", messages=messages, api_base="http://vllm.test/v1") + + assert json.loads(route.calls.last.request.content)["messages"][0]["content"][1] == { + "type": "video_url", + "video_url": {"url": "https://example.com/video.mp4"}, + } + assert messages[0]["content"][1] is video_file_part + + def test_hosted_vllm_supports_reasoning_effort(): config = HostedVLLMChatConfig() supported_params = config.get_supported_openai_params(model="hosted_vllm/gpt-oss-120b")