litellm/tests/unit/videos/test_utils.py
Mateo Wang 98acc01e0d
feat(videos): add first-class OpenRouter video request fields (#50319)
* feat(videos): add first-class OpenRouter video request fields

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(videos): remove recursive image parameter fallback

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(videos): preserve EdenAI native parameter passthrough

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(videos): deduplicate frame roles in linear time

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(videos): preserve image selection and dropped parameter semantics

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(videos): preserve provider scalar parameters and raw extra body

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(videos): retain accepted scalar inputs for Vertex and DashScope

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-11-06 16:50:48 -08:00

215 lines
7.6 KiB
Python

"""
Pure-logic contract tests for litellm/videos/main.py's request utils
(litellm/videos/utils.py: VideoGenerationRequestUtils).
These lock the exact param-shaping behavior so a mutation that drops a filter,
flips a precedence, or stops removing a key fails loudly. The only seam is the
provider config's map_openai_params (a provider boundary); filter_out_litellm_params
runs for real, so the "litellm-internal params get stripped" assertions reflect
production. Every test asserts the exact resulting dict, never "ran without error".
"""
from unittest.mock import MagicMock
from typing import Final
import pytest
import litellm
from litellm.exceptions import UnsupportedParamsError
from litellm.llms.volcengine.videos.transformation import VolcEngineVideoConfig
from litellm.llms.vertex_ai.videos.transformation import VertexAIVideoConfig
from litellm.videos.utils import VideoGenerationRequestUtils
get_requested = (
VideoGenerationRequestUtils.get_requested_video_generation_optional_param
)
@pytest.mark.parametrize("drop_params", [False, True])
def test_extra_body_video_fields_remain_raw(drop_params: bool) -> None:
extra: Final = {"seed": 0, "duration": 6, "aspect_ratio": "1:1", "generate_audio": False,
"frame_images": [], "input_references": []}
params: Final = get_requested({"extra_body": extra})
assert get_optional("veo-3.1-generate-001", VertexAIVideoConfig(), params, drop_params=drop_params) == extra
get_optional = VideoGenerationRequestUtils.get_optional_params_video_generation
# =========================================================================== #
# get_requested_video_generation_optional_param
#
# Receives the caller's full local_vars; must return only the API-bound optional
# params. filter_out_litellm_params strips known internal keys for real; the
# values used below were chosen against the live set: seconds/size/user/foo_param/
# vertex_project/extra/a/b survive, api_key/metadata/litellm_* are stripped.
# =========================================================================== #
def test_requested__drops_none_and_excluded_keys():
result = get_requested(
{
"seconds": "8",
"size": None, # None -> dropped
"prompt": "a sunset", # excluded
"model": "sora-2", # excluded
"user": "u1",
}
)
assert result == {"seconds": "8", "user": "u1"}
def test_requested__strips_litellm_internal_params():
result = get_requested(
{
"seconds": "8",
"api_key": "sk-secret",
"metadata": {"x": 1},
"litellm_call_id": "id-123",
}
)
assert result == {"seconds": "8"}
def test_requested__timeout_always_removed():
# timeout is NOT a litellm-internal param, so only the explicit pop removes it.
result = get_requested({"seconds": "8", "timeout": 30})
assert result == {"seconds": "8"}
def test_requested__nested_kwargs_merge_and_override_base():
result = get_requested(
{"seconds": "8", "kwargs": {"size": "720x1280", "seconds": "override"}}
)
# nested kwargs win over the top-level base params on collision.
assert result == {"seconds": "override", "size": "720x1280"}
def test_requested__non_dict_kwargs_treated_as_empty():
result = get_requested({"seconds": "8", "kwargs": "not-a-dict"})
assert result == {"seconds": "8"}
def test_requested__none_input_returns_empty():
assert get_requested(None) == {}
def test_requested__top_level_extra_body_spread_and_preserved():
result = get_requested(
{"seconds": "8", "extra_body": {"vertex_project": "proj", "foo_param": "bar"}}
)
# extra_body keys are both spread at top level AND kept under "extra_body".
assert result == {
"seconds": "8",
"vertex_project": "proj",
"foo_param": "bar",
"extra_body": {"vertex_project": "proj", "foo_param": "bar"},
}
def test_requested__extra_body_kwargs_overrides_top_level():
result = get_requested(
{
"extra_body": {"a": "top", "b": "top_b"},
"kwargs": {"extra_body": {"a": "kw"}},
}
)
# kwargs' extra_body wins over the top-level extra_body on collision; the
# non-colliding top-level key survives.
assert result == {
"a": "kw",
"b": "top_b",
"extra_body": {"a": "kw", "b": "top_b"},
}
def test_requested__extra_body_strips_litellm_internal_params():
result = get_requested({"extra_body": {"api_key": "sk", "foo_param": "bar"}})
# api_key filtered out of extra_body; only foo_param remains (and is spread).
assert result == {"foo_param": "bar", "extra_body": {"foo_param": "bar"}}
def test_requested__empty_extra_body_not_added():
result = get_requested({"seconds": "8", "extra_body": {}})
assert result == {"seconds": "8"}
assert "extra_body" not in result
# =========================================================================== #
# get_optional_params_video_generation
#
# Delegates mapping to the provider config (the seam) then folds extra_body in.
# =========================================================================== #
def _config(map_return):
config = MagicMock()
config.map_openai_params.return_value = map_return
return config
def test_optional__delegates_to_map_openai_params_with_drop_params():
config = _config({"seconds": "8"})
optional_params = {"seconds": "8"}
result = get_optional(
model="sora-2",
video_generation_provider_config=config,
video_generation_optional_params=optional_params,
)
assert result == {"seconds": "8"}
config.map_openai_params.assert_called_once_with(
video_create_optional_params=optional_params,
model="sora-2",
drop_params=litellm.drop_params,
)
def test_optional__extra_body_overrides_mapped_and_is_removed():
# mapped output carries a leftover extra_body that must be popped; the input
# extra_body overrides a colliding mapped key and is spread in.
config = _config({"seconds": "8", "size": "mapped", "extra_body": {"leftover": 1}})
result = get_optional(
model="sora-2",
video_generation_provider_config=config,
video_generation_optional_params={
"extra_body": {"size": "override", "extra": "x"}
},
)
assert result == {"seconds": "8", "size": "override", "extra": "x"}
assert "extra_body" not in result
def test_optional__non_dict_extra_body_ignored():
config = _config({"seconds": "8"})
result = get_optional(
model="sora-2",
video_generation_provider_config=config,
video_generation_optional_params={"seconds": "8", "extra_body": None},
)
assert result == {"seconds": "8"}
@pytest.mark.parametrize("extra_body", [
{"content": [{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}, "role": "reference_image"}]},
{"input_reference": "https://example.com/ref.png"},
])
def test_extra_body_overrides_mapped_images_after_dropping_conflicts(extra_body: dict[str, object]) -> None:
params: Final = get_requested({
"kwargs": {
"frame_images": [{"type": "image_url", "image_url": {"url": "https://example.com/first.png"}, "frame_type": "first_frame"}],
"extra_body": extra_body,
},
})
config: Final = VolcEngineVideoConfig()
with pytest.raises(UnsupportedParamsError, match="cannot be combined"):
get_optional("model", config, params)
assert get_optional("model", config, params, drop_params=True) == {
"content": [{"type": "image_url", "image_url": {"url": "https://example.com/first.png"}, "role": "first_frame"}],
**extra_body,
}