From dfc22d31b428c9ba6fb61357cd6cf5438918e3dc Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Thu, 18 Jun 2026 15:24:35 -0500 Subject: [PATCH 1/8] feat(vertex-ai): add veo 3.1 lite model metadata --- ...odel_prices_and_context_window_backup.json | 15 ++++ model_prices_and_context_window.json | 15 ++++ .../test_vertex_video_transformation.py | 81 ++++++++++++++++++- 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index b12e4a9fea3b..335805069776 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -39343,6 +39343,21 @@ "video" ] }, + "vertex_ai/veo-3.1-lite-generate-001": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.05, + "output_cost_per_second_1080p": 0.08, + "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing#veo", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, "voyage/rerank-2": { "input_cost_per_token": 5e-08, "litellm_provider": "voyage", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index b12e4a9fea3b..335805069776 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -39343,6 +39343,21 @@ "video" ] }, + "vertex_ai/veo-3.1-lite-generate-001": { + "litellm_provider": "vertex_ai-video-models", + "max_input_tokens": 1024, + "max_tokens": 1024, + "mode": "video_generation", + "output_cost_per_second": 0.05, + "output_cost_per_second_1080p": 0.08, + "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing#veo", + "supported_modalities": [ + "text" + ], + "supported_output_modalities": [ + "video" + ] + }, "voyage/rerank-2": { "input_cost_per_token": 5e-08, "litellm_provider": "voyage", 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 55197d3165c2..a0fe57a6eba8 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 @@ -4,12 +4,16 @@ import base64 import json -import os -from unittest.mock import MagicMock, Mock, patch +from pathlib import Path +from typing import Mapping, cast +from unittest.mock import Mock, patch import httpx import pytest +import litellm +from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider +from litellm.llms.openai.cost_calculation import video_generation_cost from litellm.llms.vertex_ai.videos.transformation import ( VertexAIVideoConfig, _convert_image_to_vertex_format, @@ -17,6 +21,21 @@ from litellm.types.router import GenericLiteLLMParams from litellm.types.videos.main import VideoObject +VEO_31_LITE_VERTEX_MODEL = "vertex_ai/veo-3.1-lite-generate-001" +ROOT_MODEL_COST_PATH = ( + Path(__file__).parents[5] / "model_prices_and_context_window.json" +) +BACKUP_MODEL_COST_PATH = ( + Path(__file__).parents[5] + / "litellm" + / "model_prices_and_context_window_backup.json" +) +ModelCostMap = Mapping[str, Mapping[str, object]] + + +def _load_model_cost_map(path: Path) -> ModelCostMap: + return cast(ModelCostMap, json.loads(path.read_text())) + class TestVertexAIVideoConfig: """Test VertexAIVideoConfig transformation class.""" @@ -123,6 +142,64 @@ def test_get_complete_url_default_location(self): # Should NOT include endpoint assert not url.endswith(":predictLongRunning") + def test_veo_31_lite_model_cost_entries_match_pricing(self): + for path in (ROOT_MODEL_COST_PATH, BACKUP_MODEL_COST_PATH): + model_cost = _load_model_cost_map(path) + info = model_cost.get(VEO_31_LITE_VERTEX_MODEL) + + assert info is not None, f"{VEO_31_LITE_VERTEX_MODEL} missing from {path}" + assert info["litellm_provider"] == "vertex_ai-video-models" + assert info["mode"] == "video_generation" + assert info["max_input_tokens"] == 1024 + assert info["output_cost_per_second"] == 0.05 + assert info["output_cost_per_second_1080p"] == 0.08 + + def test_veo_31_lite_provider_routing_from_local_model_map(self): + original_model_cost = litellm.model_cost + original_vertex_video_models = set(litellm.vertex_ai_video_models) + + try: + model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) + litellm.model_cost = dict(model_cost) + litellm.vertex_ai_video_models.clear() + litellm.add_known_models(model_cost_map=litellm.model_cost) + + model, custom_llm_provider, _, _ = get_llm_provider( + model="veo-3.1-lite-generate-001" + ) + + assert model == "veo-3.1-lite-generate-001" + assert custom_llm_provider == "vertex_ai" + finally: + litellm.model_cost = original_model_cost + litellm.vertex_ai_video_models.clear() + litellm.vertex_ai_video_models.update(original_vertex_video_models) + + def test_veo_31_lite_cost_uses_resolution_tiers(self): + model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) + model_info = model_cost[VEO_31_LITE_VERTEX_MODEL] + + assert ( + video_generation_cost( + model=VEO_31_LITE_VERTEX_MODEL, + duration_seconds=10.0, + custom_llm_provider="vertex_ai", + model_info=dict(model_info), + video_resolution="720p", + ) + == 0.5 + ) + assert ( + video_generation_cost( + model=VEO_31_LITE_VERTEX_MODEL, + duration_seconds=10.0, + custom_llm_provider="vertex_ai", + model_info=dict(model_info), + video_resolution="1080p", + ) + == 0.8 + ) + def test_transform_video_create_request(self): """Test transformation of video creation request.""" prompt = "A cat playing with a ball of yarn" From cc8e128d53e4c25d2da8c1b035ce435a735777dc Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Thu, 18 Jun 2026 16:26:12 -0500 Subject: [PATCH 2/8] test(vertex-ai): tighten veo lite review fixes --- .../test_vertex_video_transformation.py | 67 ++++++++----------- 1 file changed, 29 insertions(+), 38 deletions(-) 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 a0fe57a6eba8..927c7dd94b2d 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 @@ -154,51 +154,42 @@ def test_veo_31_lite_model_cost_entries_match_pricing(self): assert info["output_cost_per_second"] == 0.05 assert info["output_cost_per_second_1080p"] == 0.08 - def test_veo_31_lite_provider_routing_from_local_model_map(self): - original_model_cost = litellm.model_cost - original_vertex_video_models = set(litellm.vertex_ai_video_models) - - try: - model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) - litellm.model_cost = dict(model_cost) - litellm.vertex_ai_video_models.clear() - litellm.add_known_models(model_cost_map=litellm.model_cost) + def test_veo_31_lite_provider_routing_from_local_model_map( + self, monkeypatch: pytest.MonkeyPatch + ): + model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) + vertex_video_models = { + model_name.removeprefix("vertex_ai/") + for model_name, info in model_cost.items() + if info.get("litellm_provider") == "vertex_ai-video-models" + } + monkeypatch.setattr(litellm, "vertex_ai_video_models", vertex_video_models) - model, custom_llm_provider, _, _ = get_llm_provider( - model="veo-3.1-lite-generate-001" - ) + model, custom_llm_provider, _, _ = get_llm_provider( + model="veo-3.1-lite-generate-001" + ) - assert model == "veo-3.1-lite-generate-001" - assert custom_llm_provider == "vertex_ai" - finally: - litellm.model_cost = original_model_cost - litellm.vertex_ai_video_models.clear() - litellm.vertex_ai_video_models.update(original_vertex_video_models) + assert model == "veo-3.1-lite-generate-001" + assert custom_llm_provider == "vertex_ai" def test_veo_31_lite_cost_uses_resolution_tiers(self): model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) model_info = model_cost[VEO_31_LITE_VERTEX_MODEL] - assert ( - video_generation_cost( - model=VEO_31_LITE_VERTEX_MODEL, - duration_seconds=10.0, - custom_llm_provider="vertex_ai", - model_info=dict(model_info), - video_resolution="720p", - ) - == 0.5 - ) - assert ( - video_generation_cost( - model=VEO_31_LITE_VERTEX_MODEL, - duration_seconds=10.0, - custom_llm_provider="vertex_ai", - model_info=dict(model_info), - video_resolution="1080p", - ) - == 0.8 - ) + assert video_generation_cost( + model=VEO_31_LITE_VERTEX_MODEL, + duration_seconds=10.0, + custom_llm_provider="vertex_ai", + model_info=dict(model_info), + video_resolution="720p", + ) == pytest.approx(0.5) + assert video_generation_cost( + model=VEO_31_LITE_VERTEX_MODEL, + duration_seconds=10.0, + custom_llm_provider="vertex_ai", + model_info=dict(model_info), + video_resolution="1080p", + ) == pytest.approx(0.8) def test_transform_video_create_request(self): """Test transformation of video creation request.""" From 1e4b30e4ed198fa532af923f39f38134af1f8a1d Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Thu, 18 Jun 2026 17:35:28 -0500 Subject: [PATCH 3/8] fix(vertex-ai): map veo video size to resolution --- .../llms/vertex_ai/videos/transformation.py | 42 +++++++++++++++---- ...odel_prices_and_context_window_backup.json | 3 +- model_prices_and_context_window.json | 3 +- .../test_vertex_video_transformation.py | 39 +++++++++++++++++ 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index d28f5b5b1202..3e7859c49a8d 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -91,6 +91,13 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase): 3. Extract video data (base64) from response """ + _OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: dict[str, str] = { + "1280x720": "16:9", + "1920x1080": "16:9", + "720x1280": "9:16", + "1080x1920": "9:16", + } + def __init__(self): BaseVideoConfig.__init__(self) VertexBase.__init__(self) @@ -133,6 +140,8 @@ def map_openai_params( - prompt → prompt (in instances) - input_reference → image (in instances) - size → aspectRatio (e.g., "1280x720" → "16:9") + - size → resolution when inferable ("1280x720"/"720x1280" → "720p", + "1920x1080"/"1080x1920" → "1080p"); skipped if ``resolution`` is already set - seconds → durationSeconds (defaults to 4 seconds if not provided) """ mapped_params: Final[dict[str, Any]] = {} @@ -147,6 +156,9 @@ def map_openai_params( if "parameters" in video_create_optional_params: mapped_params["parameters"] = video_create_optional_params["parameters"] + if "resolution" in video_create_optional_params: + mapped_params["resolution"] = video_create_optional_params["resolution"] + # Map size to aspectRatio if "size" in video_create_optional_params: size: Final = video_create_optional_params["size"] @@ -154,6 +166,15 @@ def map_openai_params( aspect_ratio: Final = self._convert_size_to_aspect_ratio(size) if aspect_ratio: mapped_params["aspectRatio"] = aspect_ratio + nested_params: Final = video_create_optional_params.get("parameters") + has_resolution = "resolution" in mapped_params or ( + isinstance(nested_params, dict) + and nested_params.get("resolution") is not None + ) + if not has_resolution: + inferred_resolution = self._convert_size_to_resolution(size) + if inferred_resolution is not None: + mapped_params["resolution"] = inferred_resolution # Map seconds to durationSeconds, default to 4 seconds (matching OpenAI) if "seconds" in video_create_optional_params: @@ -177,14 +198,21 @@ def _convert_size_to_aspect_ratio(self, size: str) -> str | None: if not size: return None - aspect_ratio_map: Final = { - "1280x720": "16:9", - "1920x1080": "16:9", - "720x1280": "9:16", - "1080x1920": "9:16", - } + return self._OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO.get(size, "16:9") - return aspect_ratio_map.get(size, "16:9") + def _convert_size_to_resolution(self, size: str) -> str | None: + if not size or size not in self._OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: + return None + try: + width, height = size.split("x", 1) + smaller_edge = min(int(width), int(height)) + except (ValueError, TypeError): + return None + if smaller_edge == 720: + return "720p" + if smaller_edge == 1080: + return "1080p" + return None def validate_environment( self, diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 335805069776..80cb98d91f54 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -39352,7 +39352,8 @@ "output_cost_per_second_1080p": 0.08, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing#veo", "supported_modalities": [ - "text" + "text", + "image" ], "supported_output_modalities": [ "video" diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 335805069776..80cb98d91f54 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -39352,7 +39352,8 @@ "output_cost_per_second_1080p": 0.08, "source": "https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing#veo", "supported_modalities": [ - "text" + "text", + "image" ], "supported_output_modalities": [ "video" 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 927c7dd94b2d..ec0aa187ff0b 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 @@ -153,6 +153,7 @@ def test_veo_31_lite_model_cost_entries_match_pricing(self): assert info["max_input_tokens"] == 1024 assert info["output_cost_per_second"] == 0.05 assert info["output_cost_per_second_1080p"] == 0.08 + assert info["supported_modalities"] == ["text", "image"] def test_veo_31_lite_provider_routing_from_local_model_map( self, monkeypatch: pytest.MonkeyPatch @@ -284,6 +285,44 @@ def test_map_openai_params(self): assert mapped["durationSeconds"] == 8 assert mapped["aspectRatio"] == "16:9" + assert mapped["resolution"] == "720p" + + def test_map_openai_size_to_1080p_resolution(self): + mapped = self.config.map_openai_params( + video_create_optional_params={"size": "1920x1080"}, + model=VEO_31_LITE_VERTEX_MODEL, + drop_params=False, + ) + + assert mapped["aspectRatio"] == "16:9" + assert mapped["resolution"] == "1080p" + + def test_map_openai_size_does_not_override_provider_resolution(self): + mapped = self.config.map_openai_params( + video_create_optional_params={ + "size": "1920x1080", + "parameters": {"resolution": "720p"}, + }, + model=VEO_31_LITE_VERTEX_MODEL, + drop_params=False, + ) + + assert mapped["aspectRatio"] == "16:9" + assert "resolution" not in mapped + assert mapped["parameters"] == {"resolution": "720p"} + + def test_map_openai_size_does_not_override_direct_resolution(self): + mapped = self.config.map_openai_params( + video_create_optional_params={ + "size": "1920x1080", + "resolution": "720p", + }, + model=VEO_31_LITE_VERTEX_MODEL, + drop_params=False, + ) + + assert mapped["aspectRatio"] == "16:9" + assert mapped["resolution"] == "720p" def test_map_openai_params_default_duration(self): """Test that durationSeconds is omitted when not provided.""" From 9012842c2ffd3d93f0ff2a372de87c787280b3fb Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Wed, 15 Jul 2026 21:04:48 -0500 Subject: [PATCH 4/8] fix(vertex-ai): gate veo resolution inference --- .../llms/vertex_ai/videos/transformation.py | 30 ++++++++----------- litellm/types/videos/main.py | 1 + .../test_vertex_video_transformation.py | 24 ++++++++++++--- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index 3e7859c49a8d..9389a5f419d1 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -97,6 +97,12 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase): "720x1280": "9:16", "1080x1920": "9:16", } + _OPENAI_VIDEO_SIZE_TO_RESOLUTION: dict[str, str] = { + "1280x720": "720p", + "1920x1080": "1080p", + "720x1280": "720p", + "1080x1920": "1080p", + } def __init__(self): BaseVideoConfig.__init__(self) @@ -140,8 +146,9 @@ def map_openai_params( - prompt → prompt (in instances) - input_reference → image (in instances) - size → aspectRatio (e.g., "1280x720" → "16:9") - - size → resolution when inferable ("1280x720"/"720x1280" → "720p", - "1920x1080"/"1080x1920" → "1080p"); skipped if ``resolution`` is already set + - size → resolution for Veo 3 models when inferable + ("1280x720"/"720x1280" → "720p", "1920x1080"/"1080x1920" → "1080p"); + skipped if ``resolution`` is already set - seconds → durationSeconds (defaults to 4 seconds if not provided) """ mapped_params: Final[dict[str, Any]] = {} @@ -168,10 +175,10 @@ def map_openai_params( mapped_params["aspectRatio"] = aspect_ratio nested_params: Final = video_create_optional_params.get("parameters") has_resolution = "resolution" in mapped_params or ( - isinstance(nested_params, dict) - and nested_params.get("resolution") is not None + isinstance(nested_params, dict) and nested_params.get("resolution") is not None ) - if not has_resolution: + supports_resolution = model.removeprefix("vertex_ai/").startswith("veo-3.") + if supports_resolution and not has_resolution: inferred_resolution = self._convert_size_to_resolution(size) if inferred_resolution is not None: mapped_params["resolution"] = inferred_resolution @@ -201,18 +208,7 @@ def _convert_size_to_aspect_ratio(self, size: str) -> str | None: return self._OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO.get(size, "16:9") def _convert_size_to_resolution(self, size: str) -> str | None: - if not size or size not in self._OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: - return None - try: - width, height = size.split("x", 1) - smaller_edge = min(int(width), int(height)) - except (ValueError, TypeError): - return None - if smaller_edge == 720: - return "720p" - if smaller_edge == 1080: - return "1080p" - return None + return self._OPENAI_VIDEO_SIZE_TO_RESOLUTION.get(size) def validate_environment( self, diff --git a/litellm/types/videos/main.py b/litellm/types/videos/main.py index 3677cec3c8fd..4c57efe05b0a 100644 --- a/litellm/types/videos/main.py +++ b/litellm/types/videos/main.py @@ -76,6 +76,7 @@ class VideoCreateOptionalRequestParams(TypedDict, total=False): image: Any | None # Image for image-to-video; dict with gcsUri/bytesBase64Encoded, or file-like object parameters: dict[str, Any] | None # Provider-specific parameters block passed directly to the API model: str | None + resolution: str | None seconds: str | None size: str | None characters: list[dict[str, str]] | None 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 ec0aa187ff0b..1c2952cbb0b9 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 @@ -285,17 +285,33 @@ def test_map_openai_params(self): assert mapped["durationSeconds"] == 8 assert mapped["aspectRatio"] == "16:9" - assert mapped["resolution"] == "720p" + assert "resolution" not in mapped - def test_map_openai_size_to_1080p_resolution(self): + @pytest.mark.parametrize( + ("size", "expected_resolution"), + (("1280x720", "720p"), ("1920x1080", "1080p")), + ) + def test_map_openai_size_to_resolution_for_veo_3( + self, size: str, expected_resolution: str + ): mapped = self.config.map_openai_params( - video_create_optional_params={"size": "1920x1080"}, + video_create_optional_params={"size": size}, model=VEO_31_LITE_VERTEX_MODEL, drop_params=False, ) assert mapped["aspectRatio"] == "16:9" - assert mapped["resolution"] == "1080p" + assert mapped["resolution"] == expected_resolution + + def test_map_openai_size_does_not_infer_resolution_for_veo_2(self): + mapped = self.config.map_openai_params( + video_create_optional_params={"size": "1920x1080"}, + model="vertex_ai/veo-2.0-generate-001", + drop_params=False, + ) + + assert mapped["aspectRatio"] == "16:9" + assert "resolution" not in mapped def test_map_openai_size_does_not_override_provider_resolution(self): mapped = self.config.map_openai_params( From d6e859cc97cc99d8fa249627f4459c23fe75d8c3 Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Wed, 15 Jul 2026 21:22:39 -0500 Subject: [PATCH 5/8] fix(vertex-ai): gate resolution inference by pricing metadata --- .../llms/vertex_ai/videos/transformation.py | 11 ++++- .../test_vertex_video_transformation.py | 45 ++++++++++++++++--- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index 9389a5f419d1..bac3d31501bb 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -12,6 +12,7 @@ import httpx from httpx._types import RequestFiles +import litellm from litellm.constants import DEFAULT_GOOGLE_VIDEO_DURATION_SECONDS from litellm.images.utils import ImageEditRequestUtils from litellm.llms.base_llm.videos.transformation import BaseVideoConfig @@ -146,7 +147,7 @@ def map_openai_params( - prompt → prompt (in instances) - input_reference → image (in instances) - size → aspectRatio (e.g., "1280x720" → "16:9") - - size → resolution for Veo 3 models when inferable + - size → resolution for models with resolution-tier pricing when inferable ("1280x720"/"720x1280" → "720p", "1920x1080"/"1080x1920" → "1080p"); skipped if ``resolution`` is already set - seconds → durationSeconds (defaults to 4 seconds if not provided) @@ -177,7 +178,7 @@ def map_openai_params( has_resolution = "resolution" in mapped_params or ( isinstance(nested_params, dict) and nested_params.get("resolution") is not None ) - supports_resolution = model.removeprefix("vertex_ai/").startswith("veo-3.") + supports_resolution = self._supports_resolution_inference(model) if supports_resolution and not has_resolution: inferred_resolution = self._convert_size_to_resolution(size) if inferred_resolution is not None: @@ -210,6 +211,12 @@ def _convert_size_to_aspect_ratio(self, size: str) -> str | None: def _convert_size_to_resolution(self, size: str) -> str | None: return self._OPENAI_VIDEO_SIZE_TO_RESOLUTION.get(size) + @staticmethod + def _supports_resolution_inference(model: str) -> bool: + model_key = model if model.startswith("vertex_ai/") else f"vertex_ai/{model}" + model_info = litellm.model_cost.get(model_key, {}) + return model_info.get("output_cost_per_second_1080p") is not None + def validate_environment( self, headers: dict, 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 1c2952cbb0b9..11cea7c3d233 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 @@ -288,15 +288,33 @@ def test_map_openai_params(self): assert "resolution" not in mapped @pytest.mark.parametrize( - ("size", "expected_resolution"), - (("1280x720", "720p"), ("1920x1080", "1080p")), + ("model", "size", "expected_resolution"), + ( + (VEO_31_LITE_VERTEX_MODEL, "1280x720", "720p"), + ( + VEO_31_LITE_VERTEX_MODEL.removeprefix("vertex_ai/"), + "1920x1080", + "1080p", + ), + ), ) - def test_map_openai_size_to_resolution_for_veo_3( - self, size: str, expected_resolution: str + def test_map_openai_size_to_resolution_for_resolution_tier_model( + self, + model: str, + size: str, + expected_resolution: str, + monkeypatch: pytest.MonkeyPatch, ): + model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) + monkeypatch.setitem( + litellm.model_cost, + VEO_31_LITE_VERTEX_MODEL, + dict(model_cost[VEO_31_LITE_VERTEX_MODEL]), + ) + mapped = self.config.map_openai_params( video_create_optional_params={"size": size}, - model=VEO_31_LITE_VERTEX_MODEL, + model=model, drop_params=False, ) @@ -313,6 +331,23 @@ def test_map_openai_size_does_not_infer_resolution_for_veo_2(self): assert mapped["aspectRatio"] == "16:9" assert "resolution" not in mapped + def test_map_openai_size_does_not_infer_resolution_for_existing_veo_3( + self, monkeypatch: pytest.MonkeyPatch + ): + model = "veo-3.1-generate-001" + model_key = f"vertex_ai/{model}" + model_cost = _load_model_cost_map(BACKUP_MODEL_COST_PATH) + monkeypatch.setitem(litellm.model_cost, model_key, dict(model_cost[model_key])) + + mapped = self.config.map_openai_params( + video_create_optional_params={"size": "1920x1080"}, + model=model, + drop_params=False, + ) + + assert mapped["aspectRatio"] == "16:9" + assert "resolution" not in mapped + def test_map_openai_size_does_not_override_provider_resolution(self): mapped = self.config.map_openai_params( video_create_optional_params={ From d40bf05b119a01599c2254e527521eb2da6d1035 Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Wed, 12 Aug 2026 12:15:30 -0500 Subject: [PATCH 6/8] style(vertex-ai): update video test imports --- .../llms/vertex_ai/videos/test_vertex_video_transformation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 11cea7c3d233..f4d3c95a2b11 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 @@ -4,8 +4,9 @@ import base64 import json +from collections.abc import Mapping from pathlib import Path -from typing import Mapping, cast +from typing import cast from unittest.mock import Mock, patch import httpx From 6f3a7c80ca58bf611a087ad533502906deccf85c Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Wed, 12 Aug 2026 12:56:51 -0500 Subject: [PATCH 7/8] style(vertex-ai): annotate Veo class mappings --- litellm/llms/vertex_ai/videos/transformation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index bac3d31501bb..eb368245eb75 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -7,7 +7,7 @@ import base64 import time -from typing import TYPE_CHECKING, Any, Final, cast +from typing import TYPE_CHECKING, Any, ClassVar, Final, cast import httpx from httpx._types import RequestFiles @@ -92,13 +92,13 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase): 3. Extract video data (base64) from response """ - _OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: dict[str, str] = { + _OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: ClassVar[dict[str, str]] = { "1280x720": "16:9", "1920x1080": "16:9", "720x1280": "9:16", "1080x1920": "9:16", } - _OPENAI_VIDEO_SIZE_TO_RESOLUTION: dict[str, str] = { + _OPENAI_VIDEO_SIZE_TO_RESOLUTION: ClassVar[dict[str, str]] = { "1280x720": "720p", "1920x1080": "1080p", "720x1280": "720p", From bbe9fc78e9bb3492831bdcaa1f16a6b432e7d448 Mon Sep 17 00:00:00 2001 From: Emerson Gomes Date: Wed, 12 Aug 2026 13:39:06 -0500 Subject: [PATCH 8/8] style(vertex-ai): satisfy Veo quality gates --- .../llms/vertex_ai/videos/transformation.py | 36 +++++++++++-------- litellm/types/videos/main.py | 4 +-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/litellm/llms/vertex_ai/videos/transformation.py b/litellm/llms/vertex_ai/videos/transformation.py index eb368245eb75..b925f82c8869 100644 --- a/litellm/llms/vertex_ai/videos/transformation.py +++ b/litellm/llms/vertex_ai/videos/transformation.py @@ -7,6 +7,8 @@ import base64 import time +from collections.abc import Mapping +from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar, Final, cast import httpx @@ -92,18 +94,22 @@ class VertexAIVideoConfig(BaseVideoConfig, VertexBase): 3. Extract video data (base64) from response """ - _OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: ClassVar[dict[str, str]] = { - "1280x720": "16:9", - "1920x1080": "16:9", - "720x1280": "9:16", - "1080x1920": "9:16", - } - _OPENAI_VIDEO_SIZE_TO_RESOLUTION: ClassVar[dict[str, str]] = { - "1280x720": "720p", - "1920x1080": "1080p", - "720x1280": "720p", - "1080x1920": "1080p", - } + _OPENAI_VIDEO_SIZE_TO_ASPECT_RATIO: ClassVar[Mapping[str, str]] = MappingProxyType( + { + "1280x720": "16:9", + "1920x1080": "16:9", + "720x1280": "9:16", + "1080x1920": "9:16", + } + ) + _OPENAI_VIDEO_SIZE_TO_RESOLUTION: ClassVar[Mapping[str, str]] = MappingProxyType( + { + "1280x720": "720p", + "1920x1080": "1080p", + "720x1280": "720p", + "1080x1920": "1080p", + } + ) def __init__(self): BaseVideoConfig.__init__(self) @@ -213,9 +219,9 @@ def _convert_size_to_resolution(self, size: str) -> str | None: @staticmethod def _supports_resolution_inference(model: str) -> bool: - model_key = model if model.startswith("vertex_ai/") else f"vertex_ai/{model}" - model_info = litellm.model_cost.get(model_key, {}) - return model_info.get("output_cost_per_second_1080p") is not None + model_key: Final = model if model.startswith("vertex_ai/") else f"vertex_ai/{model}" + model_info: Final = litellm.model_cost.get(model_key) + return model_info is not None and model_info.get("output_cost_per_second_1080p") is not None def validate_environment( self, diff --git a/litellm/types/videos/main.py b/litellm/types/videos/main.py index 4c57efe05b0a..99b08f6caf6b 100644 --- a/litellm/types/videos/main.py +++ b/litellm/types/videos/main.py @@ -2,7 +2,7 @@ from openai.types.audio.transcription_create_params import FileTypes from pydantic import BaseModel -from typing_extensions import TypedDict +from typing_extensions import ReadOnly, TypedDict class VideoObject(BaseModel): @@ -76,7 +76,7 @@ class VideoCreateOptionalRequestParams(TypedDict, total=False): image: Any | None # Image for image-to-video; dict with gcsUri/bytesBase64Encoded, or file-like object parameters: dict[str, Any] | None # Provider-specific parameters block passed directly to the API model: str | None - resolution: str | None + resolution: ReadOnly[str | None] seconds: str | None size: str | None characters: list[dict[str, str]] | None