mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-03 08:23:16 +00:00
feat(videos): add variant parameter to video content download (#21955)
openai videos models support the features to download variants. See more details here: https://developers.openai.com/api/docs/guides/video-generation#use-image-references. Plumb variant (e.g. "thumbnail", "spritesheet") through the full video content download chain: avideo_content → video_content → video_content_handler → transform_video_content_request. OpenAI appends ?variant=<value> to the GET URL; other providers accept the parameter in their signature but ignore it.
This commit is contained in:
@@ -118,10 +118,11 @@ class BaseVideoConfig(ABC):
|
||||
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
|
||||
"""
|
||||
|
||||
@@ -5400,6 +5400,7 @@ class BaseLLMHTTPHandler:
|
||||
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 @@ class BaseLLMHTTPHandler:
|
||||
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 @@ class BaseLLMHTTPHandler:
|
||||
api_base=api_base,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
variant=variant,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -5488,6 +5491,7 @@ class BaseLLMHTTPHandler:
|
||||
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 @@ class BaseLLMHTTPHandler:
|
||||
api_base=api_base,
|
||||
litellm_params=litellm_params,
|
||||
headers=headers,
|
||||
variant=variant,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -393,10 +393,11 @@ class GeminiVideoConfig(BaseVideoConfig):
|
||||
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
|
||||
|
||||
@@ -172,18 +172,22 @@ class OpenAIVideoConfig(BaseVideoConfig):
|
||||
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] = {}
|
||||
|
||||
|
||||
@@ -310,10 +310,11 @@ class RunwayMLVideoConfig(BaseVideoConfig):
|
||||
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.
|
||||
|
||||
@@ -455,6 +455,7 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase):
|
||||
api_base: str,
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
headers: dict,
|
||||
variant: Optional[str] = None,
|
||||
) -> Tuple[str, Dict]:
|
||||
"""
|
||||
Transform the video content request for Veo API.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user