diff --git a/litellm/llms/base_llm/videos/transformation.py b/litellm/llms/base_llm/videos/transformation.py index 50cada42b87..1ad91a43df8 100644 --- a/litellm/llms/base_llm/videos/transformation.py +++ b/litellm/llms/base_llm/videos/transformation.py @@ -118,10 +118,11 @@ def transform_video_content_request( api_base: str, litellm_params: GenericLiteLLMParams, headers: dict, + variant: Optional[str] = None, ) -> Tuple[str, Dict]: """ Transform the video content request into a URL and data/params - + Returns: Tuple[str, Dict]: (url, params) for the video content request """ diff --git a/litellm/llms/custom_httpx/llm_http_handler.py b/litellm/llms/custom_httpx/llm_http_handler.py index 5087f2e0078..ac59ff3d0ed 100644 --- a/litellm/llms/custom_httpx/llm_http_handler.py +++ b/litellm/llms/custom_httpx/llm_http_handler.py @@ -5400,6 +5400,7 @@ def video_content_handler( api_key: Optional[str] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, _is_async: bool = False, + variant: Optional[str] = None, ) -> Union[bytes, Coroutine[Any, Any, bytes]]: """ Handle video content download requests. @@ -5415,6 +5416,7 @@ def video_content_handler( extra_headers=extra_headers, api_key=api_key, client=client, + variant=variant, ) if client is None or not isinstance(client, HTTPHandler): @@ -5446,6 +5448,7 @@ def video_content_handler( api_base=api_base, litellm_params=litellm_params, headers=headers, + variant=variant, ) try: @@ -5488,6 +5491,7 @@ async def async_video_content_handler( extra_headers: Optional[Dict[str, Any]] = None, api_key: Optional[str] = None, client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None, + variant: Optional[str] = None, ) -> bytes: """ Async version of the video content download handler. @@ -5522,6 +5526,7 @@ async def async_video_content_handler( api_base=api_base, litellm_params=litellm_params, headers=headers, + variant=variant, ) try: diff --git a/litellm/llms/gemini/videos/transformation.py b/litellm/llms/gemini/videos/transformation.py index 4120d1cad22..7daeb75b651 100644 --- a/litellm/llms/gemini/videos/transformation.py +++ b/litellm/llms/gemini/videos/transformation.py @@ -393,10 +393,11 @@ def transform_video_content_request( api_base: str, litellm_params: GenericLiteLLMParams, headers: dict, + variant: Optional[str] = None, ) -> Tuple[str, Dict]: """ Transform the video content request for Veo API. - + For Veo, we need to: 1. Get operation status to extract video URI 2. Return download URL for the video diff --git a/litellm/llms/openai/videos/transformation.py b/litellm/llms/openai/videos/transformation.py index 0dd7940a92e..5c880ab6658 100644 --- a/litellm/llms/openai/videos/transformation.py +++ b/litellm/llms/openai/videos/transformation.py @@ -172,18 +172,22 @@ def transform_video_content_request( api_base: str, litellm_params: GenericLiteLLMParams, headers: dict, + variant: Optional[str] = None, ) -> Tuple[str, Dict]: """ Transform the video content request for OpenAI API. - + OpenAI API expects the following request: - GET /v1/videos/{video_id}/content + - GET /v1/videos/{video_id}/content?variant=thumbnail """ original_video_id = extract_original_video_id(video_id) - + # Construct the URL for video content download url = f"{api_base.rstrip('/')}/{original_video_id}/content" - + if variant is not None: + url = f"{url}?variant={variant}" + # No additional data needed for GET content request data: Dict[str, Any] = {} diff --git a/litellm/llms/runwayml/videos/transformation.py b/litellm/llms/runwayml/videos/transformation.py index 5a46ebb664b..318a732dc2a 100644 --- a/litellm/llms/runwayml/videos/transformation.py +++ b/litellm/llms/runwayml/videos/transformation.py @@ -310,10 +310,11 @@ def transform_video_content_request( api_base: str, litellm_params: GenericLiteLLMParams, headers: dict, + variant: Optional[str] = None, ) -> Tuple[str, Dict]: """ Transform the video content request for RunwayML API. - + RunwayML doesn't have a separate content download endpoint. The video URL is returned in the task output field. We'll retrieve the task and extract the video URL. diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index 66cd1437642..8cdccc4cd64 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -455,6 +455,7 @@ def transform_video_content_request( api_base: str, litellm_params: GenericLiteLLMParams, headers: dict, + variant: Optional[str] = None, ) -> Tuple[str, Dict]: """ Transform the video content request for Veo API. diff --git a/litellm/videos/main.py b/litellm/videos/main.py index db09ab04f11..2225b9eec78 100644 --- a/litellm/videos/main.py +++ b/litellm/videos/main.py @@ -273,6 +273,7 @@ def video_content( video_id: str, timeout: Optional[float] = None, custom_llm_provider: Optional[str] = None, + variant: Optional[str] = None, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Optional[Dict[str, Any]] = None, @@ -367,6 +368,7 @@ def video_content( extra_headers=extra_headers, client=kwargs.get("client"), _is_async=_is_async, + variant=variant, ) except Exception as e: @@ -385,6 +387,7 @@ async def avideo_content( video_id: str, timeout: Optional[float] = None, custom_llm_provider: Optional[str] = None, + variant: Optional[str] = None, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. extra_headers: Optional[Dict[str, Any]] = None, @@ -422,6 +425,7 @@ async def avideo_content( video_id=video_id, timeout=timeout, custom_llm_provider=custom_llm_provider, + variant=variant, extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, diff --git a/tests/test_litellm/test_video_generation.py b/tests/test_litellm/test_video_generation.py index 75552d3d100..363e4063073 100644 --- a/tests/test_litellm/test_video_generation.py +++ b/tests/test_litellm/test_video_generation.py @@ -801,6 +801,83 @@ def test_openai_transform_video_content_request_empty_params(): assert params == {} +@pytest.mark.parametrize( + "variant,expected_suffix", + [ + ("thumbnail", "?variant=thumbnail"), + ("spritesheet", "?variant=spritesheet"), + ], +) +def test_openai_transform_video_content_request_with_variant(variant, expected_suffix): + """OpenAI content transform should append ?variant= when variant is provided.""" + config = OpenAIVideoConfig() + url, params = config.transform_video_content_request( + video_id="video_123", + api_base="https://api.openai.com/v1/videos", + litellm_params={}, + headers={}, + variant=variant, + ) + + assert url == f"https://api.openai.com/v1/videos/video_123/content{expected_suffix}" + assert params == {} + + +def test_openai_transform_video_content_request_variant_none_no_query_param(): + """OpenAI content transform should NOT append ?variant= when variant is None.""" + config = OpenAIVideoConfig() + url, params = config.transform_video_content_request( + video_id="video_123", + api_base="https://api.openai.com/v1/videos", + litellm_params={}, + headers={}, + variant=None, + ) + + assert "variant" not in url + assert url == "https://api.openai.com/v1/videos/video_123/content" + + +def test_video_content_handler_passes_variant_to_url(): + """HTTP handler should pass variant through to the final URL.""" + from litellm.llms.custom_httpx.http_handler import HTTPHandler + from litellm.types.router import GenericLiteLLMParams + + if hasattr(litellm, "in_memory_llm_clients_cache"): + litellm.in_memory_llm_clients_cache.flush_cache() + + handler = BaseLLMHTTPHandler() + config = OpenAIVideoConfig() + + mock_client = MagicMock(spec=HTTPHandler) + mock_response = MagicMock() + mock_response.content = b"thumbnail-bytes" + mock_client.get.return_value = mock_response + + with patch( + "litellm.llms.custom_httpx.llm_http_handler._get_httpx_client", + return_value=mock_client, + ): + result = handler.video_content_handler( + video_id="video_abc", + video_content_provider_config=config, + custom_llm_provider="openai", + litellm_params=GenericLiteLLMParams( + api_base="https://api.openai.com/v1" + ), + logging_obj=MagicMock(), + timeout=5.0, + api_key="sk-test", + client=mock_client, + _is_async=False, + variant="thumbnail", + ) + + assert result == b"thumbnail-bytes" + called_url = mock_client.get.call_args.kwargs["url"] + assert called_url == "https://api.openai.com/v1/videos/video_abc/content?variant=thumbnail" + + def test_video_content_handler_uses_get_for_openai(): """HTTP handler must use GET (not POST) for OpenAI content download.""" from litellm.llms.custom_httpx.http_handler import HTTPHandler