From aeb723816ffbc7e0839337873f05d4ffb9680bc3 Mon Sep 17 00:00:00 2001 From: Sameer Kankute Date: Thu, 26 Feb 2026 14:49:46 +0530 Subject: [PATCH] Fix: Passing of image and parameters in videos api --- .../llms/vertex_ai/videos/transformation.py | 18 +- litellm/types/videos/main.py | 5 +- .../test_vertex_video_transformation.py | 176 +++++++++++++++++- 3 files changed, 193 insertions(+), 6 deletions(-) diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index 8cdccc4cd6..48f454a806 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -119,6 +119,12 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase): # Map input_reference to image (will be processed in transform_video_create_request) if "input_reference" in video_create_optional_params: mapped_params["image"] = video_create_optional_params["input_reference"] + elif "image" in video_create_optional_params: + mapped_params["image"] = video_create_optional_params["image"] + + # Pass through a provider-specific parameters block if provided directly + if "parameters" in video_create_optional_params: + mapped_params["parameters"] = video_create_optional_params["parameters"] # Map size to aspectRatio if "size" in video_create_optional_params: @@ -270,7 +276,17 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase): instance_dict.update(params_copy["instances"]) params_copy.pop("instances") elif "image" in params_copy and params_copy["image"] is not None: - image_data = _convert_image_to_vertex_format(params_copy["image"]) + image = params_copy["image"] + if isinstance(image, dict): + # Already in Vertex format e.g. {"gcsUri": "gs://..."} or + # {"bytesBase64Encoded": "...", "mimeType": "..."} + image_data = image + elif isinstance(image, str) and image.startswith("gs://"): + # Bare GCS URI — Vertex AI accepts gcsUri natively, no download needed + image_data = {"gcsUri": image} + else: + # File-like object — encode to base64 + image_data = _convert_image_to_vertex_format(image) instance_dict["image"] = image_data params_copy.pop("image") diff --git a/litellm/types/videos/main.py b/litellm/types/videos/main.py index 65c4cfe0e0..8e595db39f 100644 --- a/litellm/types/videos/main.py +++ b/litellm/types/videos/main.py @@ -1,7 +1,8 @@ from typing import Any, Dict, List, Literal, Optional -from typing_extensions import TypedDict from pydantic import BaseModel +from typing_extensions import TypedDict + from litellm.types.utils import FileTypes @@ -72,6 +73,8 @@ class VideoCreateOptionalRequestParams(TypedDict, total=False): Params here: https://platform.openai.com/docs/api-reference/videos/create """ input_reference: Optional[FileTypes] # File reference for input image + image: Optional[Any] # Image for image-to-video; dict with gcsUri/bytesBase64Encoded, or file-like object + parameters: Optional[Dict[str, Any]] # Provider-specific parameters block passed directly to the API model: Optional[str] seconds: Optional[str] size: Optional[str] diff --git a/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py b/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py index 7ae344e499..f0c5899e04 100644 --- a/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/videos/test_vertex_video_transformation.py @@ -1,19 +1,20 @@ """ Tests for Vertex AI (Veo) video generation transformation. """ +import base64 import json import os -import pytest -from unittest.mock import Mock, MagicMock, patch +from unittest.mock import MagicMock, Mock, patch + import httpx -import base64 +import pytest from litellm.llms.vertex_ai.videos.transformation import ( VertexAIVideoConfig, _convert_image_to_vertex_format, ) -from litellm.types.videos.main import VideoObject from litellm.types.router import GenericLiteLLMParams +from litellm.types.videos.main import VideoObject class TestVertexAIVideoConfig: @@ -548,3 +549,170 @@ class TestConvertImageToVertexFormat: decoded = base64.b64decode(result["bytesBase64Encoded"]) assert decoded == fake_image_data + +class TestImageAndParametersPassthrough: + """ + Tests that image (gcsUri / bare gs:// / file-like) and a pre-built + parameters dict are correctly forwarded through map_openai_params and + transform_video_create_request. + """ + + def setup_method(self): + self.config = VertexAIVideoConfig() + self.api_base = ( + "https://us-central1-aiplatform.googleapis.com/v1/projects/" + "test-project/locations/us-central1/publishers/google/models/veo-002" + ) + + # ------------------------------------------------------------------ # + # map_openai_params # + # ------------------------------------------------------------------ # + + def test_map_openai_params_passes_image_dict(self): + """image dict (gcsUri format) is forwarded as-is.""" + image = {"gcsUri": "gs://my-bucket/boardwalk.jpg"} + mapped = self.config.map_openai_params( + video_create_optional_params={"image": image}, + model="veo-002", + drop_params=False, + ) + assert mapped["image"] == image + + def test_map_openai_params_passes_parameters_dict(self): + """A pre-built parameters dict is forwarded as-is.""" + params = {"sampleCount": 1, "videoLengthSeconds": 5, "aspectRatio": "16:9"} + mapped = self.config.map_openai_params( + video_create_optional_params={"parameters": params}, + model="veo-002", + drop_params=False, + ) + assert mapped["parameters"] == params + + def test_map_openai_params_input_reference_takes_priority_over_image(self): + """input_reference wins over a directly passed image key.""" + mock_file = Mock() + image_dict = {"gcsUri": "gs://my-bucket/other.jpg"} + mapped = self.config.map_openai_params( + video_create_optional_params={ + "input_reference": mock_file, + "image": image_dict, + }, + model="veo-002", + drop_params=False, + ) + assert mapped["image"] is mock_file + + # ------------------------------------------------------------------ # + # transform_video_create_request – image forms # + # ------------------------------------------------------------------ # + + def test_transform_request_image_gcs_uri_dict(self): + """image passed as {"gcsUri": "gs://..."} is placed in instances as-is.""" + image = {"gcsUri": "gs://my-bucket/boardwalk.jpg"} + data, _, url = self.config.transform_video_create_request( + model="veo-002", + prompt="Cinematic drone shot", + api_base=self.api_base, + video_create_optional_request_params={"image": image}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert data["instances"][0]["image"] == image + assert url.endswith(":predictLongRunning") + + def test_transform_request_image_bare_gs_uri_string(self): + """A bare gs:// string is wrapped in {"gcsUri": ...} without downloading.""" + gs_uri = "gs://my-bucket/boardwalk.jpg" + data, _, _ = self.config.transform_video_create_request( + model="veo-002", + prompt="Cinematic drone shot", + api_base=self.api_base, + video_create_optional_request_params={"image": gs_uri}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert data["instances"][0]["image"] == {"gcsUri": gs_uri} + + def test_transform_request_image_bytes_base64_dict(self): + """image already in bytesBase64Encoded format is passed through unchanged.""" + image = {"bytesBase64Encoded": "abc123", "mimeType": "image/jpeg"} + data, _, _ = self.config.transform_video_create_request( + model="veo-002", + prompt="Cinematic drone shot", + api_base=self.api_base, + video_create_optional_request_params={"image": image}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert data["instances"][0]["image"] == image + + # ------------------------------------------------------------------ # + # transform_video_create_request – parameters dict # + # ------------------------------------------------------------------ # + + def test_transform_request_parameters_dict_not_double_nested(self): + """A pre-built parameters dict becomes request_data["parameters"] directly.""" + params = {"sampleCount": 1, "videoLengthSeconds": 5, "aspectRatio": "16:9"} + data, _, _ = self.config.transform_video_create_request( + model="veo-002", + prompt="Cinematic drone shot", + api_base=self.api_base, + video_create_optional_request_params={"parameters": params}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert data["parameters"] == params + # Must NOT be double-nested + assert "parameters" not in data["parameters"] + + # ------------------------------------------------------------------ # + # Full user scenario # + # ------------------------------------------------------------------ # + + def test_transform_request_full_user_scenario(self): + """ + Reproduces the exact user request: + image: {"gcsUri": "gs://your-bucket-name/path/to/boardwalk.jpg"} + parameters: {"sampleCount": 1, "videoLengthSeconds": 5, + "aspectRatio": "16:9", "storageUri": "gs://test/outputs/"} + """ + image = {"gcsUri": "gs://your-bucket-name/path/to/boardwalk.jpg"} + parameters = { + "sampleCount": 1, + "videoLengthSeconds": 5, + "aspectRatio": "16:9", + "storageUri": "gs://test/outputs/", + } + + # Simulate the full pipeline: map_openai_params → transform_video_create_request + mapped = self.config.map_openai_params( + video_create_optional_params={"image": image, "parameters": parameters}, + model="veo-3.1-generate-preview", + drop_params=False, + ) + + data, _, url = self.config.transform_video_create_request( + model="veo-3.1-generate-preview", + prompt="Cinematic drone shot moving forward along the beach boardwalk", + api_base=self.api_base, + video_create_optional_request_params=mapped, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + # instances contains prompt + image + assert len(data["instances"]) == 1 + instance = data["instances"][0] + assert instance["prompt"] == "Cinematic drone shot moving forward along the beach boardwalk" + assert instance["image"] == image + + # parameters block is correct and not double-nested + assert data["parameters"] == parameters + assert "parameters" not in data["parameters"] + + assert url.endswith(":predictLongRunning") +